255 lines
9.5 KiB
Bash
Executable file
255 lines
9.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# check_mattermost.sh — outside-in checks for Taler Mattermost (chat)
|
|
#
|
|
# Default host: mattermost.taler.net
|
|
# Override: MATTERMOST_PUBLIC=https://mattermost.example.org
|
|
# MATTERMOST_HOST=mattermost.example.org
|
|
#
|
|
# Client compatibility (feature) — server min for current *official* clients:
|
|
# Android / iOS store apps → default ≥ 10.11.0 (Play/App Store + mobile docs 2026)
|
|
# Desktop (Win/Mac/Linux) → default ≥ 10.11.0 (current Desktop / ESR pairing)
|
|
# Vendor support floor → default ≥ 10.11.0 (ESR; older = unsupported)
|
|
# Web SPA → served by this server (always loads) but WARN if
|
|
# server is below support floor (security/EOL).
|
|
#
|
|
# Env:
|
|
# MATTERMOST_CLIENT_CHECK=1|0 master switch (default 1)
|
|
# MATTERMOST_MOBILE_CHECK=1|0 alias / legacy (if set 0 and CLIENT unset → off)
|
|
# MATTERMOST_MOBILE_MIN_SERVER default 10.11.0
|
|
# MATTERMOST_DESKTOP_MIN_SERVER default 10.11.0
|
|
# MATTERMOST_SUPPORT_MIN_SERVER default 10.11.0 (vendor-supported floor)
|
|
#
|
|
# Outside-only (no SSH). Phase name: mattermost
|
|
#
|
|
set -euo pipefail
|
|
ROOT=$(cd "$(dirname "$0")" && pwd)
|
|
# shellcheck source=lib.sh
|
|
source "$ROOT/lib.sh"
|
|
|
|
set_area mattermost
|
|
section "mattermost · public chat (outside-in)"
|
|
|
|
# Defaults: current official clients + vendor ESR floor (2026-07 docs).
|
|
: "${MATTERMOST_MOBILE_MIN_SERVER:=10.11.0}"
|
|
: "${MATTERMOST_DESKTOP_MIN_SERVER:=10.11.0}"
|
|
: "${MATTERMOST_SUPPORT_MIN_SERVER:=10.11.0}"
|
|
# Master switch: CLIENT_CHECK, else legacy MOBILE_CHECK, else on
|
|
if [ -n "${MATTERMOST_CLIENT_CHECK:-}" ]; then
|
|
:
|
|
elif [ -n "${MATTERMOST_MOBILE_CHECK:-}" ]; then
|
|
MATTERMOST_CLIENT_CHECK="${MATTERMOST_MOBILE_CHECK}"
|
|
else
|
|
MATTERMOST_CLIENT_CHECK=1
|
|
fi
|
|
: "${MATTERMOST_MOBILE_CHECK:=${MATTERMOST_CLIENT_CHECK}}"
|
|
|
|
if [ -n "${MATTERMOST_PUBLIC:-}" ]; then
|
|
BASE="${MATTERMOST_PUBLIC%/}"
|
|
elif [ -n "${MATTERMOST_HOST:-}" ]; then
|
|
BASE="https://${MATTERMOST_HOST}"
|
|
else
|
|
BASE="https://mattermost.taler.net"
|
|
fi
|
|
HOST=${BASE#https://}
|
|
HOST=${HOST#http://}
|
|
HOST=${HOST%%/*}
|
|
|
|
info "target" "$BASE"
|
|
info "client policy" "mobile≥${MATTERMOST_MOBILE_MIN_SERVER} · desktop≥${MATTERMOST_DESKTOP_MIN_SERVER} · support≥${MATTERMOST_SUPPORT_MIN_SERVER} (CLIENT_CHECK=${MATTERMOST_CLIENT_CHECK})"
|
|
|
|
tmp=$(mktemp -d)
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
# GET with follow redirects (Mattermost SPA often 302 → /)
|
|
mm_get() {
|
|
local url="$1" out="$2"
|
|
curl -skS -L --max-redirs 5 -m "${TIMEOUT:-12}" -o "$out" -w '%{http_code}' "$url" 2>/dev/null || echo 000
|
|
}
|
|
|
|
# --- DNS / HTTPS landing ---
|
|
set_group landing
|
|
code=$(mm_get "$BASE/" "$tmp/root.html")
|
|
if [ "$code" = "200" ] && head -c 512 "$tmp/root.html" | grep -qiE '<!doctype html|<html'; then
|
|
ok "landing" "$BASE/ → HTTP $code HTML"
|
|
else
|
|
fail "landing" "$BASE/ → HTTP $code (want 200 HTML Mattermost SPA)"
|
|
fi
|
|
|
|
# --- Official Mattermost health API + server version (X-Version-Id) ---
|
|
set_group api
|
|
# Capture body + headers (version is in X-Version-Id, not always in JSON)
|
|
hdr_file="$tmp/ping.hdr"
|
|
code=$(curl -skS -L --max-redirs 5 -m "${TIMEOUT:-12}" \
|
|
-D "$hdr_file" -o "$tmp/ping.json" -w '%{http_code}' \
|
|
"$BASE/api/v4/system/ping" 2>/dev/null || echo 000)
|
|
|
|
MM_SERVER_VER=""
|
|
if [ -f "$hdr_file" ]; then
|
|
# X-Version-Id: 9.5.3.8427860509.c8cabb… or 10.11.0.…
|
|
MM_SERVER_VER=$(grep -i '^x-version-id:' "$hdr_file" | head -1 \
|
|
| sed 's/^[Xx]-[Vv]ersion-[Ii]d:[[:space:]]*//' | tr -d '\r' \
|
|
| sed 's/^\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/')
|
|
fi
|
|
|
|
if [ "$code" != "200" ]; then
|
|
fail "system/ping" "$BASE/api/v4/system/ping → HTTP $code (want 200 JSON)"
|
|
else
|
|
if python3 - "$tmp/ping.json" <<'PY' 2>/dev/null
|
|
import json, sys
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
sys.exit(0 if isinstance(d, dict) else 2)
|
|
PY
|
|
then
|
|
ok "system/ping" "$BASE/api/v4/system/ping → HTTP 200 JSON${MM_SERVER_VER:+ · server ${MM_SERVER_VER}}"
|
|
else
|
|
fail "system/ping" "$BASE/api/v4/system/ping → HTTP 200 but not valid JSON object"
|
|
fi
|
|
fi
|
|
|
|
# --- Server version vs official client families (mobile / desktop / support / web) ---
|
|
set_group clients
|
|
mm_ver_ge() {
|
|
# $1=server $2=need → 0 if server >= need
|
|
python3 - "$1" "$2" <<'PY'
|
|
import sys
|
|
|
|
def parse(v: str):
|
|
parts = []
|
|
for p in v.strip().split(".")[:3]:
|
|
try:
|
|
parts.append(int(p))
|
|
except ValueError:
|
|
parts.append(0)
|
|
while len(parts) < 3:
|
|
parts.append(0)
|
|
return tuple(parts)
|
|
|
|
sys.exit(0 if parse(sys.argv[1]) >= parse(sys.argv[2]) else 1)
|
|
PY
|
|
}
|
|
|
|
# One policy line: id|min_env_default|severity(error|warn)|human label
|
|
# severity: error → fail; warn → warn
|
|
_mm_client_policies() {
|
|
printf '%s\n' \
|
|
"mobile|${MATTERMOST_MOBILE_MIN_SERVER}|error|Android/iOS store apps (Mattermost Mobile)" \
|
|
"desktop|${MATTERMOST_DESKTOP_MIN_SERVER}|error|Desktop app (Windows/macOS/Linux official)" \
|
|
"support|${MATTERMOST_SUPPORT_MIN_SERVER}|error|vendor-supported server floor (ESR/current)" \
|
|
"web|${MATTERMOST_SUPPORT_MIN_SERVER}|warn|browser webapp (loads with server, but EOL server is insecure/unsupported)"
|
|
}
|
|
|
|
if [ "${MATTERMOST_CLIENT_CHECK}" = "0" ]; then
|
|
info "client compat" "skipped (MATTERMOST_CLIENT_CHECK=0 / MATTERMOST_MOBILE_CHECK=0)"
|
|
elif [ -z "$MM_SERVER_VER" ]; then
|
|
warn "server version" "could not parse X-Version-Id from /api/v4/system/ping — cannot judge client compatibility"
|
|
else
|
|
info "server version" "X-Version-Id → ${MM_SERVER_VER}"
|
|
while IFS='|' read -r cid cmin csever clabel; do
|
|
[ -n "$cid" ] || continue
|
|
if mm_ver_ge "$MM_SERVER_VER" "$cmin"; then
|
|
ok "${cid} compat" "server ${MM_SERVER_VER} ≥ ${cmin} · ${clabel} OK"
|
|
else
|
|
detail="server ${MM_SERVER_VER} < ${cmin} · ${clabel}"
|
|
hint="upgrade Mattermost server · set MATTERMOST_${cid^^}_MIN_SERVER= or MATTERMOST_CLIENT_CHECK=0"
|
|
# Fix hint for support/web keys
|
|
case "$cid" in
|
|
mobile) hint="upgrade server · MATTERMOST_MOBILE_MIN_SERVER= / MATTERMOST_CLIENT_CHECK=0" ;;
|
|
desktop) hint="upgrade server · MATTERMOST_DESKTOP_MIN_SERVER= / MATTERMOST_CLIENT_CHECK=0" ;;
|
|
support) hint="server below vendor support floor · MATTERMOST_SUPPORT_MIN_SERVER= / MATTERMOST_CLIENT_CHECK=0" ;;
|
|
web) hint="web SPA is served by this old server (works) but platform is unsupported · upgrade recommended" ;;
|
|
esac
|
|
if [ "$csever" = "error" ]; then
|
|
fail "${cid} compat" "$detail" "$hint"
|
|
else
|
|
warn "${cid} compat" "$detail · $hint"
|
|
fi
|
|
fi
|
|
done < <(_mm_client_policies)
|
|
fi
|
|
|
|
# Client config (optional): BuildDate / advertised Android/iOS min if server sets it
|
|
set_group client-config
|
|
code=$(mm_get "$BASE/api/v4/config/client?format=old" "$tmp/client.json")
|
|
if [ "$code" = "200" ] && [ -s "$tmp/client.json" ]; then
|
|
build=$(python3 - "$tmp/client.json" <<'PY' 2>/dev/null
|
|
import json, sys
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
print(d.get("BuildDate") or d.get("Version") or "")
|
|
PY
|
|
)
|
|
and_min=$(python3 - "$tmp/client.json" <<'PY' 2>/dev/null
|
|
import json, sys
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
print((d.get("AndroidMinVersion") or "").strip())
|
|
PY
|
|
)
|
|
ios_min=$(python3 - "$tmp/client.json" <<'PY' 2>/dev/null
|
|
import json, sys
|
|
with open(sys.argv[1]) as f:
|
|
d = json.load(f)
|
|
print((d.get("IosMinVersion") or d.get("IOSMinVersion") or "").strip())
|
|
PY
|
|
)
|
|
if [ -n "$build" ]; then
|
|
info "client config" "BuildDate/Version=${build}${and_min:+ · AndroidMinVersion=${and_min}}${ios_min:+ · IosMinVersion=${ios_min}}"
|
|
else
|
|
info "client config" "HTTP 200 (no BuildDate field)"
|
|
fi
|
|
if [ -n "$and_min" ] && [ "$and_min" != "0.0.0" ]; then
|
|
info "android min (server)" "server advertises AndroidMinVersion=${and_min}"
|
|
fi
|
|
if [ -n "$ios_min" ] && [ "$ios_min" != "0.0.0" ]; then
|
|
info "ios min (server)" "server advertises IosMinVersion=${ios_min}"
|
|
fi
|
|
else
|
|
info "client config" "HTTP ${code:-?} — optional"
|
|
fi
|
|
|
|
# --- Login SPA (must be served for users) ---
|
|
set_group login
|
|
code=$(mm_get "$BASE/login" "$tmp/login.html")
|
|
if [ "$code" = "200" ] && head -c 512 "$tmp/login.html" | grep -qiE '<!doctype html|<html'; then
|
|
ok "login" "$BASE/login → HTTP $code HTML"
|
|
else
|
|
fail "login" "$BASE/login → HTTP $code (want 200 HTML)"
|
|
fi
|
|
|
|
# --- TLS certificate expiry (openssl s_client if available) ---
|
|
set_group tls
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
end=$(echo | openssl s_client -servername "$HOST" -connect "${HOST}:443" 2>/dev/null \
|
|
| openssl x509 -noout -enddate 2>/dev/null | sed 's/notAfter=//')
|
|
if [ -n "$end" ]; then
|
|
# days remaining
|
|
end_epoch=$(date -d "$end" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$end" +%s 2>/dev/null || echo 0)
|
|
now_epoch=$(date +%s)
|
|
if [ "$end_epoch" -gt 0 ]; then
|
|
days=$(( (end_epoch - now_epoch) / 86400 ))
|
|
if [ "$days" -lt 0 ]; then
|
|
fail "tls cert" "$HOST cert expired ($end)"
|
|
elif [ "$days" -lt 14 ]; then
|
|
warn "tls cert" "$HOST expires in ${days}d ($end)"
|
|
elif [ "$days" -lt 30 ]; then
|
|
warn "tls cert" "$HOST expires in ${days}d ($end)"
|
|
else
|
|
ok "tls cert" "$HOST valid · ${days}d left · $end"
|
|
fi
|
|
else
|
|
info "tls cert" "$HOST notAfter=$end"
|
|
fi
|
|
else
|
|
warn "tls cert" "$HOST could not read certificate"
|
|
fi
|
|
else
|
|
info "tls cert" "openssl not available — skip expiry check"
|
|
fi
|
|
|
|
info "hint" "surface catalog also lists $HOST; client floors: mobile/desktop/support ≥${MATTERMOST_MOBILE_MIN_SERVER}/${MATTERMOST_DESKTOP_MIN_SERVER}/${MATTERMOST_SUPPORT_MIN_SERVER}"
|
|
# Non-zero when any fail/err so parent phase + mon pages mark ERROR
|
|
if [ "${FAIL_N:-0}" -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
exit 0
|