Files
VoiletSshTools/smoketest/run.sh
T
liyp 4365e80075 sshtools: 非交互 SSH 工具集(golink / goscp)
golink: 一条命令在远端执行,退出码原样透传,从不提示、不分配终端。
goscp:  用 SCP 协议上传,-files 一个参数同时接受文件和目录。

- 纯 Go,唯一依赖 golang.org/x/crypto,可离线交叉编译到六个桌面平台
- internal/sshx 统一建连、认证(password + keyboard-interactive)与主机密钥策略
- smoketest/ 自带迷你 sshd,57 项端到端测试(golink 38 + goscp 19)
2026-10-19 10:22:53 +08:00

204 lines
9.1 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# End-to-end smoke test for golink against the bundled miniature sshd.
#
# ./smoketest/run.sh
#
# Builds golink and the test server into .smoketest/bin, starts the server on a
# free port and checks stdout, stderr and exit status for every case. Each case
# runs with stdin closed and a hard timeout, which also proves golink never waits
# for interactive input. Exits non-zero if any case fails.
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(dirname "$here")"
work="$root/.smoketest"
export GOCACHE="${GOCACHE:-$root/.gocache}"
USER_NAME=tester
PASS_WORD=s3cr3t
pass=0
fail=0
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
bad() { fail=$((fail + 1)); printf ' FAIL %s\n' "$1"; [ -n "${2:-}" ] && printf ' %s\n' "$2"; }
rm -rf "$work"
mkdir -p "$work/bin"
printf 'building\n'
go build -o "$work/bin/golink" ./cmd/golink || exit 1
go build -o "$work/bin/sshd" ./smoketest/sshd || exit 1
sshd_pids=()
cleanup() {
local p
for p in ${sshd_pids[@]+"${sshd_pids[@]}"}; do kill "$p" 2>/dev/null; done
wait 2>/dev/null
}
trap cleanup EXIT
start_sshd() { # <name> <auth-mode> [extra sshd flags...]
local name="$1" mode="$2" i
shift 2
"$work/bin/sshd" -addr 127.0.0.1:0 -auth "$mode" -user "$USER_NAME" -pass "$PASS_WORD" \
-hostkey "$work/hostkey" -knownhosts-out "$work/known_hosts_$name" "$@" \
>"$work/$name.out" 2>"$work/$name.err" &
sshd_pids+=($!)
for i in $(seq 1 100); do
grep -q '^HOSTKEY_SHA256=' "$work/$name.out" 2>/dev/null && break
sleep 0.05
done
ADDR="$(sed -n 's/^ADDR=//p' "$work/$name.out" | head -1)"
PORT="${ADDR##*:}"
FINGERPRINT="$(sed -n 's/^HOSTKEY_SHA256=//p' "$work/$name.out" | head -1)"
if [ -z "$PORT" ]; then
echo "sshd ($name) did not start: $(cat "$work/$name.err")" >&2
exit 1
fi
}
# gl runs golink with stdin closed and a watchdog, so a hang shows up as a
# failure instead of a stuck test run.
gl() { timeout 30 "$work/bin/golink" "$@" </dev/null; }
expect_status() { # <name> <want-status> <args...>
local name="$1" want="$2"; shift 2
local status
gl "$@" >/dev/null 2>"$work/last.err"
status=$?
if [ "$status" -ne "$want" ]; then
bad "$name" "exit=$status want=$want; stderr: $(head -1 "$work/last.err")"
else
ok "$name"
fi
}
expect_output() { # <name> <want-status> <want-stdout> <args...>
local name="$1" want="$2" wantout="$3"; shift 3
local status out
out="$(gl "$@" 2>"$work/last.err")"
status=$?
if [ "$status" -ne "$want" ]; then
bad "$name" "exit=$status want=$want; stderr: $(head -1 "$work/last.err")"
elif [ "$out" != "$wantout" ]; then
bad "$name" "stdout=$(printf '%q' "$out") want=$(printf '%q' "$wantout")"
else
ok "$name"
fi
}
start_sshd main both
HOST=127.0.0.1
TARGET="$USER_NAME@$HOST"
G=( "$TARGET" -p "$PASS_WORD" -port "$PORT" )
printf '\n== basic execution ==\n'
expect_output "echo" 0 "hello from golink" "${G[@]}" -command "echo hello from golink"
expect_output "flags in any order" 0 "any-order" -command "echo any-order" -p "$PASS_WORD" "$TARGET" -port "$PORT"
expect_output "host:port inline" 0 "inline" "$TARGET:$PORT" -p "$PASS_WORD" -command "echo inline"
expect_output "-l instead of user@host" 0 "via-l" -l "$USER_NAME" "$HOST" -port "$PORT" -p "$PASS_WORD" -command "echo via-l"
expect_output "trailing args become the command" 0 "a b c" "$TARGET" -p "$PASS_WORD" -port "$PORT" echo a b c
expect_output "shell pipeline remotely" 0 "2" "${G[@]}" -command "printf 'x\ny\n' | wc -l"
expect_output "-pw plink-style alias" 0 "pw-ok" "$TARGET" -pw "$PASS_WORD" -port "$PORT" -command "echo pw-ok"
printf '\n== exit status passthrough ==\n'
expect_status "exit 0" 0 "${G[@]}" -command "true"
expect_status "exit 7" 7 "${G[@]}" -command "exit 7"
expect_status "exit 42" 42 "${G[@]}" -command "exit 42"
expect_status "unknown command -> 127" 127 "${G[@]}" -command "definitely-not-a-real-command-xyz"
expect_status "killed by SIGTERM -> 143" 143 "${G[@]}" -command 'kill -TERM $$'
expect_status "killed by SIGKILL -> 137" 137 "${G[@]}" -command 'kill -KILL $$'
printf '\n== stdout / stderr separation ==\n'
out="$(gl "${G[@]}" -command "echo to-stdout; echo to-stderr 1>&2" 2>"$work/last.err")"
if [ "$out" = "to-stdout" ] && grep -qx 'to-stderr' "$work/last.err"; then
ok "remote stderr stays on stderr"
else
bad "remote stderr stays on stderr" "stdout=$(printf '%q' "$out") stderr=$(cat "$work/last.err")"
fi
printf '\n== stdin piping ==\n'
if printf 'piped-data\n' | timeout 30 "$work/bin/golink" "${G[@]}" -command "cat" >"$work/stdin.out" 2>/dev/null; then
if [ "$(cat "$work/stdin.out")" = "piped-data" ]; then
ok "stdin piped into remote cat"
else
bad "stdin piped into remote cat" "got $(printf '%q' "$(cat "$work/stdin.out")")"
fi
else
bad "stdin piped into remote cat" "golink failed"
fi
head -c 200000 /dev/urandom >"$work/blob"
got="$(timeout 30 "$work/bin/golink" "${G[@]}" -command "wc -c" <"$work/blob" 2>/dev/null | tr -d ' ')"
[ "$got" = "200000" ] && ok "200000 byte stdin round trip" || bad "200000 byte stdin round trip" "got $got"
printf '\n== larger output ==\n'
lines="$(gl "${G[@]}" -command "seq 1 20000" 2>/dev/null | wc -l)"
[ "$lines" = "20000" ] && ok "20000 lines streamed back" || bad "20000 lines streamed back" "got $lines"
printf '\n== host key policy ==\n'
expect_status "-hostkey correct fingerprint" 0 "${G[@]}" -hostkey "$FINGERPRINT" -command "true"
expect_status "-hostkey wrong fingerprint -> 255" 255 "${G[@]}" -hostkey "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" -command true
expect_output "-knownhosts file" 0 "known-ok" "${G[@]}" -knownhosts "$work/known_hosts_main" -command "echo known-ok"
: >"$work/empty_known_hosts"
expect_status "-knownhosts unknown host -> 255" 255 "${G[@]}" -knownhosts "$work/empty_known_hosts" -command true
printf '\n== failures are fast, never interactive ==\n'
expect_status "wrong password -> 255" 255 "$TARGET" -p wrong -port "$PORT" -command true
expect_status "wrong user -> 255" 255 "nosuchuser@$HOST" -p "$PASS_WORD" -port "$PORT" -command true
expect_status "no credentials -> 2" 2 "$TARGET" -port "$PORT" -command true
expect_status "missing user -> 2" 2 "$HOST" -p "$PASS_WORD" -port "$PORT" -command true
expect_status "missing command -> 2" 2 "${G[@]}"
expect_status "connection refused -> 255" 255 "$TARGET" -p "$PASS_WORD" -port 1 -timeout 3s -command true
expect_status "-h exits 0" 0 -h
expect_status "-version exits 0" 0 -version
printf '\n== verbose output does not pollute stdout ==\n'
out="$(gl "${G[@]}" -v -command "echo v-ok" 2>"$work/last.err")"
if [ "$out" = "v-ok" ] && grep -q 'host key SHA256:' "$work/last.err"; then
ok "-v diagnostics go to stderr"
else
bad "-v diagnostics go to stderr" "stdout=$(printf '%q' "$out") stderr=$(head -3 "$work/last.err" | tr '\n' '|')"
fi
printf '\n== GOLINK_PASSWORD ==\n'
out="$(GOLINK_PASSWORD="$PASS_WORD" timeout 30 "$work/bin/golink" "$TARGET" -port "$PORT" -command "echo from-env" </dev/null 2>"$work/last.err")"
if [ "$out" = "from-env" ]; then
ok "GOLINK_PASSWORD supplies the password"
else
bad "GOLINK_PASSWORD supplies the password" "stdout=$(printf '%q' "$out") stderr=$(head -1 "$work/last.err")"
fi
printf '\n== keyboard-interactive only server ==\n'
start_sshd kbd keyboard-interactive
expect_output "keyboard-interactive login" 0 "kbd-ok" "$USER_NAME@$HOST" -p "$PASS_WORD" -port "$PORT" -command "echo kbd-ok"
expect_status "keyboard-interactive wrong password -> 255" 255 "$USER_NAME@$HOST" -p nope -port "$PORT" -command true
printf '\n== public key authentication ==\n'
if command -v ssh-keygen >/dev/null 2>&1; then
ssh-keygen -q -t ed25519 -N "" -C golink-smoketest -f "$work/id_ed25519" >/dev/null 2>&1
ssh-keygen -q -t ed25519 -N "" -C golink-other -f "$work/id_other" >/dev/null 2>&1
ssh-keygen -q -t ed25519 -N "hunter2" -C golink-locked -f "$work/id_locked" >/dev/null 2>&1
start_sshd key publickey -authorized-key "$work/id_ed25519.pub"
expect_output "-i private key" 0 "key-ok" "$USER_NAME@$HOST" -port "$PORT" -i "$work/id_ed25519" -command "echo key-ok"
expect_status "unauthorized key -> 255" 255 "$USER_NAME@$HOST" -port "$PORT" -i "$work/id_other" -command true
gl "$USER_NAME@$HOST" -port "$PORT" -i "$work/id_locked" -command true >/dev/null 2>"$work/last.err"
lock_status=$?
if [ "$lock_status" -eq 255 ] && grep -qi 'passphrase' "$work/last.err"; then
ok "encrypted key fails with a clear message"
else
bad "encrypted key fails with a clear message" "exit=$lock_status stderr: $(head -1 "$work/last.err")"
fi
# -auth publickey server: only the exact key is accepted, and a key-only
# client must fail against a password-only server instead of prompting
start_sshd pwdonly password
expect_status "key rejected by password-only server -> 255" 255 "$USER_NAME@$HOST" -port "$PORT" -i "$work/id_ed25519" -command true
expect_output "key and password together" 0 "mix-ok" "$USER_NAME@$HOST" -port "$PORT" -i "$work/id_ed25519" -p "$PASS_WORD" -command "echo mix-ok"
else
printf ' skip ssh-keygen not available, public key cases skipped\n'
fi
printf '\n%d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]