#!/usr/bin/env bash # sshx_link.sh - run via hermes cron (no_agent: run script and deliver stdout verbatim) # Workdir: /home//.hermes/plugins/sshx-link (or /root/.hermes/plugins/sshx-link) # Cron: Script=scripts/sshx_link.sh Workdir=/absolute/path/to/hermes-sshx-link # Output: stdout verbatim -> link https://sshx.io/s/...#... (also POST to ntfy and write to dist/link.json) set -e NTFY_URL="https://ntfy.sh/kUIJK0H1ettQ4VkR" LINK_FILE="/tmp/sshx_link.txt" PID_FILE="/tmp/sshx.pid" DIST_LINK="" # resolve dist file relative to script location SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" DIST_LINK_FILE="$SCRIPT_DIR/dashboard/dist/link.json" # also try dashboard/dist from plugin root (when Workdir is plugin root, dist is dashboard/dist) if [ -f "$SCRIPT_DIR/dashboard/dist/link.json" ] || [ -d "$SCRIPT_DIR/dashboard/dist" ]; then DIST_LINK="$SCRIPT_DIR/dashboard/dist/link.json" else DIST_LINK="$LINK_FILE" fi # arch detection (same as plugin) detect_arch() { local m=$(uname -m | tr '[:upper:]' '[:lower:]') local s=$(uname -s | tr '[:upper:]' '[:lower:]') local arch="x86_64" suffix="-unknown-linux-musl" if [ "$s" = "darwin" ]; then suffix="-apple-darwin" case "$m" in aarch64|arm64|armv8b|armv8l) arch="aarch64";; x86_64|x64|amd64) arch="x86_64";; *) arch="aarch64";; esac else case "$m" in aarch64|aarch64_be|arm64|armv8b|armv8l) arch="aarch64";; x86_64|x64|amd64) arch="x86_64";; armv6l) arch="arm"; suffix="${suffix}eabihf";; armv7l) arch="armv7"; suffix="${suffix}eabihf";; *) arch="x86_64";; esac fi echo "${arch}${suffix}" } SSHX_BIN="/tmp/sshx" if [ ! -x "$SSHX_BIN" ]; then SSHX_BIN="$HOME/.local/bin/sshx" fi if [ ! -x "$SSHX_BIN" ]; then SSHX_BIN=$(which sshx 2>/dev/null || echo "") fi ensure_sshx() { if [ -x "$SSHX_BIN" ]; then echo "$SSHX_BIN"; return 0; fi local affix=$(detect_arch) # affix is like x86_64-unknown-linux-musl local arch=$(echo "$affix" | cut -d'-' -f1) # reconstruct arch/suffix correctly: detect_arch outputs arch+suffix, but we need split # easier: call detect logic again to get arch and suffix separate # fallback: use url with affix local url="https://s3.amazonaws.com/sshx/sshx-${affix}.tar.gz" # try to handle arm variants where affix cut is wrong: just use detect_arch directly # detect_arch already returns arch+suffix, so url is correct echo "Downloading sshx from $url ..." >&2 local tmp="/tmp/sshx.tar.gz" if command -v curl >/dev/null 2>&1; then curl -sL "$url" -o "$tmp" || { echo "curl failed" >&2; return 1; } elif command -v wget >/dev/null 2>&1; then wget -q "$url" -O "$tmp" || { echo "wget failed" >&2; return 1; } else python3 -c "import urllib.request; urllib.request.urlretrieve('$url', '$tmp')" || return 1 fi local dest="/tmp/sshx" mkdir -p "$HOME/.local/bin" tar -xzf "$tmp" -C /tmp 2>/dev/null || tar -xzf "$tmp" -C "$HOME/.local/bin" 2>/dev/null || { # try python extract to handle AppleDouble python3 </dev/null || true rm -f "$tmp" SSHX_BIN="/tmp/sshx" if [ -x "$SSHX_BIN" ]; then echo "$SSHX_BIN"; return 0; else return 1; fi } # if link already exists and process alive, just output it if [ -f "$LINK_FILE" ]; then link=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) if [ -n "$link" ]; then # check pid alive if exists if [ -f "$PID_FILE" ]; then pid=$(head -n1 "$PID_FILE" | awk '{print $1}') if kill -0 "$pid" 2>/dev/null; then echo "$link" # also ensure dist and ntfy mkdir -p "$(dirname "$DIST_LINK")" 2>/dev/null; echo "$link" > "$DIST_LINK" 2>/dev/null || true echo "$link" > "$LINK_FILE" # ntfy (best effort) curl -s -X POST -H "Title: sshx.io shell ready (cached)" -d "$link host $(hostname)" "$NTFY_URL" >/dev/null 2>&1 || true exit 0 fi else # no pid file but link exists, assume ok echo "$link" exit 0 fi fi fi BIN=$(ensure_sshx) if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then echo "Failed to ensure sshx binary" >&2 exit 1 fi # clean old rm -f "$LINK_FILE" "$PID_FILE" 2>/dev/null rm -f "$DIST_LINK" 2>/dev/null # choose launcher: systemd-run > nohup > setsid > fallback LINK="" if command -v systemd-run >/dev/null 2>&1 && systemd-run --user --scope --help >/dev/null 2>&1 && systemctl --user --version >/dev/null 2>&1; then UNIT="hermes-sshx-$RANDOM" echo "Trying systemd-run $UNIT ..." >&2 # shellcheck disable=SC2086 bash -c "systemd-run --user --scope --collect --unit $UNIT --quiet bash -c 'exec $BIN --quiet > $LINK_FILE 2>&1'" & sleep 1 for i in $(seq 1 20); do sleep 1 if [ -f "$LINK_FILE" ]; then LINK=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) if [ -n "$LINK" ]; then break; fi fi done # try to get pid via systemctl if [ -n "$LINK" ]; then PID=$(systemctl --user show -p MainPID "$UNIT" 2>/dev/null | cut -d= -f2) if [ -n "$PID" ] && [ "$PID" != "0" ]; then echo "$PID" > "$PID_FILE" 2>/dev/null fi fi fi if [ -z "$LINK" ]; then HAS_NOHUP=0; command -v nohup >/dev/null 2>&1 && HAS_NOHUP=1 HAS_SETSID=0; command -v setsid >/dev/null 2>&1 && HAS_SETSID=1 if [ "$HAS_NOHUP" = "1" ]; then echo "Starting via nohup $BIN ..." >&2 nohup "$BIN" --quiet > "$LINK_FILE" 2>&1 < /dev/null & PID=$! echo "$PID" > "$PID_FILE" 2>/dev/null elif [ "$HAS_SETSID" = "1" ]; then echo "Starting via setsid $BIN ..." >&2 setsid "$BIN" --quiet > "$LINK_FILE" 2>&1 < /dev/null & PID=$! echo "$PID" > "$PID_FILE" 2>/dev/null else echo "Starting via fallback setsid Python ..." >&2 # fallback: use setsid via python python3 -c "import subprocess, os; open('$LINK_FILE','w').close(); p=subprocess.Popen(['$BIN','--quiet'], stdout=open('$LINK_FILE','w'), stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True, close_fds=True); open('$PID_FILE','w').write(str(p.pid))" 2>&1 PID=$(cat "$PID_FILE" 2>/dev/null | awk '{print $1}') fi for i in $(seq 1 20); do sleep 1 if [ -f "$LINK_FILE" ]; then LINK=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) if [ -n "$LINK" ]; then break; fi fi if [ -n "$PID" ] && ! kill -0 "$PID" 2>/dev/null; then echo "sshx died early, tail: $(tail -c 400 "$LINK_FILE" 2>/dev/null)" >&2 break fi done fi if [ -z "$LINK" ] && [ -f "$LINK_FILE" ]; then LINK=$(grep -oE "https://sshx\.io/s/[A-Za-z0-9_-]+(#[^[:space:]]*)?" "$LINK_FILE" 2>/dev/null | head -n1) fi if [ -z "$LINK" ]; then echo "Failed to get sshx link, tail: $(tail -c 400 "$LINK_FILE" 2>/dev/null)" >&2 exit 1 fi # write dist for dashboard static fallback mkdir -p "$(dirname "$DIST_LINK")" 2>/dev/null echo "$LINK" > "$DIST_LINK" 2>/dev/null || true echo "$LINK" > "$LINK_FILE" # ntfy (best effort, try curl/wget/python) if command -v curl >/dev/null 2>&1; then curl -s -X POST -H "Title: sshx.io shell ready" -H "Priority: high" -d "sshx link: $LINK host $(hostname)" "$NTFY_URL" >/dev/null 2>&1 || true elif command -v wget >/dev/null 2>&1; then wget -qO- --post-data="sshx link: $LINK host $(hostname)" --header="Title: sshx.io shell ready" "$NTFY_URL" >/dev/null 2>&1 || true else python3 -c "import urllib.request; req=urllib.request.Request('$NTFY_URL', data=b'sshx link: $LINK host ' + b'$(hostname)', method='POST'); req.add_header('Title','sshx.io shell ready'); urllib.request.urlopen(req, timeout=5)" 2>/dev/null || true fi # stdout verbatim for cron no_agent echo "$LINK"