Retry and serialize pineapd/hak5 calls, queue virtual-pager keys, and grey out buttons until the pager finishes. Deploy now installs python3-light after factory firmware. Bump version to 1.3.2. Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
7.9 KiB
Bash
Executable File
236 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
PAGER_HOST="172.16.52.1"
|
|
PAGER_USER="root"
|
|
PASSWORD=""
|
|
SSH_KEY=""
|
|
BUILD_DIR=""
|
|
PORTAL_REFRESH=true
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/deploy.sh [options]
|
|
|
|
Options:
|
|
--host HOST Pager address (default: 172.16.52.1)
|
|
--user USER SSH user (default: root)
|
|
--password PASSWORD SSH/device password (requires sshpass)
|
|
--ssh-key PATH SSH private key
|
|
--build-dir PATH Build output directory (default: <repo>/build)
|
|
--no-portal-refresh Skip the best-effort portal refresh
|
|
-h, --help Show this help
|
|
|
|
If neither --password nor --ssh-key is supplied, ssh/scp prompt normally.
|
|
EOF
|
|
}
|
|
|
|
while (($#)); do
|
|
case "$1" in
|
|
--host) PAGER_HOST="${2:?missing value for --host}"; shift 2 ;;
|
|
--user) PAGER_USER="${2:?missing value for --user}"; shift 2 ;;
|
|
--password) PASSWORD="${2:?missing value for --password}"; shift 2 ;;
|
|
--ssh-key) SSH_KEY="${2:?missing value for --ssh-key}"; shift 2 ;;
|
|
--build-dir) BUILD_DIR="${2:?missing value for --build-dir}"; shift 2 ;;
|
|
--no-portal-refresh) PORTAL_REFRESH=false; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
for command in python3 zip scp ssh; do
|
|
command -v "$command" >/dev/null || {
|
|
printf 'Required command not found: %s\n' "$command" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
if [[ -n "$PASSWORD" ]] && ! command -v sshpass >/dev/null; then
|
|
printf 'Password deployment requires sshpass (brew install hudochenkov/sshpass/sshpass).\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
PAYLOAD_KEY="pager-webui"
|
|
PAYLOAD_CATEGORY="remote_access"
|
|
PAYLOAD_DIR="$ROOT/payload/user/$PAYLOAD_CATEGORY/$PAYLOAD_KEY"
|
|
BUILD_DIR="${BUILD_DIR:-$ROOT/build}"
|
|
OUT_DIR="$BUILD_DIR/$PAYLOAD_KEY"
|
|
STAGE="$OUT_DIR/stage"
|
|
|
|
[[ -d "$PAYLOAD_DIR" ]] || {
|
|
printf 'Payload directory not found: %s\n' "$PAYLOAD_DIR" >&2
|
|
exit 1
|
|
}
|
|
|
|
mkdir -p "$OUT_DIR"
|
|
rm -rf "$STAGE"
|
|
mkdir -p "$STAGE/user/$PAYLOAD_CATEGORY"
|
|
cp -R "$PAYLOAD_DIR" "$STAGE/user/$PAYLOAD_CATEGORY/$PAYLOAD_KEY"
|
|
find "$STAGE" \( -type d -name __pycache__ -o -type f -name '*.pyc' \) -prune -exec rm -rf {} +
|
|
|
|
B64_KEY="$(python3 -c 'import base64; print(base64.urlsafe_b64encode(b"pager-webui").decode().rstrip("="))')"
|
|
ZIP_NAME="payload-$B64_KEY.zip"
|
|
ZIP_PATH="$OUT_DIR/$ZIP_NAME"
|
|
MANIFEST_PATH="$OUT_DIR/_hak5_manifest.json"
|
|
rm -f "$ZIP_PATH"
|
|
(
|
|
cd "$STAGE"
|
|
zip -q -r "$ZIP_PATH" user
|
|
)
|
|
|
|
HASH="$(python3 -c 'import hashlib, sys; print(hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest())' "$ZIP_PATH")"
|
|
python3 - "$PAYLOAD_DIR/_hak5_manifest.json" "$MANIFEST_PATH" "$HASH" "$ZIP_NAME" <<'PY'
|
|
import json
|
|
import sys
|
|
import time
|
|
|
|
source, destination, digest, zip_name = sys.argv[1:]
|
|
with open(source, encoding='utf-8') as handle:
|
|
manifest = json.load(handle)
|
|
manifest['time'] = int(time.time())
|
|
manifest['last_hash'] = digest
|
|
manifest['zip'] = zip_name
|
|
with open(destination, 'w', encoding='ascii') as handle:
|
|
json.dump(manifest, handle, indent=2)
|
|
handle.write('\n')
|
|
PY
|
|
printf 'Built: %s\n' "$ZIP_PATH"
|
|
|
|
TARGET="$PAGER_USER@$PAGER_HOST"
|
|
SSH_OPTS=(-o StrictHostKeyChecking=accept-new)
|
|
if [[ -n "$PASSWORD" && -z "$SSH_KEY" ]]; then
|
|
SSH_OPTS+=(-o PreferredAuthentications=password -o PubkeyAuthentication=no)
|
|
fi
|
|
|
|
run_scp() {
|
|
if [[ -n "$PASSWORD" && -n "$SSH_KEY" ]]; then
|
|
SSHPASS="$PASSWORD" sshpass -e scp "${SSH_OPTS[@]}" -i "$SSH_KEY" "$@"
|
|
elif [[ -n "$PASSWORD" ]]; then
|
|
SSHPASS="$PASSWORD" sshpass -e scp "${SSH_OPTS[@]}" "$@"
|
|
elif [[ -n "$SSH_KEY" ]]; then
|
|
scp "${SSH_OPTS[@]}" -i "$SSH_KEY" "$@"
|
|
else
|
|
scp "${SSH_OPTS[@]}" "$@"
|
|
fi
|
|
}
|
|
|
|
run_ssh() {
|
|
if [[ -n "$PASSWORD" && -n "$SSH_KEY" ]]; then
|
|
SSHPASS="$PASSWORD" sshpass -e ssh "${SSH_OPTS[@]}" -i "$SSH_KEY" "$@"
|
|
elif [[ -n "$PASSWORD" ]]; then
|
|
SSHPASS="$PASSWORD" sshpass -e ssh "${SSH_OPTS[@]}" "$@"
|
|
elif [[ -n "$SSH_KEY" ]]; then
|
|
ssh "${SSH_OPTS[@]}" -i "$SSH_KEY" "$@"
|
|
else
|
|
ssh "${SSH_OPTS[@]}" "$@"
|
|
fi
|
|
}
|
|
|
|
install_python3() {
|
|
if run_ssh "$TARGET" 'command -v python3 >/dev/null'; then
|
|
printf 'python3 already present on the pager.\n'
|
|
return 0
|
|
fi
|
|
printf 'python3 missing on pager; installing python3-light (offline ipks).\n'
|
|
local cache="$ROOT/build/python-ipk"
|
|
local pkg_base='https://downloads.openwrt.org/releases/24.10.1/packages/mipsel_24kc'
|
|
mkdir -p "$cache"
|
|
local files=(
|
|
"base/libbz2-1.0_1.0.8-r1_mipsel_24kc.ipk"
|
|
"packages/libpython3-3.11_3.11.14-r1_mipsel_24kc.ipk"
|
|
"packages/python3-base_3.11.14-r1_mipsel_24kc.ipk"
|
|
"packages/python3-light_3.11.14-r1_mipsel_24kc.ipk"
|
|
)
|
|
local names=()
|
|
local rel
|
|
for rel in "${files[@]}"; do
|
|
local name="${rel##*/}"
|
|
names+=("$name")
|
|
if [[ ! -s "$cache/$name" ]]; then
|
|
curl -fsSL --retry 3 -o "$cache/$name" "$pkg_base/$rel"
|
|
fi
|
|
done
|
|
run_ssh "$TARGET" 'mkdir -p /tmp/python-ipk && rm -rf /tmp/python-ipk/*'
|
|
(
|
|
cd "$cache"
|
|
run_scp "${names[@]}" "$TARGET:/tmp/python-ipk/"
|
|
)
|
|
run_ssh "$TARGET" 'set -e
|
|
cd /tmp/python-ipk
|
|
opkg install libbz2-1.0_*.ipk libpython3-3.11_*.ipk python3-base_*.ipk python3-light_*.ipk
|
|
command -v python3 >/dev/null
|
|
python3 -c "import json,socket,hashlib,threading,select,subprocess,struct,base64,re"
|
|
rm -rf /tmp/python-ipk
|
|
echo PYTHON_OK'
|
|
}
|
|
|
|
install_python3
|
|
|
|
run_scp "$ZIP_PATH" "$MANIFEST_PATH" "$TARGET:/tmp/"
|
|
|
|
REMOTE_PAYLOAD_DIR="user/$PAYLOAD_CATEGORY/$PAYLOAD_KEY"
|
|
LEGACY_PAYLOAD_DIR="user/general/$PAYLOAD_KEY"
|
|
REMOTE_COMMAND="set -e
|
|
cd /root/payloads
|
|
stage='.pager-webui.deploy.\$\$'
|
|
backup='.pager-webui.backup.\$\$'
|
|
trap 'rm -rf \"\$stage\" \"\$backup\"' EXIT
|
|
mkdir -p \"\$stage\"
|
|
cd \"\$stage\"
|
|
unzip -q '/tmp/$ZIP_NAME'
|
|
new=\"\$PWD/$REMOTE_PAYLOAD_DIR\"
|
|
[ -f \"\$new/server.py\" ] && [ -f \"\$new/payload.sh\" ] && [ -d \"\$new/www\" ]
|
|
cp /tmp/_hak5_manifest.json \"\$new/_hak5_manifest.json\"
|
|
chmod +x \"\$new/payload.sh\" \"\$new/pagerwebui.init\"
|
|
chmod -R 755 \"\$new/www\"
|
|
cd /root/payloads
|
|
if [ -d '$REMOTE_PAYLOAD_DIR' ]; then
|
|
mkdir -p \"\$(dirname \"\$backup\")\"
|
|
mv '$REMOTE_PAYLOAD_DIR' \"\$backup\"
|
|
fi
|
|
if mv \"\$new\" '$REMOTE_PAYLOAD_DIR'; then
|
|
rm -rf \"\$backup\" '$LEGACY_PAYLOAD_DIR'
|
|
else
|
|
[ ! -d \"\$backup\" ] || mv \"\$backup\" '$REMOTE_PAYLOAD_DIR'
|
|
exit 1
|
|
fi
|
|
rm -f '/tmp/$ZIP_NAME' /tmp/_hak5_manifest.json
|
|
cp '$REMOTE_PAYLOAD_DIR/pagerwebui.init' /etc/init.d/pagerwebui
|
|
chmod +x /etc/init.d/pagerwebui
|
|
/etc/init.d/pagerwebui enable
|
|
if /etc/init.d/pagerwebui running >/dev/null 2>&1; then
|
|
/etc/init.d/pagerwebui restart
|
|
else
|
|
/etc/init.d/pagerwebui start
|
|
fi
|
|
echo EXTRACT_OK"
|
|
run_ssh "$TARGET" "$REMOTE_COMMAND"
|
|
printf 'Installed to /root/payloads/%s/\n' "$REMOTE_PAYLOAD_DIR"
|
|
|
|
if $PORTAL_REFRESH && [[ -n "$PASSWORD" ]]; then
|
|
PASSWORD_B64="$(printf '%s' "$PASSWORD" | base64)"
|
|
PORTAL_OK=false
|
|
for attempt in 1 2 3; do
|
|
if printf '%s\n' "$PASSWORD_B64" | run_ssh "$TARGET" 'read -r password_b64
|
|
password=$(printf "%s" "$password_b64" | base64 -d)
|
|
login_body=$(printf "%s" "$password" | python3 -c '"'"'import json, sys; print(json.dumps({"username": "root", "password": sys.stdin.read()}))'"'"')
|
|
token=$(curl -sS -X POST http://127.0.0.1:1471/api/login -H "Content-Type: application/json" -d "$login_body" |
|
|
python3 -c '"'"'import json, sys; print(json.load(sys.stdin).get("token", ""))'"'"')
|
|
[ -n "$token" ] &&
|
|
curl -fsS -X POST http://127.0.0.1:1471/api/payloads/portal/refresh -H "Authorization: Bearer $token" >/dev/null'; then
|
|
PORTAL_OK=true
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
if $PORTAL_OK; then
|
|
printf 'Portal refreshed.\n'
|
|
else
|
|
printf 'Warning: portal refresh failed; payload installation is complete.\n' >&2
|
|
fi
|
|
elif $PORTAL_REFRESH; then
|
|
printf 'Skipping portal refresh without --password; payload installation is complete.\n'
|
|
fi
|
|
|
|
printf 'Deploy complete. Browse http://%s:8080/\n' "$PAGER_HOST"
|