#!/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" < "$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_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("/") 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