diff --git a/install-update-oidc-mobile-bridge.sh b/install-update-oidc-mobile-bridge.sh new file mode 100755 index 0000000..d8994b7 --- /dev/null +++ b/install-update-oidc-mobile-bridge.sh @@ -0,0 +1,1166 @@ +#!/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