Files
mattermost/install-update-oidc-mobile-bridge.sh

1167 lines
35 KiB
Bash
Executable File

#!/usr/bin/env bash
script_name="$(basename $(realpath $0))"
working_dir="$(dirname $(realpath $0))"
LOCK_DIR="/tmp/$(basename $0).$$.LOCK"
log_file="${LOCK_DIR}/${script_name%%.*}.log"
backup_date="$(date +%Y-%m-%d-%H%M%S)"
MM_OIDC_REPOSITORY="https://github.com/server-camp/mattermost-oidc-plugin.git"
MM_BRIDGE_SOURCE_BASE="/usr/local/src/mattermost-oidc/mobile-bridge"
MM_BRIDGE_INSTALL_DIR="/usr/local/sbin"
MM_BRIDGE_NAME="mattermost-oidc-mobile-bridge"
MM_BRIDGE_LINK="${MM_BRIDGE_INSTALL_DIR}/${MM_BRIDGE_NAME}"
MM_BRIDGE_SERVICE="mattermost-oidc-mobile-bridge.service"
MM_SERVICE="mattermost.service"
MM_USER="mattermost"
MM_GROUP="mattermost"
MM_BRIDGE_LISTEN="127.0.0.1:8066"
MM_BRIDGE_UPSTREAM="http://127.0.0.1:8065"
MM_CONFIG_FILE="/opt/mattermost/config/config.json"
MM_SITE_URL=""
INSTALLATION_MODE=""
MM_CURRENT_VERSION=""
MM_CURRENT_TARGET=""
MM_NEW_VERSION=""
MM_LATEST_VERSION=""
MM_SOURCE_DIR=""
MM_BRIDGE_BIN=""
NGINX_CONFIG_FILE=""
NGINX_CHANGED=false
SYMLINK_CHANGED=false
UNIT_CREATED=false
ROLLBACK_TARGET=""
LEGACY_BRIDGE_BACKUP=""
# ----------
# Base Function(s)
# ----------
clean_up() {
rm -rf "$LOCK_DIR"
blank_line
exit $1
}
echononl(){
if $terminal ; then
echo X\\c > /tmp/shprompt$$
if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then
echo -e -n " $*\\c" 1>&2
else
echo -e -n " $*" 1>&2
fi
rm /tmp/shprompt$$
fi
}
fatal(){
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mFatal\033[m ] $*"
else
echo -e " [ Fatal ] $*"
fi
echo ""
if $terminal ; then
echo -e " \033[1mScript terminated\033[m.."
else
echo -e " Script terminated.."
fi
echo ""
clean_up 1
}
error (){
echo ""
if $terminal ; then
echo -e " [ \033[31m\033[1mError\033[m ] $*"
else
echo " [ Error ] $*"
fi
echo ""
}
warn (){
echo ""
if $terminal ; then
echo -e " [ \033[33m\033[1mWarning\033[m ] $*"
else
echo " [ Warning ] $*"
fi
echo ""
}
info (){
if $terminal ; then
echo ""
echo -e " [ \033[32m\033[1mInfo\033[m ] $*"
echo ""
fi
}
echo_ok() {
if $terminal ; then
echo -e "\033[85G[ \033[32mok\033[m ]"
fi
}
echo_failed(){
if $terminal ; then
echo -e "\033[85G[ \033[1;31mfailed\033[m ]"
fi
}
echo_skipped() {
if $terminal ; then
echo -e "\033[85G[ \033[33m\033[1mskipped\033[m ]"
fi
}
echo_wait(){
if $terminal ; then
echo -en "\033[85G[ \033[5m\033[1m..\033[m ]"
fi
}
blank_line() {
if $terminal ; then
echo ""
fi
}
ask_yes_no() {
local question="$1"
local default_answer="${2:-no}"
local answer=""
local prompt="[yes/no]"
if [[ "$default_answer" = "yes" ]]; then
prompt="[yes/no, default: yes]"
elif [[ "$default_answer" = "no" ]]; then
prompt="[yes/no, default: no]"
fi
while true ; do
echononl "$question $prompt: "
read answer
answer="${answer,,}"
[[ -z "$answer" ]] && answer="$default_answer"
case "$answer" in
yes) return 0 ;;
no) return 1 ;;
*) warn "Wrong entry! Please enter 'yes' or 'no'." ;;
esac
done
}
rollback_bridge() {
local rollback_failed=false
local rollback_path=""
[[ "$SYMLINK_CHANGED" = true ]] || return 0
if [[ "$INSTALLATION_MODE" = "initial-installation" ]]; then
warn "The new bridge did not pass all checks. Cleaning up the initial activation."
systemctl stop "$MM_BRIDGE_SERVICE" >> "$log_file" 2>&1 || true
if [[ "$UNIT_CREATED" = true ]]; then
systemctl disable "$MM_BRIDGE_SERVICE" >> "$log_file" 2>&1 || rollback_failed=true
fi
rm -f "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 || rollback_failed=true
if [[ "$NGINX_CHANGED" = true && -n "$NGINX_BACKUP" && -f "$NGINX_BACKUP" ]]; then
warn "Restoring the nginx configuration from before the initial bridge installation."
if cp -a "$NGINX_BACKUP" "$NGINX_CONFIG_FILE" >> "$log_file" 2>&1 \
&& nginx -t >> "$log_file" 2>&1 \
&& systemctl reload nginx.service >> "$log_file" 2>&1 ; then
info "Previous nginx configuration was restored and reloaded."
else
rollback_failed=true
error "Could not fully restore/reload the previous nginx configuration. Check nginx manually."
fi
fi
if [[ "$rollback_failed" = true ]]; then
error "Initial-install cleanup was not fully successful. Please check '${MM_BRIDGE_LINK}', '${MM_BRIDGE_SERVICE}' and nginx manually."
else
info "Initial activation was removed. The new binary, source directory and unit file were kept for analysis."
fi
return 0
fi
warn "The new bridge did not pass all checks. Trying to restore the previous installation."
if [[ -n "$ROLLBACK_TARGET" ]]; then
if [[ "$ROLLBACK_TARGET" = /* ]]; then
rollback_path="$ROLLBACK_TARGET"
else
rollback_path="${MM_BRIDGE_INSTALL_DIR}/${ROLLBACK_TARGET}"
fi
fi
if [[ -n "$ROLLBACK_TARGET" && -e "$rollback_path" ]]; then
ln -s "$ROLLBACK_TARGET" "${MM_BRIDGE_LINK}.rollback.$$" >> "$log_file" 2>&1 \
&& mv -Tf "${MM_BRIDGE_LINK}.rollback.$$" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 \
|| rollback_failed=true
elif [[ -n "$LEGACY_BRIDGE_BACKUP" && -f "$LEGACY_BRIDGE_BACKUP" ]]; then
rm -f "$MM_BRIDGE_LINK" >> "$log_file" 2>&1
cp -a "$LEGACY_BRIDGE_BACKUP" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 || rollback_failed=true
else
rollback_failed=true
fi
if [[ "$rollback_failed" = false ]]; then
systemctl restart "$MM_BRIDGE_SERVICE" >> "$log_file" 2>&1 || rollback_failed=true
fi
if [[ "$rollback_failed" = true ]]; then
error "Automatic rollback failed. Please check '${MM_BRIDGE_LINK}' and '${MM_BRIDGE_SERVICE}' manually."
else
info "Previous bridge installation was restored and restarted."
fi
}
bridge_test() {
local response=""
local location=""
local http_code=""
local headers_file="${LOCK_DIR}/bridge-redirect-headers"
echononl "Check bridge service status.."
if systemctl is-active --quiet "$MM_BRIDGE_SERVICE" ; then
echo_ok
else
echo_failed
return 1
fi
echononl "Check bridge listener '${MM_BRIDGE_LISTEN}'.."
_listener_ready=false
for _listener_try in {1..10}; do
if ss -lnt | awk -v listen="$MM_BRIDGE_LISTEN" '$4 == listen {found=1} END {exit !found}' ; then
_listener_ready=true
break
fi
sleep 1
done
if [[ "$_listener_ready" = true ]]; then
echo_ok
else
echo_failed
return 1
fi
echononl "Test mobile client configuration endpoint.."
if response="$(curl -fsS --max-time 10 \
-A 'Mattermost Mobile/2.0' \
"http://${MM_BRIDGE_LISTEN}/api/v4/config/client" 2>> "$log_file")" \
&& printf '%s' "$response" | python3 -c '
import json, sys
try:
value = json.load(sys.stdin).get("EnableSignUpWithOpenId")
except Exception:
raise SystemExit(1)
raise SystemExit(0 if value in (True, "true") else 1)
' ; then
echo_ok
else
echo_failed
return 1
fi
echononl "Test browser client configuration endpoint.."
if response="$(curl -fsS --max-time 10 \
-A 'Mozilla/5.0' \
"http://${MM_BRIDGE_LISTEN}/api/v4/config/client" 2>> "$log_file")" \
&& printf '%s' "$response" | python3 -c '
import json, sys
try:
value = json.load(sys.stdin).get("EnableSignUpWithOpenId")
except Exception:
raise SystemExit(1)
raise SystemExit(0 if value in (False, "false") else 1)
' ; then
echo_ok
else
echo_failed
return 1
fi
echononl "Test mobile OIDC redirect endpoint.."
: > "$headers_file"
http_code="$(curl -sS --max-time 10 \
-o /dev/null -D "$headers_file" -w '%{http_code}' \
"http://${MM_BRIDGE_LISTEN}/oauth/openid/mobile_login?redirect_to=mmauth%3A%2F%2Fcallback" \
2>> "$log_file")"
location="$(awk 'BEGIN{IGNORECASE=1} /^Location:/ {sub(/^[^:]*:[[:space:]]*/,""); sub(/\r$/,""); print; exit}' "$headers_file")"
if [[ "$http_code" = "302" ]] \
&& [[ "$location" == /plugins/mattermost-oidc/oauth2/connect* ]] \
&& [[ "$location" == *"mobile_redirect="* ]]; then
echo_ok
else
echo_failed
return 1
fi
return 0
}
nginx_e2e_test() {
local response=""
local location=""
local http_code=""
local headers_file="${LOCK_DIR}/nginx-redirect-headers"
[[ -n "$MM_SITE_URL" ]] || return 0
echononl "Test mobile client configuration endpoint through nginx.."
if response="$(curl -fsS --max-time 10 \
-A 'Mattermost Mobile/2.0' \
"${MM_SITE_URL%/}/api/v4/config/client" 2>> "$log_file")" \
&& printf '%s' "$response" | python3 -c '
import json, sys
try:
value = json.load(sys.stdin).get("EnableSignUpWithOpenId")
except Exception:
raise SystemExit(1)
raise SystemExit(0 if value in (True, "true") else 1)
' ; then
echo_ok
else
echo_failed
return 1
fi
echononl "Test browser client configuration endpoint through nginx.."
if response="$(curl -fsS --max-time 10 \
-A 'Mozilla/5.0' \
"${MM_SITE_URL%/}/api/v4/config/client" 2>> "$log_file")" \
&& printf '%s' "$response" | python3 -c '
import json, sys
try:
value = json.load(sys.stdin).get("EnableSignUpWithOpenId")
except Exception:
raise SystemExit(1)
raise SystemExit(0 if value in (False, "false") else 1)
' ; then
echo_ok
else
echo_failed
return 1
fi
echononl "Test mobile OIDC redirect endpoint through nginx.."
: > "$headers_file"
http_code="$(curl -sS --max-time 10 \
-o /dev/null -D "$headers_file" -w '%{http_code}' \
"${MM_SITE_URL%/}/oauth/openid/mobile_login?redirect_to=mmauth%3A%2F%2Fcallback" \
2>> "$log_file")"
location="$(awk 'BEGIN{IGNORECASE=1} /^Location:/ {sub(/^[^:]*:[[:space:]]*/,""); sub(/\r$/,""); print; exit}' "$headers_file")"
if [[ "$http_code" = "302" ]] \
&& [[ "$location" == /plugins/mattermost-oidc/oauth2/connect* ]] \
&& [[ "$location" == *"mobile_redirect="* ]]; then
echo_ok
else
echo_failed
return 1
fi
return 0
}
# ----------
# Jobhandling
# ----------
trap 'clean_up 1' SIGHUP SIGINT SIGTERM
if ! mkdir "$LOCK_DIR" 2>/dev/null ; then
echo "Cannot create lock directory '$LOCK_DIR'."
exit 1
fi
if [[ -t 1 ]] ; then
terminal=true
else
fatal "Script must run in a terminal."
fi
# ==========
# Begin Main Script
# ==========
if $terminal ; then
echo ""
echo -e "\033[1m----------\033[m"
echo -e "\033[32m\033[1mRunning script \033[m\033[1m$script_name\033[32m .. \033[m"
echo -e "\033[1m----------\033[m"
fi
blank_line
# ----------
# Some checks
# ----------
echononl "Check if script is running as root.."
if [[ "$(id -u)" -eq 0 ]]; then
echo_ok
else
echo_failed
fatal "This script must be run as root."
fi
for _cmd in git systemctl systemd-analyze curl ss awk grep sed sort file install readlink ln mv cp mkdir python3 ; do
echononl "Check for command '${_cmd}'.."
if command -v "$_cmd" > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
fatal "Required command '${_cmd}' was not found."
fi
done
echononl "Check Mattermost user '${MM_USER}'.."
if id "$MM_USER" > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
fatal "Mattermost user '${MM_USER}' does not exist."
fi
echononl "Check Mattermost group '${MM_GROUP}'.."
if getent group "$MM_GROUP" > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
fatal "Mattermost group '${MM_GROUP}' does not exist."
fi
echononl "Check Mattermost service '${MM_SERVICE}'.."
if systemctl cat "$MM_SERVICE" > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
fatal "Mattermost service '${MM_SERVICE}' was not found."
fi
# ----------
# Detect installation mode
# ----------
echononl "Detect existing Mattermost OIDC Mobile Bridge installation.."
if [[ -L "$MM_BRIDGE_LINK" ]]; then
MM_CURRENT_TARGET="$(readlink "$MM_BRIDGE_LINK")"
if [[ ! -e "$MM_BRIDGE_LINK" ]]; then
echo_failed
fatal "Broken symlink '${MM_BRIDGE_LINK}' -> '${MM_CURRENT_TARGET}'."
fi
if [[ "$MM_CURRENT_TARGET" =~ ^${MM_BRIDGE_NAME}-([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
MM_CURRENT_VERSION="${BASH_REMATCH[1]}"
else
echo_failed
fatal "Cannot determine installed bridge version from symlink target '${MM_CURRENT_TARGET}'."
fi
INSTALLATION_MODE="upgrade"
ROLLBACK_TARGET="$MM_CURRENT_TARGET"
echo_ok
elif [[ -e "$MM_BRIDGE_LINK" ]]; then
INSTALLATION_MODE="legacy-upgrade"
echo_ok
else
INSTALLATION_MODE="initial-installation"
echo_ok
fi
# ----------
# Determine latest stable release
# ----------
echononl "Determine latest stable Mattermost OIDC release.."
MM_LATEST_VERSION="$(git ls-remote --tags --refs "$MM_OIDC_REPOSITORY" 2> "$log_file" \
| awk '{print $2}' \
| sed -nE 's#refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$#\1#p' \
| sort -V \
| tail -1)"
if [[ -n "$MM_LATEST_VERSION" ]]; then
echo_ok
else
echo_failed
fatal "Could not determine latest stable release from '${MM_OIDC_REPOSITORY}'. $(cat "$log_file")"
fi
blank_line
if [[ "$INSTALLATION_MODE" = "initial-installation" ]]; then
echo " Installation mode....................: Initial installation"
elif [[ "$INSTALLATION_MODE" = "legacy-upgrade" ]]; then
echo " Installation mode....................: Upgrade of legacy/unversioned installation"
echo " Currently installed version.........: unknown"
else
echo " Installation mode....................: Upgrade"
echo " Currently installed version.........: ${MM_CURRENT_VERSION}"
fi
echo " Latest stable version...............: ${MM_LATEST_VERSION}"
blank_line
while true ; do
echononl "New Mattermost OIDC Mobile Bridge Version [${MM_LATEST_VERSION}]: "
read MM_NEW_VERSION
MM_NEW_VERSION="${MM_NEW_VERSION#v}"
[[ -z "$MM_NEW_VERSION" ]] && MM_NEW_VERSION="$MM_LATEST_VERSION"
echononl "Check release tag 'v${MM_NEW_VERSION}'.."
if git ls-remote --exit-code --tags "$MM_OIDC_REPOSITORY" \
"refs/tags/v${MM_NEW_VERSION}" "refs/tags/v${MM_NEW_VERSION}^{}" \
> /dev/null 2> "$log_file" ; then
echo_ok
break
else
echo_failed
warn "Tag 'v${MM_NEW_VERSION}' does not exist in the upstream repository."
fi
done
if [[ "$INSTALLATION_MODE" = "upgrade" && "$MM_NEW_VERSION" = "$MM_CURRENT_VERSION" ]]; then
blank_line
warn "Mattermost OIDC Mobile Bridge ${MM_CURRENT_VERSION} is already installed."
if ! ask_yes_no "Reinstall version ${MM_NEW_VERSION}" "no" ; then
info "Version ${MM_CURRENT_VERSION} is already installed. Nothing to do."
clean_up 0
fi
fi
MM_SOURCE_DIR="${MM_BRIDGE_SOURCE_BASE}/mattermost-oidc-plugin-${MM_NEW_VERSION}"
MM_BRIDGE_BIN="${MM_BRIDGE_INSTALL_DIR}/${MM_BRIDGE_NAME}-${MM_NEW_VERSION}"
if [[ -r "$MM_CONFIG_FILE" ]]; then
MM_SITE_URL="$(python3 - "$MM_CONFIG_FILE" 2>> "$log_file" <<'PY_EOF'
import json, sys
try:
with open(sys.argv[1], encoding="utf-8") as f:
data = json.load(f)
print(data.get("ServiceSettings", {}).get("SiteURL", ""))
except Exception:
pass
PY_EOF
)"
fi
# ----------
# Check systemd and nginx state
# ----------
UNIT_EXISTS=false
if systemctl cat "$MM_BRIDGE_SERVICE" > /dev/null 2>&1 ; then
UNIT_EXISTS=true
fi
NGINX_AVAILABLE=false
if command -v nginx > /dev/null 2>&1 ; then
NGINX_AVAILABLE=true
fi
if [[ "$NGINX_AVAILABLE" = true ]]; then
echononl "Try to detect active Mattermost nginx configuration.."
_nginx_candidates=()
_mm_site_host=""
if [[ -n "$MM_SITE_URL" ]]; then
_mm_site_host="$(python3 - "$MM_SITE_URL" <<'PY_EOF'
from urllib.parse import urlparse
import sys
try:
print(urlparse(sys.argv[1]).hostname or "")
except Exception:
pass
PY_EOF
)"
fi
if [[ -n "$_mm_site_host" ]]; then
mapfile -t _nginx_candidates < <(nginx -T 2>/dev/null \
| awk -v host="$_mm_site_host" '
/^# configuration file \/.*:$/ {f=$0; sub(/^# configuration file /,"",f); sub(/:$/,"",f)}
/^[[:space:]]*server_name[[:space:]]+/ {
line=$0; sub(/#.*/,"",line); gsub(/;/,"",line)
n=split(line,a,/[[:space:]]+/)
for (i=2; i<=n; i++) if (a[i] == host && f != "") print f
}
' | sort -u)
fi
if [[ ${#_nginx_candidates[@]} -eq 0 ]]; then
mapfile -t _nginx_candidates < <(nginx -T 2>/dev/null \
| awk -v upstream="$MM_BRIDGE_UPSTREAM" '
/^# configuration file \/.*:$/ {f=$0; sub(/^# configuration file /,"",f); sub(/:$/,"",f)}
index($0, "proxy_pass " upstream) {if (f != "") print f}
' | sort -u)
fi
if [[ ${#_nginx_candidates[@]} -eq 1 && -f "${_nginx_candidates[0]}" ]]; then
NGINX_CONFIG_FILE="${_nginx_candidates[0]}"
echo_ok
else
echo_skipped
fi
fi
if [[ -n "$NGINX_CONFIG_FILE" ]]; then
echo
echo " Detected Mattermost nginx vHost configuration:"
echo " ${NGINX_CONFIG_FILE}"
echononl "Configuration file [RETURN = use detected file]: "
read _nginx_input
[[ -n "$_nginx_input" ]] && NGINX_CONFIG_FILE="$_nginx_input"
elif [[ "$NGINX_AVAILABLE" = true ]]; then
echo
echo " Mattermost nginx vHost configuration could not be detected automatically."
echo " Enter the full path to the nginx vHost configuration file."
echo " Press RETURN without entering a path to skip nginx setup."
echononl "Configuration file: "
read NGINX_CONFIG_FILE
fi
if [[ -n "$NGINX_CONFIG_FILE" && ! -f "$NGINX_CONFIG_FILE" ]]; then
fatal "nginx configuration file '${NGINX_CONFIG_FILE}' does not exist."
fi
# Work on the real file, not on a sites-enabled symlink. This also makes
# timestamped backups independent from the live symlink.
if [[ -n "$NGINX_CONFIG_FILE" ]]; then
NGINX_CONFIG_FILE="$(readlink -f "$NGINX_CONFIG_FILE")"
[[ -f "$NGINX_CONFIG_FILE" ]] || fatal "Could not resolve nginx configuration file."
fi
# ----------
# Summary
# ----------
blank_line
echo -e "\033[37m\033[1mMattermost OIDC Mobile Bridge installation settings:\033[m"
blank_line
echo " Installation mode....................: ${INSTALLATION_MODE}"
[[ -n "$MM_CURRENT_VERSION" ]] && echo " Current bridge version...............: ${MM_CURRENT_VERSION}"
echo " New bridge version...................: ${MM_NEW_VERSION}"
echo " Latest stable version................: ${MM_LATEST_VERSION}"
echo " Repository...........................: ${MM_OIDC_REPOSITORY}"
echo " Source directory.....................: ${MM_SOURCE_DIR}"
echo " Versioned binary.....................: ${MM_BRIDGE_BIN}"
echo " Stable symlink.......................: ${MM_BRIDGE_LINK}"
echo " Bridge service.......................: ${MM_BRIDGE_SERVICE}"
echo " Bridge listen address................: ${MM_BRIDGE_LISTEN}"
echo " Mattermost upstream..................: ${MM_BRIDGE_UPSTREAM}"
if [[ -n "$NGINX_CONFIG_FILE" ]]; then
echo " nginx configuration..................: ${NGINX_CONFIG_FILE}"
else
echo " nginx configuration..................: not selected"
fi
blank_line
if ! ask_yes_no "einverstanden" "no" ; then
fatal "Stopped by user"
fi
# ----------
# Prepare source tree
# ----------
blank_line
echo -e "\033[37m\033[1mBuild Mattermost OIDC Mobile Bridge..\033[m"
blank_line
echononl "Create source base directory.."
if mkdir -p "$MM_BRIDGE_SOURCE_BASE" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
if [[ -e "$MM_SOURCE_DIR" ]]; then
echononl "Backup existing source directory.."
if mv "$MM_SOURCE_DIR" "${MM_SOURCE_DIR}.${backup_date}" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
else
echononl "Backup existing source directory.."
echo_skipped
fi
echononl "Clone release 'v${MM_NEW_VERSION}'.."
if git clone --depth 1 --branch "v${MM_NEW_VERSION}" "$MM_OIDC_REPOSITORY" "$MM_SOURCE_DIR" \
> "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
echononl "Verify checked out release tag.."
if [[ "$(git -C "$MM_SOURCE_DIR" describe --tags --exact-match 2> "$log_file")" = "v${MM_NEW_VERSION}" ]]; then
echo_ok
else
echo_failed
fatal "Source tree does not match expected tag 'v${MM_NEW_VERSION}'."
fi
if [[ ! -f "${MM_SOURCE_DIR}/mobile-bridge/go.mod" || ! -f "${MM_SOURCE_DIR}/mobile-bridge/main.go" ]]; then
fatal "Expected mobile-bridge source files were not found in '${MM_SOURCE_DIR}/mobile-bridge'."
fi
GO_REQUIRED_VERSION="$(awk '$1 == "go" {print $2; exit}' "${MM_SOURCE_DIR}/mobile-bridge/go.mod")"
[[ -n "$GO_REQUIRED_VERSION" ]] || fatal "Could not determine required Go version from mobile-bridge/go.mod."
echononl "Check if Go is installed.."
if command -v go > /dev/null 2>&1 ; then
echo_ok
else
echo_failed
fatal "Go is not installed. Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} requires Go ${GO_REQUIRED_VERSION} or newer."
fi
GO_INSTALLED_VERSION="$(go version 2>/dev/null | awk '{sub(/^go/,"",$3); print $3}')"
[[ -n "$GO_INSTALLED_VERSION" ]] || fatal "Could not determine installed Go version."
echononl "Check Go version (installed: ${GO_INSTALLED_VERSION}, required: ${GO_REQUIRED_VERSION}).."
if [[ "$(printf '%s\n%s\n' "$GO_REQUIRED_VERSION" "$GO_INSTALLED_VERSION" | sort -V | head -1)" = "$GO_REQUIRED_VERSION" ]]; then
echo_ok
else
echo_failed
fatal "Installed Go version '${GO_INSTALLED_VERSION}' is too old. Version '${GO_REQUIRED_VERSION}' or newer is required."
fi
echononl "Build Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION}.."
if ( cd "${MM_SOURCE_DIR}/mobile-bridge" && CGO_ENABLED=0 go build -trimpath -o "${LOCK_DIR}/${MM_BRIDGE_NAME}" . ) \
> "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
echononl "Verify built bridge binary.."
if [[ -x "${LOCK_DIR}/${MM_BRIDGE_NAME}" ]] \
&& file "${LOCK_DIR}/${MM_BRIDGE_NAME}" | grep -q 'ELF' ; then
echo_ok
else
echo_failed
fatal "Built bridge binary is missing, not executable or not an ELF binary."
fi
# ----------
# Install versioned binary
# ----------
blank_line
echo -e "\033[37m\033[1mInstall Mattermost OIDC Mobile Bridge..\033[m"
blank_line
if [[ -e "$MM_BRIDGE_BIN" ]]; then
echononl "Backup existing bridge binary.."
if mv "$MM_BRIDGE_BIN" "${MM_BRIDGE_BIN}.${backup_date}" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
else
echononl "Backup existing bridge binary.."
echo_skipped
fi
echononl "Install versioned bridge binary.."
if install -o root -g root -m 0755 "${LOCK_DIR}/${MM_BRIDGE_NAME}" "$MM_BRIDGE_BIN" \
> "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
# Preserve an old unversioned installation before replacing it with a symlink.
if [[ "$INSTALLATION_MODE" = "legacy-upgrade" ]]; then
LEGACY_BRIDGE_BACKUP="${MM_BRIDGE_LINK}.${backup_date}"
echononl "Backup legacy bridge binary.."
if mv "$MM_BRIDGE_LINK" "$LEGACY_BRIDGE_BACKUP" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
fi
# ----------
# Install/check systemd unit
# ----------
UNIT_FILE="/etc/systemd/system/${MM_BRIDGE_SERVICE}"
if [[ "$UNIT_EXISTS" = false ]]; then
echononl "Create systemd service '${MM_BRIDGE_SERVICE}'.."
cat > "$UNIT_FILE" <<SYSTEMD_EOF
[Unit]
Description=Mattermost OIDC Mobile Bridge
After=network.target mattermost.service
Requires=mattermost.service
[Service]
Type=simple
User=${MM_USER}
Group=${MM_GROUP}
Environment=LISTEN=${MM_BRIDGE_LISTEN}
Environment=UPSTREAM=${MM_BRIDGE_UPSTREAM}
ExecStart=${MM_BRIDGE_LINK}
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
SYSTEMD_EOF
if [[ $? -eq 0 ]]; then
UNIT_CREATED=true
echo_ok
else
echo_failed
fatal "Failed to create '${UNIT_FILE}'."
fi
echononl "Verify systemd service '${MM_BRIDGE_SERVICE}'.."
if systemd-analyze verify "$UNIT_FILE" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
echononl "Reload systemd configuration.."
if systemctl daemon-reload > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
echononl "Enable '${MM_BRIDGE_SERVICE}'.."
if systemctl enable "$MM_BRIDGE_SERVICE" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
else
echononl "Keep existing systemd service '${MM_BRIDGE_SERVICE}'.."
echo_ok
_unit_text="$(systemctl cat "$MM_BRIDGE_SERVICE" 2>/dev/null)"
UNIT_EXECSTART_OK=true
if [[ "$_unit_text" != *"ExecStart=${MM_BRIDGE_LINK}"* ]]; then
UNIT_EXECSTART_OK=false
warn "Existing systemd unit does not contain expected ExecStart=${MM_BRIDGE_LINK}. It will not be changed automatically."
fi
[[ "$_unit_text" == *"LISTEN=${MM_BRIDGE_LISTEN}"* ]] \
|| warn "Existing systemd unit does not contain expected LISTEN=${MM_BRIDGE_LISTEN}. It will not be changed automatically."
[[ "$_unit_text" == *"UPSTREAM=${MM_BRIDGE_UPSTREAM}"* ]] \
|| warn "Existing systemd unit does not contain expected UPSTREAM=${MM_BRIDGE_UPSTREAM}. It will not be changed automatically."
[[ "$_unit_text" == *"User=${MM_USER}"* ]] \
|| warn "Existing systemd unit does not contain expected User=${MM_USER}. It will not be changed automatically."
[[ "$_unit_text" == *"Group=${MM_GROUP}"* ]] \
|| warn "Existing systemd unit does not contain expected Group=${MM_GROUP}. It will not be changed automatically."
if [[ "$UNIT_EXECSTART_OK" = false ]]; then
blank_line
if ! ask_yes_no "Continue although ExecStart differs from the expected stable bridge symlink" "no" ; then
fatal "Stopped by user because existing systemd ExecStart differs."
fi
fi
fi
# ----------
# Configure nginx if requested
# ----------
if [[ -n "$NGINX_CONFIG_FILE" ]]; then
blank_line
echo -e "\033[37m\033[1mCheck nginx configuration..\033[m"
blank_line
API_LOCATION_STATE="missing"
MOBILE_LOCATION_STATE="missing"
_bridge_proxy_pass="proxy_passhttp://${MM_BRIDGE_LISTEN};"
if grep -Eq '^[[:space:]]*location[[:space:]]*=[[:space:]]*/api/v4/config/client[[:space:]]*\{' "$NGINX_CONFIG_FILE" ; then
if awk -v expected="$_bridge_proxy_pass" '
/^[[:space:]]*location[[:space:]]*=[[:space:]]*\/api\/v4\/config\/client[[:space:]]*\{/ {inloc=1; depth=1; next}
inloc {depth += gsub(/\{/ ,"{"); depth -= gsub(/\}/,"}"); line=$0; gsub(/[[:space:]]/,"",line); if (line == expected) ok=1; if (depth<=0) exit}
END {exit !ok}
' "$NGINX_CONFIG_FILE" ; then
API_LOCATION_STATE="ok"
else
API_LOCATION_STATE="different"
fi
fi
if grep -Eq '^[[:space:]]*location[[:space:]]*=[[:space:]]*/oauth/openid/mobile_login[[:space:]]*\{' "$NGINX_CONFIG_FILE" ; then
if awk -v expected="$_bridge_proxy_pass" '
/^[[:space:]]*location[[:space:]]*=[[:space:]]*\/oauth\/openid\/mobile_login[[:space:]]*\{/ {inloc=1; depth=1; next}
inloc {depth += gsub(/\{/ ,"{"); depth -= gsub(/\}/,"}"); line=$0; gsub(/[[:space:]]/,"",line); if (line == expected) ok=1; if (depth<=0) exit}
END {exit !ok}
' "$NGINX_CONFIG_FILE" ; then
MOBILE_LOCATION_STATE="ok"
else
MOBILE_LOCATION_STATE="different"
fi
fi
echo " /api/v4/config/client.................: ${API_LOCATION_STATE}"
echo " /oauth/openid/mobile_login............: ${MOBILE_LOCATION_STATE}"
blank_line
if [[ "$API_LOCATION_STATE" = "different" || "$MOBILE_LOCATION_STATE" = "different" ]]; then
warn "At least one required location already exists with a different configuration. Existing location blocks will not be modified automatically."
fi
MISSING_LOCATIONS=false
[[ "$API_LOCATION_STATE" = "missing" ]] && MISSING_LOCATIONS=true
[[ "$MOBILE_LOCATION_STATE" = "missing" ]] && MISSING_LOCATIONS=true
if [[ "$MISSING_LOCATIONS" = true ]]; then
if ask_yes_no "Add missing Mattermost OIDC Mobile Bridge locations to nginx configuration" "yes" ; then
NGINX_BACKUP="${NGINX_CONFIG_FILE}.${backup_date}"
echononl "Backup nginx configuration.."
if cp -a "$NGINX_CONFIG_FILE" "$NGINX_BACKUP" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
fatal "$(cat "$log_file")"
fi
NGINX_INSERT_FILE="${LOCK_DIR}/nginx-locations.conf"
: > "$NGINX_INSERT_FILE"
if [[ "$API_LOCATION_STATE" = "missing" ]]; then
cat >> "$NGINX_INSERT_FILE" <<NGINX_API_EOF
location = /api/v4/config/client {
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${MM_BRIDGE_LISTEN};
}
NGINX_API_EOF
fi
if [[ "$MOBILE_LOCATION_STATE" = "missing" ]]; then
cat >> "$NGINX_INSERT_FILE" <<NGINX_MOBILE_EOF
location = /oauth/openid/mobile_login {
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${MM_BRIDGE_LISTEN};
}
NGINX_MOBILE_EOF
fi
echononl "Insert missing bridge locations before Mattermost 'location /'.."
python3 - "$NGINX_CONFIG_FILE" "$NGINX_INSERT_FILE" "$MM_BRIDGE_UPSTREAM" <<'PY_EOF' > "$log_file" 2>&1
import re
import sys
from pathlib import Path
config = Path(sys.argv[1])
insert = Path(sys.argv[2]).read_text()
upstream = sys.argv[3].rstrip("/")
text = config.read_text()
def matching_brace(data, open_pos):
depth = 0
quote = None
escaped = False
comment = False
for pos in range(open_pos, len(data)):
ch = data[pos]
if comment:
if ch == "\n":
comment = False
continue
if quote:
if escaped:
escaped = False
elif ch == "\\":
escaped = True
elif ch == quote:
quote = None
continue
if ch == "#":
comment = True
elif ch in ("\"", "'"):
quote = ch
elif ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return pos
return None
servers = []
for match in re.finditer(r'(?m)^[ \t]*server[ \t]*\{', text):
open_pos = text.find("{", match.start(), match.end())
close_pos = matching_brace(text, open_pos)
if close_pos is None:
raise SystemExit("Could not parse nginx server block")
body = text[open_pos + 1:close_pos]
proxy_re = re.compile(r'proxy_pass\s+' + re.escape(upstream) + r'/?\s*;')
if proxy_re.search(body):
servers.append((open_pos + 1, close_pos, body))
if len(servers) != 1:
raise SystemExit(f"Expected exactly one server block proxying to {upstream}, found {len(servers)}")
body_start, body_end, body = servers[0]
locations = list(re.finditer(r'(?m)^[ \t]*location[ \t]+/[ \t]*\{', body))
if len(locations) != 1:
raise SystemExit(f"Expected exactly one general 'location /' in Mattermost server block, found {len(locations)}")
insert_pos = body_start + locations[0].start()
text = text[:insert_pos] + insert + text[insert_pos:]
config.write_text(text)
PY_EOF
if [[ $? -eq 0 ]]; then
echo_ok
NGINX_CHANGED=true
else
echo_failed
cp -a "$NGINX_BACKUP" "$NGINX_CONFIG_FILE"
fatal "Could not insert nginx location blocks. Original configuration was restored. $(cat "$log_file")"
fi
echononl "Test nginx configuration.."
if nginx -t > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
cp -a "$NGINX_CONFIG_FILE" "${NGINX_CONFIG_FILE}.failed-${backup_date}" 2>/dev/null
cp -a "$NGINX_BACKUP" "$NGINX_CONFIG_FILE"
nginx -t >> "$log_file" 2>&1
fatal "nginx configuration test failed. Original configuration was restored. See '${NGINX_CONFIG_FILE}.failed-${backup_date}' and '${log_file}' while this script is running."
fi
else
warn "Missing nginx location blocks were not added."
fi
fi
fi
# ----------
# Switch stable symlink and start bridge
# ----------
blank_line
echo -e "\033[37m\033[1mActivate Mattermost OIDC Mobile Bridge..\033[m"
blank_line
echononl "Switch stable bridge symlink to '${MM_BRIDGE_NAME}-${MM_NEW_VERSION}'.."
_tmp_link="${MM_BRIDGE_LINK}.new.$$"
rm -f "$_tmp_link"
if ln -s "${MM_BRIDGE_NAME}-${MM_NEW_VERSION}" "$_tmp_link" > "$log_file" 2>&1 \
&& mv -Tf "$_tmp_link" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then
SYMLINK_CHANGED=true
echo_ok
else
rm -f "$_tmp_link"
echo_failed
fatal "$(cat "$log_file")"
fi
if [[ "$NGINX_CHANGED" = true ]]; then
echononl "Reload nginx service.."
if systemctl reload nginx.service > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
error "nginx reload failed. Restoring the previous nginx configuration."
cp -a "$NGINX_CONFIG_FILE" "${NGINX_CONFIG_FILE}.failed-${backup_date}" 2>/dev/null || true
if cp -a "$NGINX_BACKUP" "$NGINX_CONFIG_FILE" >> "$log_file" 2>&1 && nginx -t >> "$log_file" 2>&1 && systemctl reload nginx.service >> "$log_file" 2>&1 ; then
info "Previous nginx configuration was restored and reloaded."
else
error "Could not fully restore/reload the previous nginx configuration. Check nginx manually."
fi
rollback_bridge
fatal "nginx reload failed after adding the Mattermost OIDC Mobile Bridge locations."
fi
fi
echononl "Restart Mattermost OIDC Mobile Bridge service.."
if systemctl restart "$MM_BRIDGE_SERVICE" > "$log_file" 2>&1 ; then
echo_ok
else
echo_failed
rollback_bridge
fatal "Failed to restart '${MM_BRIDGE_SERVICE}'."
fi
if ! bridge_test ; then
rollback_bridge
fatal "Mattermost OIDC Mobile Bridge functional test failed."
fi
if [[ -n "$NGINX_CONFIG_FILE" ]]; then
if [[ -n "$MM_SITE_URL" ]]; then
if ! nginx_e2e_test ; then
rollback_bridge
fatal "Mattermost OIDC Mobile Bridge end-to-end test through nginx failed."
fi
else
warn "Mattermost SiteURL could not be read from '${MM_CONFIG_FILE}'. End-to-end nginx tests were skipped."
fi
fi
# ----------
# Final information
# ----------
blank_line
echo -e "\033[37m\033[1mMattermost OIDC Mobile Bridge successfully installed.\033[m"
blank_line
echo " Installed version.....................: ${MM_NEW_VERSION}"
echo " Binary................................: ${MM_BRIDGE_BIN}"
echo " Symlink...............................: ${MM_BRIDGE_LINK} -> $(readlink "$MM_BRIDGE_LINK")"
echo " Source directory......................: ${MM_SOURCE_DIR}"
echo " Service...............................: ${MM_BRIDGE_SERVICE}"
if [[ -n "$NGINX_CONFIG_FILE" ]]; then
echo " nginx configuration...................: ${NGINX_CONFIG_FILE}"
fi
blank_line
clean_up 0