hermes-sshx-link/scripts/sshx_link.sh

228 lines
8.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# sshx_link.sh - run via hermes cron (no_agent: run script and deliver stdout verbatim)
# Workdir: /home/<user>/.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 <<PY
import tarfile, os, shutil, stat
tf=tarfile.open("$tmp","r:gz")
m=None
for x in tf.getmembers():
import os as _o
b=_o.path.basename(x.name)
if b.startswith("._"): continue
if b=="sshx" and x.isfile():
m=x; break
if not m:
for x in tf.getmembers():
b=_o.path.basename(x.name)
if b.startswith("._"): continue
if x.isfile():
m=x; break
import tempfile
tmpdir=tempfile.mkdtemp()
try:
tf.extract(m, path=tmpdir, filter='fully_trusted')
except: tf.extract(m, path=tmpdir)
import os as _o2
ext=_o2.path.join(tmpdir, m.name)
if not _o2.path.exists(ext):
for r,_,files in _o2.walk(tmpdir):
if "sshx" in files:
ext=_o2.path.join(r,"sshx"); break
shutil.copy2(ext, "$dest")
os.chmod("$dest", os.stat("$dest").st_mode | stat.S_IEXEC)
try: shutil.copy2("$dest","$HOME/.local/bin/sshx")
except: pass
PY
}
# if still not at /tmp/sshx, try find
if [ ! -x "/tmp/sshx" ] && [ -x "$HOME/.local/bin/sshx" ]; then
cp "$HOME/.local/bin/sshx" /tmp/sshx
fi
chmod +x /tmp/sshx 2>/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"