#!/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="" REINSTALL_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="" local rollback_tmp_link="${MM_BRIDGE_LINK}.rollback.$$" [[ "$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." return 1 else info "Initial activation was removed. The new binary, source directory and unit file were kept for analysis." return 0 fi 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 # A same-version reinstall replaces the file referenced by ROLLBACK_TARGET. # Restore that saved binary first; changing the symlink alone would otherwise # still point to the newly installed (and possibly broken) binary. if [[ -n "$REINSTALL_BRIDGE_BACKUP" ]]; then echononl "Restore previous same-version bridge binary.." _restore_tmp="${MM_BRIDGE_BIN}.restore.$$" rm -f "$_restore_tmp" if [[ -f "$REINSTALL_BRIDGE_BACKUP" ]] \ && cp -a "$REINSTALL_BRIDGE_BACKUP" "$_restore_tmp" >> "$log_file" 2>&1 \ && mv -Tf "$_restore_tmp" "$MM_BRIDGE_BIN" >> "$log_file" 2>&1 ; then echo_ok else rm -f "$_restore_tmp" echo_failed error "Could not restore '${REINSTALL_BRIDGE_BACKUP}' to '${MM_BRIDGE_BIN}'." rollback_failed=true fi fi if [[ "$rollback_failed" = false ]]; then if [[ -n "$ROLLBACK_TARGET" && -e "$rollback_path" ]]; then echononl "Restore previous stable bridge symlink.." rm -f "$rollback_tmp_link" if ln -s "$ROLLBACK_TARGET" "$rollback_tmp_link" >> "$log_file" 2>&1 \ && mv -Tf "$rollback_tmp_link" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then echo_ok else rm -f "$rollback_tmp_link" echo_failed error "Could not restore stable bridge symlink '${MM_BRIDGE_LINK}' -> '${ROLLBACK_TARGET}'." rollback_failed=true fi elif [[ -n "$LEGACY_BRIDGE_BACKUP" && -f "$LEGACY_BRIDGE_BACKUP" ]]; then echononl "Restore previous legacy bridge binary.." if rm -f "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 \ && cp -a "$LEGACY_BRIDGE_BACKUP" "$MM_BRIDGE_LINK" >> "$log_file" 2>&1 ; then echo_ok else echo_failed error "Could not restore legacy bridge binary from '${LEGACY_BRIDGE_BACKUP}'." rollback_failed=true fi else error "No usable rollback target was found." rollback_failed=true fi fi if [[ "$rollback_failed" = false ]]; then echononl "Restart previous Mattermost OIDC Mobile Bridge service.." if systemctl restart "$MM_BRIDGE_SERVICE" >> "$log_file" 2>&1 ; then echo_ok else echo_failed error "Could not restart '${MM_BRIDGE_SERVICE}' after rollback." rollback_failed=true fi fi if [[ "$NGINX_CHANGED" = true && -n "$NGINX_BACKUP" && -f "$NGINX_BACKUP" ]]; then echononl "Restore previous nginx configuration.." 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 echo_ok else echo_failed error "Could not fully restore/reload the previous nginx configuration. Check nginx manually." rollback_failed=true fi fi if [[ "$rollback_failed" = true ]]; then error "Automatic rollback failed. Please check the bridge installation, '${MM_BRIDGE_SERVICE}' and nginx configuration manually." return 1 else info "Previous bridge installation was restored and restarted." return 0 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.." _bridge_binary_backup="${MM_BRIDGE_BIN}.${backup_date}" if mv "$MM_BRIDGE_BIN" "$_bridge_binary_backup" > "$log_file" 2>&1 ; then # During a same-version reinstall this backup is the actual rollback # payload. ROLLBACK_TARGET alone is insufficient because it names the # same versioned path that is about to be replaced. if [[ "$INSTALLATION_MODE" = "upgrade" && "$MM_NEW_VERSION" = "$MM_CURRENT_VERSION" ]]; then REINSTALL_BRIDGE_BACKUP="$_bridge_binary_backup" fi 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 # On an initial installation the systemd unit below already references the # stable bridge symlink. Create that symlink before systemd-analyze verifies # the unit. During upgrades, keep the existing symlink untouched until the # final activation step. if [[ "$INSTALLATION_MODE" = "initial-installation" ]]; then echononl "Create initial stable bridge symlink.." _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 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" < "$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 # Ensure that the bridge service is enabled also when an existing unit is reused. echononl "Ensure '${MM_BRIDGE_SERVICE}' is enabled.." if systemctl is-enabled --quiet "$MM_BRIDGE_SERVICE" 2>/dev/null ; then echo_ok else if systemctl enable "$MM_BRIDGE_SERVICE" > "$log_file" 2>&1 ; then echo_ok else echo_failed fatal "$(cat "$log_file")" 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_INSERT_FILE" < "$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("/") site_host = sys.argv[4].strip() 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 all_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] all_servers.append((open_pos + 1, close_pos, body)) def general_locations(body): return list(re.finditer(r'(?m)^[ \t]*location[ \t]+/[ \t]*\{', body)) # Prefer the Mattermost vHost identified by SiteURL/server_name. This works # with both direct proxy_pass targets and named nginx upstreams. The HTTP # redirect vHost is excluded because it has no general "location /" block. servers = [] if site_host: for server in all_servers: body = server[2] names = [] for match in re.finditer(r'(?m)^[ \t]*server_name[ \t]+([^;]+);', body): names.extend(match.group(1).split()) if site_host in names and len(general_locations(body)) == 1: servers.append(server) # Fallback for installations where SiteURL is unavailable: retain the old # direct-upstream detection. if not servers: proxy_re = re.compile(r'proxy_pass\s+' + re.escape(upstream) + r'/?\s*;') servers = [ server for server in all_servers if proxy_re.search(server[2]) and len(general_locations(server[2])) == 1 ] if len(servers) != 1: if site_host: raise SystemExit( f"Expected exactly one Mattermost server block for server_name " f"{site_host} with a general 'location /', found {len(servers)}" ) raise SystemExit( f"Expected exactly one server block proxying to {upstream} " f"with a general 'location /', found {len(servers)}" ) body_start, body_end, body = servers[0] locations = general_locations(body) 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 if [[ "$INSTALLATION_MODE" = "initial-installation" ]] \ && [[ -L "$MM_BRIDGE_LINK" ]] \ && [[ "$(readlink "$MM_BRIDGE_LINK")" = "${MM_BRIDGE_NAME}-${MM_NEW_VERSION}" ]]; then echononl "Keep initial stable bridge symlink to '${MM_BRIDGE_NAME}-${MM_NEW_VERSION}'.." echo_ok else 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 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." nginx_rollback_failed=false 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." NGINX_CHANGED=false else error "Could not fully restore/reload the previous nginx configuration. Check nginx manually." nginx_rollback_failed=true fi if rollback_bridge ; then bridge_rollback_failed=false else bridge_rollback_failed=true fi if [[ "$nginx_rollback_failed" = false && "$bridge_rollback_failed" = false ]]; then fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because nginx could not be reloaded.\n The previous nginx configuration and bridge installation were successfully restored." else fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because nginx could not be reloaded.\n The previous system state could not be fully restored.\n\n Manual intervention is required." fi 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 if rollback_bridge ; then fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because '${MM_BRIDGE_SERVICE}' could not be restarted.\n The previous bridge installation was successfully restored." else fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed because '${MM_BRIDGE_SERVICE}' could not be restarted.\n The previous installation could not be restored.\n\n Manual intervention is required." fi fi if ! bridge_test ; then if rollback_bridge ; then fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed.\n The previous bridge installation was successfully restored." else fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed\n and the previous installation could not be restored.\n\n Manual intervention is required." fi fi if [[ -n "$NGINX_CONFIG_FILE" ]]; then if [[ -n "$MM_SITE_URL" ]]; then if ! nginx_e2e_test ; then if rollback_bridge ; then fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed during the nginx end-to-end test.\n The previous bridge installation was successfully restored." else fatal "Installation of Mattermost OIDC Mobile Bridge ${MM_NEW_VERSION} failed during the nginx end-to-end test\n and the previous installation could not be restored.\n\n Manual intervention is required." fi fi else warn "Mattermost SiteURL could not be read from '${MM_CONFIG_FILE}'. End-to-end nginx tests were skipped." fi fi # ---------- # Remove redundant same-version backup after successful tests # ---------- if [[ -n "$REINSTALL_BRIDGE_BACKUP" && -f "$REINSTALL_BRIDGE_BACKUP" ]]; then if cmp -s "$REINSTALL_BRIDGE_BACKUP" "$MM_BRIDGE_BIN"; then echononl "Remove identical same-version bridge backup.." if rm -f "$REINSTALL_BRIDGE_BACKUP" > "$log_file" 2>&1 ; then echo_ok REINSTALL_BRIDGE_BACKUP="" else echo_failed warn "Could not remove redundant bridge backup '${REINSTALL_BRIDGE_BACKUP}'." fi else echononl "Keep different same-version bridge backup.." echo_ok 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