golink: 一条命令在远端执行,退出码原样透传,从不提示、不分配终端。 goscp: 用 SCP 协议上传,-files 一个参数同时接受文件和目录。 - 纯 Go,唯一依赖 golang.org/x/crypto,可离线交叉编译到六个桌面平台 - internal/sshx 统一建连、认证(password + keyboard-interactive)与主机密钥策略 - smoketest/ 自带迷你 sshd,57 项端到端测试(golink 38 + goscp 19)
204 lines
9.1 KiB
Bash
Executable File
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 ]
|