90af5cbb
test: ssh's prompts, both exclusions, and the socket's hygiene
a73x 2026-08-30 10:59
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -105,7 +105,7 @@ command -v python3 > /dev/null 2>&1 || { | |||
| 105 | E2E_DIR=$(dirname "$0") | 105 | E2E_DIR=$(dirname "$0") |
| 106 | . "$E2E_DIR/e2e_lib.sh" | 106 | . "$E2E_DIR/e2e_lib.sh" |
| 107 | 107 | ||
| 108 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_hosts 10_agent 11_select 12_panes 13_birth 14_upgrade' | 108 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_hosts 10_agent 11_select 12_panes 13_birth 14_upgrade 15_askpass' |
| 109 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses | 109 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses |
| 110 | # these by name rather than running them: a group whose fixtures are | 110 | # these by name rather than running them: a group whose fixtures are |
| 111 | # missing does not fail, it passes having tested something else — and a | 111 | # missing does not fail, it passes having tested something else — and a |
| @@ -170,8 +170,8 @@ done | |||
| 170 | # one of those and adds a convergence point would be pinning a fact every | 170 | # one of those and adds a convergence point would be pinning a fact every |
| 171 | # leg above already establishes. | 171 | # leg above already establishes. |
| 172 | 172 | ||
| 173 | [ "$OK_COUNT" = "93" ] || { | 173 | [ "$OK_COUNT" = "100" ] || { |
| 174 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 93 —" | 174 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 100 —" |
| 175 | echo " a scenario was added (update the pin) or silently lost" | 175 | echo " a scenario was added (update the pin) or silently lost" |
| 176 | exit 1 | 176 | exit 1 |
| 177 | } | 177 | } |
test/e2e_15_askpass.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,498 @@ | |||
| 1 | # shellcheck shell=sh | ||
| 2 | # e2e_15_askpass.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in | ||
| 3 | # the order they stand in; see the lib's header for what this file may | ||
| 4 | # assume and what it must register. | ||
| 5 | # | ||
| 6 | # ssh's PROMPTS. A wall dial's ssh reads nothing from /dev/tty: it is spawned | ||
| 7 | # with SSH_ASKPASS_REQUIRE=force and SSH_ASKPASS pointing at this same | ||
| 8 | # binary, which carries the question over a per-client socket to the wall | ||
| 9 | # and the answer back. The two exclusions are asserted here too, because | ||
| 10 | # they are what stops the rule being a nuisance: a POLL spells BatchMode and | ||
| 11 | # is handed no socket at all, and the ENTRY dial (`mux HOST`, before any | ||
| 12 | # wall exists) keeps ssh's own tty prompts. | ||
| 13 | # | ||
| 14 | # Everything below runs under an XDG_RUNTIME_DIR of this group's own. The | ||
| 15 | # prompt socket is named from the client's pid and lives there, so a leg | ||
| 16 | # that used the developer's real runtime dir would both litter it and make | ||
| 17 | # the hygiene assertion at the bottom read somebody else's sockets. | ||
| 18 | ASKRUN="${TMPDIR:-/tmp}/mux-e2e-askrun-$$" | ||
| 19 | defer_rm "$ASKRUN" | ||
| 20 | ASKSHIM="${TMPDIR:-/tmp}/mux-e2e-askshim-$$" | ||
| 21 | defer_rm "$ASKSHIM" | ||
| 22 | # One remote "box" per leg, each a runtime dir of its own — which is the | ||
| 23 | # whole of what makes them different machines, since the shim execs the | ||
| 24 | # remote command locally and `mux d`'s default socket resolves out of that | ||
| 25 | # variable. Sharing one would let the password leg's session decide what | ||
| 26 | # the decline leg's wall is looking at. | ||
| 27 | ASKRUN_PW="${TMPDIR:-/tmp}/mux-e2e-askbox-pw-$$" | ||
| 28 | defer_rm "$ASKRUN_PW" | ||
| 29 | ASKRUN_HK="${TMPDIR:-/tmp}/mux-e2e-askbox-hk-$$" | ||
| 30 | defer_rm "$ASKRUN_HK" | ||
| 31 | ASKRUN_NO="${TMPDIR:-/tmp}/mux-e2e-askbox-no-$$" | ||
| 32 | defer_rm "$ASKRUN_NO" | ||
| 33 | # A config home with no key in it, exported by the shim into every remote | ||
| 34 | # `mux d endpoint`. Without it the first poll's endpoint ANNOUNCES a QUIC | ||
| 35 | # port and the client caches it, and every dial after that is a UDP | ||
| 36 | # handshake with no ssh in it — a box that never asks a question again, | ||
| 37 | # which is the whole subject here. This models the ordinary case anyway: | ||
| 38 | # a machine reached by password is one whose daemon has no key to offer. | ||
| 39 | ASKCFG="${TMPDIR:-/tmp}/mux-e2e-askcfg-$$" | ||
| 40 | defer_rm "$ASKCFG" | ||
| 41 | ASKSOCK_A="$ASKRUN/muxd.sock" | ||
| 42 | ASKSOCK_PW="$ASKRUN_PW/muxd.sock" | ||
| 43 | ASKSOCK_HK="$ASKRUN_HK/muxd.sock" | ||
| 44 | ASKSOCK_NO="$ASKRUN_NO/muxd.sock" | ||
| 45 | defer_sock "$ASKSOCK_A" "$ASKSOCK_PW" "$ASKSOCK_HK" "$ASKSOCK_NO" | ||
| 46 | mkdir -p "$ASKRUN" "$ASKSHIM" "$ASKRUN_PW" "$ASKRUN_HK" "$ASKRUN_NO" "$ASKCFG" | ||
| 47 | # What the shim wrote down, one line per ssh run and one per helper run. | ||
| 48 | # Both are re-pointed per phase, so a leg counts its own runs and not the | ||
| 49 | # whole group's. | ||
| 50 | ASKDIALLOG="$ASKSHIM/dials" | ||
| 51 | ASKPROMPTLOG="$ASKSHIM/prompts" | ||
| 52 | export ASKRUN_PW ASKRUN_HK ASKRUN_NO ASKDIALLOG ASKPROMPTLOG ASKCFG | ||
| 53 | |||
| 54 | # The stand-in ssh. Its own option loop rather than `ssh_shim_head`'s: this | ||
| 55 | # group's whole subject is WHICH runs may prompt, and the shared head | ||
| 56 | # discards the options — a leg that cannot see `-o BatchMode=yes` cannot | ||
| 57 | # tell a poll from a dial, which is exactly the distinction being asserted. | ||
| 58 | # The arity guard below is the head's, verbatim and for its reason: `ssh | ||
| 59 | # HOST` with no command leaves nothing to exec, and a bare exec there exits | ||
| 60 | # 0 — silence shaped like success. | ||
| 61 | # | ||
| 62 | # What it models is real ssh's own contract under SSH_ASKPASS_REQUIRE=force: | ||
| 63 | # the helper is exec'd with the prompt as its ONE argument, its stdout is | ||
| 64 | # the answer, and a non-zero exit is a refused prompt that ends the login. | ||
| 65 | # | ||
| 66 | # And the other half of a real box's behaviour, which is what decides which | ||
| 67 | # tiles can exist here at all: a host that wants a password, or whose key | ||
| 68 | # is not known, REFUSES every BatchMode run. So its `SessionPoll` never | ||
| 69 | # returns a list, no tile is ever born off it, and the only dials that can | ||
| 70 | # raise a prompt are the two a user asked for — a picker birth and the | ||
| 71 | # entry tile's reconnect. A shim that let polls log in would put a tile on | ||
| 72 | # the wall by a road no real box has, and the leg driving it would be | ||
| 73 | # asserting on the fixture. | ||
| 74 | cat > "$ASKSHIM/ssh" <<'SHIM' | ||
| 75 | #!/bin/sh | ||
| 76 | ASKOPTS= | ||
| 77 | while [ $# -gt 0 ]; do | ||
| 78 | case "$1" in | ||
| 79 | -o) ASKOPTS="$ASKOPTS $2"; shift 2 ;; | ||
| 80 | -*) ASKOPTS="$ASKOPTS $1"; shift ;; | ||
| 81 | *) break ;; | ||
| 82 | esac | ||
| 83 | done | ||
| 84 | [ $# -ge 2 ] || exit 97 | ||
| 85 | case "$1" in | ||
| 86 | askbox-pw@*) XDG_RUNTIME_DIR="${ASKRUN_PW:?}" | ||
| 87 | ASKQ="e2e@askbox's password: "; ASKWANT=hunter2 | ||
| 88 | ASKKIND=; ASKDENY="Permission denied (publickey,password)." ;; | ||
| 89 | askbox-hk@*) XDG_RUNTIME_DIR="${ASKRUN_HK:?}" | ||
| 90 | ASKQ="Are you sure you want to continue connecting (yes/no/[fingerprint])? " | ||
| 91 | ASKWANT=yes; ASKKIND=confirm; ASKDENY="Host key verification failed." ;; | ||
| 92 | askbox-no@*) XDG_RUNTIME_DIR="${ASKRUN_NO:?}" | ||
| 93 | ASKQ="e2e@askbox's password: "; ASKWANT=hunter2 | ||
| 94 | ASKKIND=; ASKDENY="Permission denied (publickey,password)." ;; | ||
| 95 | *) exit 96 ;; | ||
| 96 | esac | ||
| 97 | # The remote's key lives under this, and there is none: see ASKCFG. | ||
| 98 | XDG_CONFIG_HOME="${ASKCFG:?}" | ||
| 99 | export XDG_RUNTIME_DIR XDG_CONFIG_HOME | ||
| 100 | unset MUX_KEY_FILE | ||
| 101 | # The host, the options it was given, and whether it was handed a socket to | ||
| 102 | # ask on. Three fields because the three kinds of run are told apart by | ||
| 103 | # exactly this: a poll carries BatchMode and no socket, a wall dial carries | ||
| 104 | # a socket, and the entry dial carries neither. | ||
| 105 | printf '%s|%s|%s\n' "$1" "$ASKOPTS" "${MUX_ASKPASS_SOCK:+socket}" >> "${ASKDIALLOG:?}" | ||
| 106 | shift | ||
| 107 | # Logged first, refused second: the poll legs count runs that HAPPENED, and | ||
| 108 | # a box that refuses them is still a box that was dialled. | ||
| 109 | case "$ASKOPTS" in | ||
| 110 | *BatchMode=yes*) printf '%s\n' "$ASKDENY" >&2; exit 255 ;; | ||
| 111 | esac | ||
| 112 | if [ -n "${MUX_ASKPASS_SOCK:-}" ] && [ "${SSH_ASKPASS_REQUIRE:-}" = force ]; then | ||
| 113 | printf '%s\n' "$ASKQ" >> "${ASKPROMPTLOG:?}" | ||
| 114 | # SSH_ASKPASS_PROMPT is ssh's own word for what it is asking, and the | ||
| 115 | # wall reads it to decide whether to star the answer. Empty for a | ||
| 116 | # secret, which is ssh's own spelling: the variable is simply unset. | ||
| 117 | if [ -n "$ASKKIND" ]; then | ||
| 118 | ASKGOT=$(SSH_ASKPASS_PROMPT="$ASKKIND" "${SSH_ASKPASS:?}" "$ASKQ") | ||
| 119 | else | ||
| 120 | ASKGOT=$("${SSH_ASKPASS:?}" "$ASKQ") | ||
| 121 | fi || { | ||
| 122 | printf '%s\n' "$ASKDENY" >&2 | ||
| 123 | exit 255 | ||
| 124 | } | ||
| 125 | [ "$ASKGOT" = "$ASKWANT" ] || { | ||
| 126 | printf '%s\n' "$ASKDENY" >&2; exit 255; } | ||
| 127 | fi | ||
| 128 | exec /bin/sh -c "$*" | ||
| 129 | SHIM | ||
| 130 | chmod +x "$ASKSHIM/ssh" | ||
| 131 | ASKPATH="$ASKSHIM:$(cd "$(dirname "$MUX")" && pwd):$PATH" | ||
| 132 | |||
| 133 | # The local box the wall is entered on: a `--sock` target spawns no ssh, so | ||
| 134 | # the entry attach is not itself a prompt — the tile under test is the one | ||
| 135 | # the picker births on the machine across the shim. | ||
| 136 | start_daemon "$ASKSOCK_A" "$OUT.ask.a" "askpass: the local daemon never bound" --shell /bin/sh | ||
| 137 | ASKAPID=$DPID | ||
| 138 | |||
| 139 | # askpass_wall STATE HOST — a wall over two daemons: the local one and one | ||
| 140 | # fake box across the shim. No tile is EVER born off the box: it refuses | ||
| 141 | # every BatchMode run, so its poll never returns a list and its picker row | ||
| 142 | # reads `unreachable`. The prompt is raised by the one dial a user asks | ||
| 143 | # for — the picker's Enter on that row — which is the real path. | ||
| 144 | askpass_wall() { | ||
| 145 | mkdir -p "$1/mux" | ||
| 146 | printf -- '--sock %s\n%s\n' "$ASKSOCK_A" "$2" > "$1/mux/hosts" | ||
| 147 | no_saved_tree "$1" | ||
| 148 | } | ||
| 149 | |||
| 150 | # ---- a password prompt is painted, masked, and answered ---------------- | ||
| 151 | # | ||
| 152 | # The claim in full: a picker birth on a box that wants a password puts | ||
| 153 | # ssh's own question on the wall, takes the answer byte by byte behind | ||
| 154 | # stars, and comes up. The oracles are outside the terminal wherever they | ||
| 155 | # can be — the far daemon's own grid says the login really happened — and | ||
| 156 | # the one that must be INSIDE it is the negative: `hunter2` appears nowhere | ||
| 157 | # in the capture, because a capture is a scrollback and a scrollback is a | ||
| 158 | # file. The row the birth is typed at is asserted too: a password box says | ||
| 159 | # `unreachable: Permission denied` on the picker, in ssh's own words. | ||
| 160 | start_daemon "$ASKSOCK_PW" "$OUT.ask.pw" "askpass: the password box never bound" --shell /bin/sh | ||
| 161 | ASKPWPID=$DPID | ||
| 162 | ASKSTATE_PW="${TMPDIR:-/tmp}/mux-e2e-askstate-pw-$$" | ||
| 163 | defer_rm "$ASKSTATE_PW" | ||
| 164 | askpass_wall "$ASKSTATE_PW" "askbox-pw@127.0.0.1" | ||
| 165 | |||
| 166 | # A marker on the local daemon, printed before this wall exists. Waited for | ||
| 167 | # AFTER the prompt is answered, not before it: the box can be on the screen | ||
| 168 | # before the entry tile has painted a single cell — tiles do not paint while | ||
| 169 | # a popup is up — so a wall that waited for the marker first would be | ||
| 170 | # waiting for a paint the popup is holding back. It doubles as the claim | ||
| 171 | # that the rects come back when the box closes. | ||
| 172 | pipe_mux "$OUT.ask.a1" "$OUT.ask.a1.err" env XDG_RUNTIME_DIR="$ASKRUN" \ | ||
| 173 | XDG_STATE_HOME="$ASKSTATE_PW" timeout 40 "$MUX" --sock "$ASKSOCK_A" | ||
| 174 | pipe_send 'printf "ask-home-%%s\\n" mark\n' | ||
| 175 | await_out "$OUT.ask.a1" "ask-home-mark" "askpass: the local daemon's marker never arrived" | ||
| 176 | pipe_detach | ||
| 177 | wait_grid "$ASKSOCK_A" "ask-home-mark" "askpass: the local marker" | ||
| 178 | |||
| 179 | : > "$ASKDIALLOG" | ||
| 180 | : > "$ASKPROMPTLOG" | ||
| 181 | set +e | ||
| 182 | XDG_RUNTIME_DIR="$ASKRUN" XDG_STATE_HOME="$ASKSTATE_PW" PATH="$ASKPATH" \ | ||
| 183 | timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 184 | --out "$OUT.askpw.cap" --err "$OUT.askpw.cap.err" -- \ | ||
| 185 | "$MUX" > "$OUT.askpw.pc" 2>&1 <<EOF | ||
| 186 | expect ask-home-mark 20000 | ||
| 187 | settle 700 20000 | ||
| 188 | send \x1cs | ||
| 189 | expect unreachable: Permission denied 15000 | ||
| 190 | send 2 | ||
| 191 | settle 400 15000 | ||
| 192 | send \r | ||
| 193 | expect askbox's password: 25000 | ||
| 194 | send hunt | ||
| 195 | settle 400 15000 | ||
| 196 | expect > ****_ 15000 | ||
| 197 | send er2 | ||
| 198 | settle 400 15000 | ||
| 199 | expect > *******_ 15000 | ||
| 200 | send \r | ||
| 201 | settle 900 25000 | ||
| 202 | send printf 'ask-pw-%s\n' up\n | ||
| 203 | expect ask-pw-up 25000 | ||
| 204 | send \x1cd | ||
| 205 | waitexit 10000 | ||
| 206 | EOF | ||
| 207 | ASKRC=$? | ||
| 208 | set -e | ||
| 209 | [ "$ASKRC" -eq 0 ] || { | ||
| 210 | echo "e2e FAIL: askpass: the password leg exited $ASKRC:" | ||
| 211 | cat "$OUT.askpw.pc"; echo "--- client stderr ---"; cat "$OUT.askpw.cap.err" | ||
| 212 | echo "--- dials ---"; cat "$ASKDIALLOG" | ||
| 213 | exit 1; } | ||
| 214 | # The login really happened, asked of the far DAEMON's own grid and not of | ||
| 215 | # the screen: a popup that painted and a session nobody reached would | ||
| 216 | # satisfy every assertion above. | ||
| 217 | dump_session "$ASKSOCK_PW" 0 | grep -q 'ask-pw-up' || { | ||
| 218 | echo "e2e FAIL: askpass: the answered login's keystrokes never reached the box:" | ||
| 219 | dump_session "$ASKSOCK_PW" 0; exit 1; } | ||
| 220 | # The one assertion that is about the capture BECAUSE it is a file: a pty | ||
| 221 | # capture is a scrollback, and a password in one is a password on disk. | ||
| 222 | if grep -aqF 'hunter2' "$OUT.askpw.cap"; then | ||
| 223 | echo "e2e FAIL: askpass: the password is in the capture in the clear"; exit 1 | ||
| 224 | fi | ||
| 225 | # ...and the helper really was ssh's, run once per dial rather than by the | ||
| 226 | # wall calling itself. | ||
| 227 | [ "$(grep -c . "$ASKPROMPTLOG")" -ge 1 ] || { | ||
| 228 | echo "e2e FAIL: askpass: ssh never ran the helper:"; cat "$ASKDIALLOG"; exit 1; } | ||
| 229 | ok "a wall dial's password prompt is painted, masked, and answered" | ||
| 230 | |||
| 231 | # ---- a poll carries BatchMode and is handed no socket ------------------- | ||
| 232 | # | ||
| 233 | # The same wall's own log, read after it has gone. The rule is not "polls | ||
| 234 | # happen not to prompt": a poller that could prompt would raise a question | ||
| 235 | # every second per host, on a wall nobody is looking at. It spells | ||
| 236 | # BatchMode, which cannot ask, and it is handed no socket to ask on — and | ||
| 237 | # the run that CAN ask is told apart from it here by both fields at once. | ||
| 238 | # The rule first, so a break in it is named by the message that describes | ||
| 239 | # it rather than by the count below going to zero. | ||
| 240 | if grep -q 'BatchMode=yes.*|socket$' "$ASKDIALLOG"; then | ||
| 241 | echo "e2e FAIL: askpass: a poll was handed a prompt socket:" | ||
| 242 | cat "$ASKDIALLOG"; exit 1 | ||
| 243 | fi | ||
| 244 | # At least one, not several: a host whose poll FAILED is re-asked a tenth | ||
| 245 | # as often on purpose (`client.pollDelayMs` reads the link that answered, | ||
| 246 | # and a refused ssh is a pipe), and this leg is not long enough to buy two. | ||
| 247 | ASKPOLLS=$(grep -c 'BatchMode=yes.*|$' "$ASKDIALLOG" || true) | ||
| 248 | [ "$ASKPOLLS" -ge 1 ] || { | ||
| 249 | echo "e2e FAIL: askpass: the wall never polled the box:" | ||
| 250 | cat "$ASKDIALLOG"; exit 1; } | ||
| 251 | # The dial that DID carry one, so the absence above is a rule and not a leg | ||
| 252 | # in which nothing was ever offered a socket. | ||
| 253 | grep -q '||socket$' "$ASKDIALLOG" || { | ||
| 254 | echo "e2e FAIL: askpass: no dial carried a prompt socket at all:" | ||
| 255 | cat "$ASKDIALLOG"; exit 1; } | ||
| 256 | ok "a poll spells BatchMode and is handed no prompt socket" | ||
| 257 | |||
| 258 | # ---- the wall takes its prompt socket with it --------------------------- | ||
| 259 | # | ||
| 260 | # The socket is named from the client's pid, and a pid comes round again. A | ||
| 261 | # file left behind is one the next client of that pid finds sitting on its | ||
| 262 | # own name. | ||
| 263 | ASKLEFT=$(find "$ASKRUN" -maxdepth 1 -name 'mux-ask-*' | wc -l) | ||
| 264 | [ "$ASKLEFT" -eq 0 ] || { | ||
| 265 | echo "e2e FAIL: askpass: the wall left $ASKLEFT prompt socket(s) behind:" | ||
| 266 | find "$ASKRUN" -maxdepth 1 -name 'mux-ask-*'; exit 1; } | ||
| 267 | ok "the wall takes its prompt socket with it" | ||
| 268 | |||
| 269 | # ---- the host-key question is answered in the CLEAR --------------------- | ||
| 270 | # | ||
| 271 | # The one prompt whose answer the user has to be able to read: they are | ||
| 272 | # being asked to compare a fingerprint, and stars would make the question | ||
| 273 | # unanswerable. Its own box and its own wall — one shim run's question is | ||
| 274 | # fixed by the host word, which is what two machines buy here. | ||
| 275 | start_daemon "$ASKSOCK_HK" "$OUT.ask.hk" "askpass: the host-key box never bound" --shell /bin/sh | ||
| 276 | ASKHKPID=$DPID | ||
| 277 | ASKSTATE_HK="${TMPDIR:-/tmp}/mux-e2e-askstate-hk-$$" | ||
| 278 | defer_rm "$ASKSTATE_HK" | ||
| 279 | askpass_wall "$ASKSTATE_HK" "askbox-hk@127.0.0.1" | ||
| 280 | pipe_mux "$OUT.ask.a2" "$OUT.ask.a2.err" env XDG_RUNTIME_DIR="$ASKRUN" \ | ||
| 281 | XDG_STATE_HOME="$ASKSTATE_HK" timeout 40 "$MUX" --sock "$ASKSOCK_A" | ||
| 282 | pipe_send 'printf "ask-hk-home-%%s\\n" mark\n' | ||
| 283 | await_out "$OUT.ask.a2" "ask-hk-home-mark" "askpass: the host-key leg's marker never arrived" | ||
| 284 | pipe_detach | ||
| 285 | wait_grid "$ASKSOCK_A" "ask-hk-home-mark" "askpass: the host-key leg's marker" | ||
| 286 | |||
| 287 | set +e | ||
| 288 | XDG_RUNTIME_DIR="$ASKRUN" XDG_STATE_HOME="$ASKSTATE_HK" PATH="$ASKPATH" \ | ||
| 289 | timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 290 | --out "$OUT.askhk.cap" --err "$OUT.askhk.cap.err" -- \ | ||
| 291 | "$MUX" > "$OUT.askhk.pc" 2>&1 <<EOF | ||
| 292 | expect ask-hk-home-mark 20000 | ||
| 293 | settle 700 20000 | ||
| 294 | send \x1cs | ||
| 295 | expect unreachable: Host key verification 15000 | ||
| 296 | send 2 | ||
| 297 | settle 400 15000 | ||
| 298 | send \r | ||
| 299 | expect continue connecting 25000 | ||
| 300 | send yes | ||
| 301 | settle 400 15000 | ||
| 302 | expect > yes_ 15000 | ||
| 303 | send \r | ||
| 304 | settle 900 25000 | ||
| 305 | send printf 'ask-hk-%s\n' up\n | ||
| 306 | expect ask-hk-up 25000 | ||
| 307 | send \x1cd | ||
| 308 | waitexit 10000 | ||
| 309 | EOF | ||
| 310 | ASKRC=$? | ||
| 311 | set -e | ||
| 312 | [ "$ASKRC" -eq 0 ] || { | ||
| 313 | echo "e2e FAIL: askpass: the host-key leg exited $ASKRC:" | ||
| 314 | cat "$OUT.askhk.pc"; echo "--- client stderr ---"; cat "$OUT.askhk.cap.err" | ||
| 315 | echo "--- dials ---"; cat "$ASKDIALLOG"; exit 1; } | ||
| 316 | dump_session "$ASKSOCK_HK" 0 | grep -q 'ask-hk-up' || { | ||
| 317 | echo "e2e FAIL: askpass: the host-key answer did not open the session:" | ||
| 318 | dump_session "$ASKSOCK_HK" 0; exit 1; } | ||
| 319 | ok "the host-key question is answered in the clear, not behind stars" | ||
| 320 | |||
| 321 | # ---- Esc on a prompt ends the tile, and the redialling stops ------------ | ||
| 322 | # | ||
| 323 | # Without this the user who declined is asked the same question every two | ||
| 324 | # seconds for as long as the wall is up — the redial loop arguing with an | ||
| 325 | # answer it already has. The oracle is the shim's own log: after the Esc, | ||
| 326 | # five seconds buys the box NO further dial that carried a socket. The | ||
| 327 | # polls go on, and are counted separately, because a poll is not the thing | ||
| 328 | # that was refused. | ||
| 329 | start_daemon "$ASKSOCK_NO" "$OUT.ask.no" "askpass: the decline box never bound" --shell /bin/sh | ||
| 330 | ASKNOPID=$DPID | ||
| 331 | ASKSTATE_NO="${TMPDIR:-/tmp}/mux-e2e-askstate-no-$$" | ||
| 332 | defer_rm "$ASKSTATE_NO" | ||
| 333 | askpass_wall "$ASKSTATE_NO" "askbox-no@127.0.0.1" | ||
| 334 | pipe_mux "$OUT.ask.a3" "$OUT.ask.a3.err" env XDG_RUNTIME_DIR="$ASKRUN" \ | ||
| 335 | XDG_STATE_HOME="$ASKSTATE_NO" timeout 40 "$MUX" --sock "$ASKSOCK_A" | ||
| 336 | pipe_send 'printf "ask-no-home-%%s\\n" mark\n' | ||
| 337 | await_out "$OUT.ask.a3" "ask-no-home-mark" "askpass: the decline leg's marker never arrived" | ||
| 338 | pipe_detach | ||
| 339 | wait_grid "$ASKSOCK_A" "ask-no-home-mark" "askpass: the decline leg's marker" | ||
| 340 | |||
| 341 | : > "$ASKDIALLOG" | ||
| 342 | : > "$ASKPROMPTLOG" | ||
| 343 | set +e | ||
| 344 | XDG_RUNTIME_DIR="$ASKRUN" XDG_STATE_HOME="$ASKSTATE_NO" PATH="$ASKPATH" \ | ||
| 345 | timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 346 | --out "$OUT.askno.cap" --err "$OUT.askno.cap.err" -- \ | ||
| 347 | "$MUX" > "$OUT.askno.pc" 2>&1 <<EOF | ||
| 348 | expect ask-no-home-mark 20000 | ||
| 349 | settle 700 20000 | ||
| 350 | send \x1cs | ||
| 351 | expect unreachable: Permission denied 15000 | ||
| 352 | send 2 | ||
| 353 | settle 400 15000 | ||
| 354 | send \r | ||
| 355 | expect askbox's password: 25000 | ||
| 356 | send \x1b | ||
| 357 | expect prompt declined 25000 | ||
| 358 | settle 5000 20000 | ||
| 359 | send printf 'ask-no-%s\n' back\n | ||
| 360 | expect ask-no-back 20000 | ||
| 361 | send \x1cd | ||
| 362 | waitexit 10000 | ||
| 363 | EOF | ||
| 364 | ASKRC=$? | ||
| 365 | set -e | ||
| 366 | [ "$ASKRC" -eq 0 ] || { | ||
| 367 | echo "e2e FAIL: askpass: the decline leg exited $ASKRC:" | ||
| 368 | cat "$OUT.askno.pc"; echo "--- client stderr ---"; cat "$OUT.askno.cap.err" | ||
| 369 | echo "--- dials ---"; cat "$ASKDIALLOG"; exit 1; } | ||
| 370 | # ONE prompt, and one only. A second says the tile redialled: five seconds | ||
| 371 | # at the 2s backoff cap is two more goes at least. | ||
| 372 | ASKNOPROMPTS=$(grep -c . "$ASKPROMPTLOG" || true) | ||
| 373 | [ "$ASKNOPROMPTS" -eq 1 ] || { | ||
| 374 | echo "e2e FAIL: askpass: the declined box was asked $ASKNOPROMPTS times, want 1:" | ||
| 375 | cat "$ASKDIALLOG"; exit 1; } | ||
| 376 | # ...and the dial that carried a socket happened once, which is what makes | ||
| 377 | # the count above a stopped loop rather than a helper that stopped running. | ||
| 378 | ASKNODIALS=$(grep -c '||socket$' "$ASKDIALLOG" || true) | ||
| 379 | [ "$ASKNODIALS" -eq 1 ] || { | ||
| 380 | echo "e2e FAIL: askpass: the declined box was dialled $ASKNODIALS times, want 1:" | ||
| 381 | cat "$ASKDIALLOG"; exit 1; } | ||
| 382 | # The tile LEFT, and the wall said why. A picker-born tile is focused and | ||
| 383 | # keeps the wall, so its ending goes through `endAction`'s vanish arm: the | ||
| 384 | # focus falls back to the tile the birth was typed from, and the sentence | ||
| 385 | # is printed on the way out. Both are asserted where the terminal cannot | ||
| 386 | # flatter them — the keys after the decline are looked for in the LOCAL | ||
| 387 | # daemon's own grid, and the sentence in the client's stderr file. | ||
| 388 | dump_session "$ASKSOCK_A" 0 | grep -q 'ask-no-back' || { | ||
| 389 | echo "e2e FAIL: askpass: the focus did not fall back to the tile the birth came from:" | ||
| 390 | dump_session "$ASKSOCK_A" 0; exit 1; } | ||
| 391 | grep -qF 'mux: prompt declined' "$OUT.askno.cap.err" || { | ||
| 392 | echo "e2e FAIL: askpass: the wall did not say why the tile left:" | ||
| 393 | cat "$OUT.askno.cap.err"; exit 1; } | ||
| 394 | ok "an Esc on a prompt ends the tile, says why, and stops the redialling" | ||
| 395 | |||
| 396 | # ---- the entry dial keeps ssh's own prompts ----------------------------- | ||
| 397 | # | ||
| 398 | # `mux HOST` is a person at a bare prompt with a /dev/tty right there, and | ||
| 399 | # no wall exists yet to paint a popup on. It must therefore be handed no | ||
| 400 | # socket at all — asserted on the shim's env dump, because "it did not | ||
| 401 | # prompt" is also what a leg that never dialled looks like. | ||
| 402 | # | ||
| 403 | # On a PTY, deliberately, though the assertion would be easier on a pipe: a | ||
| 404 | # piped client starts no listener at all, so a pipe would satisfy this leg | ||
| 405 | # with the mechanism switched off. Here the wall behind the entry tile has | ||
| 406 | # a live listener the whole time, and the claim is that the FIRST attach | ||
| 407 | # still never reaches it. | ||
| 408 | : > "$ASKDIALLOG" | ||
| 409 | : > "$ASKPROMPTLOG" | ||
| 410 | set +e | ||
| 411 | XDG_RUNTIME_DIR="$ASKRUN" XDG_STATE_HOME="$ASKSTATE_PW" PATH="$ASKPATH" \ | ||
| 412 | timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 413 | --out "$OUT.askentry.cap" --err "$OUT.askentry.cap.err" -- \ | ||
| 414 | "$MUX" askbox-pw@127.0.0.1 > "$OUT.askentry.pc" 2>&1 <<EOF | ||
| 415 | settle 1200 25000 | ||
| 416 | send printf 'ask-entry-%s\n' mark\n | ||
| 417 | expect ask-entry-mark 20000 | ||
| 418 | send \x1cd | ||
| 419 | waitexit 10000 | ||
| 420 | EOF | ||
| 421 | ASKRC=$? | ||
| 422 | set -e | ||
| 423 | [ "$ASKRC" -eq 0 ] || { | ||
| 424 | echo "e2e FAIL: askpass: the entry-dial leg exited $ASKRC:" | ||
| 425 | cat "$OUT.askentry.pc"; echo "--- client stderr ---"; cat "$OUT.askentry.cap.err" | ||
| 426 | echo "--- dials ---"; cat "$ASKDIALLOG"; exit 1; } | ||
| 427 | # The entry dial's own line: no options (it spells the asking word, which | ||
| 428 | # carries no `-o`) and no socket. The wall's dial is the same shape but for | ||
| 429 | # the third field, which is the whole distinction. | ||
| 430 | grep -q '^askbox-pw@127.0.0.1||$' "$ASKDIALLOG" || { | ||
| 431 | echo "e2e FAIL: askpass: the entry dial's ssh did not run without a prompt socket:" | ||
| 432 | cat "$ASKDIALLOG"; exit 1; } | ||
| 433 | # ...and NOTHING on this wall asked a question, which is the half that | ||
| 434 | # catches an entry tile re-dialling through the armed path instead of | ||
| 435 | # adopting the link its attach already opened. | ||
| 436 | [ "$(grep -c . "$ASKPROMPTLOG")" -eq 0 ] || { | ||
| 437 | echo "e2e FAIL: askpass: the entry dial raised a prompt:"; cat "$ASKPROMPTLOG"; exit 1; } | ||
| 438 | ok "the entry dial is handed no prompt socket and keeps ssh's own" | ||
| 439 | |||
| 440 | # ---- no runtime dir, no popup: ssh keeps its own prompts ---------------- | ||
| 441 | # | ||
| 442 | # The refusal that has no other witness. `$XDG_RUNTIME_DIR` is where the | ||
| 443 | # prompt socket goes and mux takes that directory as found; with none | ||
| 444 | # there is nowhere private to put a socket, and a guess would land in a | ||
| 445 | # shared /tmp — where a password prompt is one any local user could answer. | ||
| 446 | # So the wall starts no listener at all and ssh keeps its own prompts, | ||
| 447 | # which is the OLD behaviour and a visible one. | ||
| 448 | # | ||
| 449 | # A comment cannot fail. Without this leg a future fallback to /tmp would | ||
| 450 | # pass every other check in this file green. | ||
| 451 | : > "$ASKDIALLOG" | ||
| 452 | : > "$ASKPROMPTLOG" | ||
| 453 | set +e | ||
| 454 | env -u XDG_RUNTIME_DIR XDG_STATE_HOME="$ASKSTATE_PW" PATH="$ASKPATH" \ | ||
| 455 | timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 456 | --out "$OUT.asknort.cap" --err "$OUT.asknort.cap.err" -- \ | ||
| 457 | "$MUX" --sock "$ASKSOCK_A" > "$OUT.asknort.pc" 2>&1 <<EOF | ||
| 458 | expect ask-home-mark 20000 | ||
| 459 | settle 700 20000 | ||
| 460 | send \x1cs | ||
| 461 | expect unreachable: 15000 | ||
| 462 | send 2 | ||
| 463 | settle 400 15000 | ||
| 464 | send \r | ||
| 465 | settle 2500 20000 | ||
| 466 | send \x1cd | ||
| 467 | waitexit 10000 | ||
| 468 | EOF | ||
| 469 | ASKRC=$? | ||
| 470 | set -e | ||
| 471 | [ "$ASKRC" -eq 0 ] || { | ||
| 472 | echo "e2e FAIL: askpass: the no-runtime-dir leg exited $ASKRC:" | ||
| 473 | cat "$OUT.asknort.pc"; echo "--- client stderr ---"; cat "$OUT.asknort.cap.err" | ||
| 474 | echo "--- dials ---"; cat "$ASKDIALLOG"; exit 1; } | ||
| 475 | # The birth DID dial — otherwise the absence below is a leg that never ran. | ||
| 476 | grep -q '^askbox-pw@127.0.0.1||' "$ASKDIALLOG" || { | ||
| 477 | echo "e2e FAIL: askpass: the picker birth never dialled without a runtime dir:" | ||
| 478 | cat "$ASKDIALLOG"; exit 1; } | ||
| 479 | # ...and no run of it carried a socket, which is what says no listener was | ||
| 480 | # ever started. | ||
| 481 | if grep -q 'socket$' "$ASKDIALLOG"; then | ||
| 482 | echo "e2e FAIL: askpass: a wall with no runtime dir armed a prompt socket:" | ||
| 483 | cat "$ASKDIALLOG"; exit 1 | ||
| 484 | fi | ||
| 485 | [ "$(grep -c . "$ASKPROMPTLOG")" -eq 0 ] || { | ||
| 486 | echo "e2e FAIL: askpass: a wall with no runtime dir raised a prompt:" | ||
| 487 | cat "$ASKPROMPTLOG"; exit 1; } | ||
| 488 | ok "a wall with no runtime dir starts no listener and ssh keeps its own prompts" | ||
| 489 | |||
| 490 | assert_stopped "$ASKSOCK_A" "$ASKAPID" "askpass local" "$OUT.ask.a.stop" | ||
| 491 | ASKAPID="" | ||
| 492 | assert_stopped "$ASKSOCK_PW" "$ASKPWPID" "askpass password box" "$OUT.ask.pw.stop" | ||
| 493 | ASKPWPID="" | ||
| 494 | assert_stopped "$ASKSOCK_HK" "$ASKHKPID" "askpass host-key box" "$OUT.ask.hk.stop" | ||
| 495 | ASKHKPID="" | ||
| 496 | assert_stopped "$ASKSOCK_NO" "$ASKNOPID" "askpass decline box" "$OUT.ask.no.stop" | ||
| 497 | ASKNOPID="" | ||
| 498 | rm -rf "$ASKSHIM" "$ASKRUN" "$ASKRUN_PW" "$ASKRUN_HK" "$ASKRUN_NO" | ||