#!/bin/bash # MyLogin.world Bridge 2.0.0 for one app: connects it to your dashboard, runs it on this host (in Docker when this host # has it, else as a systemd service), then sets the app up to sign in through it. Run the command your dashboard shows, # as root, on the host the app runs on: --app is the app, --address where people open it, --code is valid 24 hours. # --host is this host's address, when the app's address points elsewhere (a proxy, a cluster); --yes answers yes. set -euo pipefail usage() { echo "Usage: bash <(curl -fsSL https://mylogin.world.compas.cs.stonybrook.edu/install/oidc) --app --code --address [--host ] [--yes]. Copy the command from your dashboard." >&2; exit 1; } app= code= address= host= yes= while [ $# -gt 0 ]; do [ "$1" = --yes ] && { yes=--yes; shift; continue; } [ $# -ge 2 ] || usage case $1 in --app) app=$2 ;; --code) code=$2 ;; --address) address=$2 ;; --host) host=$2 ;; *) usage ;; esac shift 2 done [ -n "$app" ] && [ -n "$code" ] && [ -n "$address" ] || usage [ "$(id -u)" = 0 ] || { echo "Run this as root (sudo -i)." >&2; exit 1; } if command -v docker >/dev/null && docker info >/dev/null 2>&1; then mode=docker elif [ -d /run/systemd/system ]; then mode=systemd else echo "This host has neither Docker nor systemd." >&2; exit 1; fi # the request carries no quotes or backslashes, so it needs no JSON tool safe='^[][A-Za-z0-9._:/@%+-]+$' hostname=$(hostname) [[ $app =~ ^[a-z]+$ && $code =~ $safe && $address =~ $safe && $hostname =~ $safe && $host =~ ^[][A-Za-z0-9.:-]*$ ]] || { echo "Unexpected characters in --app, --code, --address or --host." >&2; exit 1; } # the bridge runs here, at the app's address unless --host says otherwise: that must be this host if [ -z "$host" ]; then name=${address#*://} name=${name%%/*} case $name in \[*) name=${name#[} name=${name%%]*} ;; *) name=${name%:*} ;; esac mine=$(ip -o addr show 2>/dev/null | awk '{ sub("/.*", "", $4); print $4 }') for ip in $name $(getent ahosts "$name" 2>/dev/null | awk '{ print $1 }'); do grep -qxF "$ip" <<<"$mine" && { host=-; break; }; done [ "$host" = - ] || { echo "$name isn't this host. Run this on the host the app runs on, or add --host ." >&2; exit 1; } host= fi res=$(printf '{"code":"%s","address":"%s","hostname":"%s","app":"%s","host":"%s"}' "$code" "$address" "$hostname" "$app" "$host" | curl -sS -H 'content-type: application/json' --data-binary @- -w '\n%{http_code}' https://mylogin.world.compas.cs.stonybrook.edu/install/oidc) || { echo "Could not reach https://mylogin.world.compas.cs.stonybrook.edu." >&2; exit 1; } status=${res##*$'\n'} body=${res%$'\n'*} field() { printf %s "$body" | sed -n "s/.*\"$1\": *\"\([^\"]*\)\".*/\1/p"; } [ "$status" = 200 ] || { msg=$(field message); echo "${msg:-Setup failed (HTTP $status). Try again.}" >&2; exit 1; } slug=$(field slug) relay=$(field relay) app_url=$(field appUrl) admin=$(field admin) port=${app_url##*:} name=mylogin-$app setup=(setup --app "$app" --url "$address" --slug "$slug" --relay "$relay" --app-url "$app_url" --admin "$admin") free() { ! command -v ss >/dev/null || [ -z "$(ss -Hltn "( sport = :$port or sport = :$((port + 1)) )")" ] || { echo "Port $port or $((port + 1)) is in use on this host; the bridge for $app needs both." >&2; exit 1; } } if [ $mode = docker ]; then image=myloginworld/bridge:2.0.0 docker pull -q "$image" >/dev/null docker rm -f "$name" >/dev/null 2>&1 || true free # a new volume takes the image's data directory, owned by the container's user docker run --rm -v "$name:/var/lib/mylogin-oidc" "$image" "${setup[@]}" docker run -d --name "$name" --restart unless-stopped -p "$port:$port" -p "$((port + 1)):$((port + 1))" \ -v "$name:/var/lib/mylogin-oidc" "$image" >/dev/null # the same program on the host sets the app up and runs bridge commands id=$(docker create "$image") docker cp "$id:/usr/local/bin/mylogin-oidc" /usr/local/bin/mylogin-oidc >/dev/null docker rm "$id" >/dev/null else case $(uname -m) in x86_64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; armv7l|armv8l) arch=armv7 ;; *) arch=$(uname -m) ;; esac case $arch in amd64) file=mylogin-oidc-2.0.0-linux-amd64 sum=b015340b12b1910f487ea32a4aa682944c669abfd409062fa057b9ce449112af ;; arm64) file=mylogin-oidc-2.0.0-linux-arm64 sum=1d486bb9f6c505b257f80785fe60ec458af4a41ec1b35ecd76405b007de55476 ;; armv7) file=mylogin-oidc-2.0.0-linux-armv7 sum=9898e853d9414b0e5100e5cedcdb97c4eda8a5b71f711864d11cab162b09672c ;; *) echo "Unsupported architecture: $arch" >&2; exit 1 ;; esac dir=$(mktemp -d); trap 'rm -rf "$dir"' EXIT curl -fsSL -o "$dir/$file" https://mylogin.world.compas.cs.stonybrook.edu/downloads/oidc/"$file" echo "$sum $dir/$file" | sha256sum -c --quiet - || { echo "Checksum mismatch; nothing installed." >&2; exit 1; } install -m 755 "$dir/$file" /usr/local/bin/mylogin-oidc cat >/etc/systemd/system/mylogin-oidc@.service <<'UNIT' [Unit] Description=MyLogin.world Bridge for %i After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/local/bin/mylogin-oidc --dir /var/lib/mylogin-oidc/%i serve Restart=on-failure StateDirectory=mylogin-oidc/%i StateDirectoryMode=0700 NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes [Install] WantedBy=multi-user.target UNIT systemctl stop "mylogin-oidc@$app" 2>/dev/null || true free mkdir -p -m 700 "/var/lib/mylogin-oidc/$app" mylogin-oidc --dir "/var/lib/mylogin-oidc/$app" "${setup[@]}" systemctl daemon-reload systemctl enable --now "mylogin-oidc@$app" >/dev/null 2>&1 fi for _ in $(seq 40); do mylogin-oidc "$app" status 2>/dev/null | grep -q '^answering *yes' && break; sleep 0.25; done mylogin-oidc "$app" status echo mylogin-oidc configure "$app" $yes