a73x

90af5cbb

test: ssh's prompts, both exclusions, and the socket's hygiene

a73x   2026-08-30 10:59

Commit message
test: ssh's prompts, both exclusions, and the socket's hygiene

A new group and its own stand-in ssh — one that honours
SSH_ASKPASS_REQUIRE=force the way the real thing does: the helper gets the
prompt as its ONE argument, its stdout is the answer, a non-zero exit ends
the login. Its own option loop rather than the shared head, because this
group's whole subject is WHICH runs may prompt and the shared head throws
the options away.

Six scenarios: a password answered behind stars (with `hunter2` asserted
absent from the capture, which is a file), a host key answered in the
clear, an Esc that ends the tile and buys five seconds of no further dial,
a poll that carries BatchMode and no socket, an entry dial that carries
neither, and a runtime dir with no socket left in it.

The remote is given a config home with no key, so `mux d endpoint`
announces nothing and the ssh pipe IS the session. Without it the first
poll caches a QUIC endpoint and every dial after that is a UDP handshake
with no ssh in it — a box that never asks a question again, which is the
whole subject. It also models the ordinary case: a machine reached by
password is one whose daemon has no key to offer.

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"