fix: a wrong password no longer costs an entire reinstall
The endpoint credentials are asked for at the very end of a run, after every package is installed -- correct, because a password typed at picker time would sit in memory through ten minutes of downloads. But any non-200 was fatal, so one mistyped character meant repeating the whole install to get back to a three-line prompt. Now it retries, up to five attempts. URL and username persist across them and blank keeps them, shown as `Bootstrap URL [https://...]:`, so only the password is retyped -- retyping an address that was already pasted correctly is its own source of error. `q` at the URL prompt leaves the machine public-only. The message also names the fault. Every failure used to print "endpoint refused the credentials", including a 404 and an unreachable host -- which is precisely how a URL-shape bug reads as a password problem. The status is now captured alongside the body rather than relying on --fail: 401 wrong username or password 404 reached the host, but no bootstrap.env is there -- check the route 000 could not reach that address (DNS, TLS, or the host is down) The body is used only when the code is 200, so an error page is still never parsed as a blob; that was --fail's job and an explicit check is stronger. `dotup private` already re-ran only this step, leaving installed packages alone -- it was simply missing from --help, so the cheap way back in was undiscoverable. Documented, and the failure path now points at it. The end-to-end sends a wrong password first on purpose and asserts both the message and that blank-blank-correct works, which is the only way to test a recovery path. Note that harness clones the PUBLISHED repo, so it validates what a user gets, not the working tree -- these invariants are covered in the unit suite, which reads the tree directly. 105 -> 113.
This commit is contained in:
@@ -92,12 +92,30 @@ proc wait_for {pat what} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# dotup's own three: plain `read`, no TUI, so the text is the whole signal
|
# dotup's own three: plain `read`, no TUI, so the text is the whole signal.
|
||||||
|
#
|
||||||
|
# The FIRST password sent here is deliberately wrong. These credentials are
|
||||||
|
# asked for at the very end of a run, so a typo used to be fatal -- one wrong
|
||||||
|
# character and the whole install had to be repeated to get back to this
|
||||||
|
# prompt. Getting it wrong on purpose is the only way to prove the retry loop
|
||||||
|
# exists and that the URL and username survive the attempt.
|
||||||
wait_for {Bootstrap URL:} "the bootstrap URL prompt"
|
wait_for {Bootstrap URL:} "the bootstrap URL prompt"
|
||||||
send -- "$env(BOOT_URL)\r"
|
send -- "$env(BOOT_URL)\r"
|
||||||
wait_for {Username:} "the username prompt"
|
wait_for {Username:} "the username prompt"
|
||||||
send -- "$env(BOOT_USER)\r"
|
send -- "$env(BOOT_USER)\r"
|
||||||
wait_for {Password:} "the password prompt"
|
wait_for {Password:} "the password prompt"
|
||||||
|
send -- "definitely-not-the-password\r"
|
||||||
|
|
||||||
|
# The message must name the actual fault. "endpoint refused the credentials"
|
||||||
|
# was once emitted for a 401, a 404 and an unreachable host alike.
|
||||||
|
wait_for {wrong username or password} "the wrong-password message"
|
||||||
|
# Blank keeps the URL and the username, so only the password is retyped. The
|
||||||
|
# bracketed default in the prompt is what proves they were retained.
|
||||||
|
wait_for {Bootstrap URL \[} "the retry prompt, with the URL kept"
|
||||||
|
send -- "\r"
|
||||||
|
wait_for {Username \[} "the retry username prompt, with the user kept"
|
||||||
|
send -- "\r"
|
||||||
|
wait_for {Password:} "the retry password prompt"
|
||||||
send -- "$env(BOOT_PW)\r"
|
send -- "$env(BOOT_PW)\r"
|
||||||
# The private repo's seven, each answered only once its TUI is genuinely ready
|
# The private repo's seven, each answered only once its TUI is genuinely ready
|
||||||
# to read.
|
# to read.
|
||||||
|
|||||||
@@ -191,6 +191,29 @@ is "PRIV_CFG is not the default config path" "0" \
|
|||||||
is "the private source is locked down after clone" "1" \
|
is "the private source is locked down after clone" "1" \
|
||||||
"$(grep -c 'chmod -R go-rwx "\$PRIV_SRC"' $D)"
|
"$(grep -c 'chmod -R go-rwx "\$PRIV_SRC"' $D)"
|
||||||
|
|
||||||
|
printf '\n\033[1ma wrong password is not a reinstall\033[0m\n'
|
||||||
|
# The endpoint credentials are asked for at the very END of a run, after every
|
||||||
|
# package is installed. Any non-200 used to be fatal, so one mistyped character
|
||||||
|
# meant repeating the whole install to get back to the prompt.
|
||||||
|
is "the prompt retries instead of returning" "0" \
|
||||||
|
"$(grep -c 'endpoint refused the credentials' $D)"
|
||||||
|
is "…up to a bounded number of attempts" "1" \
|
||||||
|
"$(grep -c '"$p_try" -gt 5' $D)"
|
||||||
|
is "…and points at the cheap way back in" "1" \
|
||||||
|
"$(grep -c 'Re-run only this step: dotup private' $D)"
|
||||||
|
# One message for 401, 404 and an unreachable host is how a URL-shape bug spends
|
||||||
|
# an evening looking like a password problem. Each needs a different next move.
|
||||||
|
is "a 401 names the password" "1" "$(grep -c 'wrong username or password' $D)"
|
||||||
|
is "a 404 names the route" "1" "$(grep -c 'no bootstrap.env is there' $D)"
|
||||||
|
is "an unreachable host says so" "1" "$(grep -c 'could not reach that address' $D)"
|
||||||
|
# --fail is gone, so the code must be checked explicitly or an error page would
|
||||||
|
# be parsed as the credential blob.
|
||||||
|
is "the blob is used only on 200" "1" "$(grep -cF '200) break ;;' $D)"
|
||||||
|
# The subcommand existed but was absent from --help, so there was no way to
|
||||||
|
# discover the recovery path.
|
||||||
|
is "dotup private is documented" "1" \
|
||||||
|
"$(sh $D --help 2>&1 | grep -c 'ONLY the private tier')"
|
||||||
|
|
||||||
printf '\n\033[1mrisk model — the invariants that matter\033[0m\n'
|
printf '\n\033[1mrisk model — the invariants that matter\033[0m\n'
|
||||||
reset
|
reset
|
||||||
is "defaults tick nothing invasive" "" "$(picked invasive)"
|
is "defaults tick nothing invasive" "" "$(picked invasive)"
|
||||||
|
|||||||
@@ -781,30 +781,80 @@ cmd_private() {
|
|||||||
head_ "private tier"
|
head_ "private tier"
|
||||||
say " The address is in no repository. Leave it blank to stay public-only."
|
say " The address is in no repository. Leave it blank to stay public-only."
|
||||||
# Read into variables: nothing reaches argv, so nothing reaches `ps`.
|
# Read into variables: nothing reaches argv, so nothing reaches `ps`.
|
||||||
P_URL=''; P_USER=''; P_PW=''
|
P_URL=''; P_USER=''; P_PW=''; p_in=''
|
||||||
printf ' Bootstrap URL: ' >&2; IFS= read -r P_URL || :
|
|
||||||
[ -n "$P_URL" ] || { say " public-only machine. Nothing was asked for."; return 0; }
|
|
||||||
# Accept the directory OR the full file URL, because both are in circulation:
|
|
||||||
# the rotation scripts print the file form and say to store THAT in Bitwarden,
|
|
||||||
# so pasting what you saved is the likely case. Without this, the request
|
|
||||||
# becomes .../bootstrap.env/bootstrap.env, the 404 trips curl --fail, and the
|
|
||||||
# error below blames the credentials for what is actually a URL shape.
|
|
||||||
P_URL=${P_URL%/}
|
|
||||||
P_URL=${P_URL%/bootstrap.env}
|
|
||||||
printf ' Username: ' >&2; IFS= read -r P_USER || :
|
|
||||||
printf ' Password: ' >&2
|
|
||||||
stty -echo 2>/dev/null || :; IFS= read -r P_PW || :; stty echo 2>/dev/null || :; printf '\n' >&2
|
|
||||||
# shellcheck disable=SC2064
|
# shellcheck disable=SC2064
|
||||||
trap 'unset P_URL P_USER P_PW P_BLOB p_repo p_tok p_cred 2>/dev/null || :' EXIT INT TERM
|
trap 'unset P_URL P_USER P_PW P_BLOB p_repo p_tok p_cred 2>/dev/null || :' EXIT INT TERM
|
||||||
|
|
||||||
# curl -K - reads its config, credentials included, from stdin rather than
|
# Ask, and keep asking.
|
||||||
# the command line. --fail matters too: without it a 401 body is parsed as
|
#
|
||||||
# if it were the blob.
|
# These three questions come at the very END of a run, after every package is
|
||||||
P_BLOB=$(printf 'user = "%s:%s"\nsilent\nfail\n' "$P_USER" "$P_PW" \
|
# installed, because a password typed at picker time would sit in memory
|
||||||
| curl -K - "${P_URL%/}/bootstrap.env" 2>/dev/null) || {
|
# through ten minutes of downloads. That ordering is right, and it is exactly
|
||||||
err "endpoint refused the credentials. The machine stays public-only."
|
# what made one mistyped character so expensive: any non-200 used to be fatal,
|
||||||
unset P_PW; return 1; }
|
# so a typo meant re-running the entire install to get back to this prompt.
|
||||||
unset P_PW # spent. only the fetched credentials exist now.
|
# Nothing about a wrong password justifies reinstalling a compiler.
|
||||||
|
#
|
||||||
|
# URL and username persist across attempts and blank keeps them, because the
|
||||||
|
# password is the thing you get wrong, and retyping an address you already
|
||||||
|
# pasted correctly is its own source of error.
|
||||||
|
p_try=0
|
||||||
|
while :; do
|
||||||
|
p_try=$((p_try + 1))
|
||||||
|
if [ "$p_try" -gt 5 ]; then
|
||||||
|
err "five failed attempts — stopping rather than looping."
|
||||||
|
say " Nothing else needs redoing. Re-run only this step: dotup private"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$P_URL" ]; then printf ' Bootstrap URL [%s]: ' "$P_URL" >&2
|
||||||
|
else printf ' Bootstrap URL: ' >&2; fi
|
||||||
|
IFS= read -r p_in || :
|
||||||
|
case $p_in in
|
||||||
|
q|Q) say " public-only machine. Nothing was asked for."; return 0 ;;
|
||||||
|
'') [ -n "$P_URL" ] || { say " public-only machine. Nothing was asked for."; return 0; } ;;
|
||||||
|
*) P_URL=$p_in ;;
|
||||||
|
esac
|
||||||
|
# Accept the directory OR the full file URL, because both are in
|
||||||
|
# circulation: the rotation scripts print the file form and say to store
|
||||||
|
# THAT in Bitwarden, so pasting what you saved is the likely case. Without
|
||||||
|
# this the request becomes .../bootstrap.env/bootstrap.env, and the 404
|
||||||
|
# would be reported below as a credential problem.
|
||||||
|
P_URL=${P_URL%/}
|
||||||
|
P_URL=${P_URL%/bootstrap.env}
|
||||||
|
|
||||||
|
if [ -n "$P_USER" ]; then printf ' Username [%s]: ' "$P_USER" >&2
|
||||||
|
else printf ' Username: ' >&2; fi
|
||||||
|
IFS= read -r p_in || :
|
||||||
|
[ -z "$p_in" ] || P_USER=$p_in
|
||||||
|
|
||||||
|
printf ' Password: ' >&2
|
||||||
|
stty -echo 2>/dev/null || :; IFS= read -r P_PW || :; stty echo 2>/dev/null || :; printf '\n' >&2
|
||||||
|
|
||||||
|
# curl -K - reads its config, credentials included, from stdin rather than
|
||||||
|
# the command line, so nothing reaches `ps`. The status is captured
|
||||||
|
# alongside the body instead of relying on --fail, because "it failed" is
|
||||||
|
# not a useful thing to say here: 401, 404 and an unreachable host each
|
||||||
|
# need a different next move, and the old single message blamed the
|
||||||
|
# credentials for all three. The body is used only when the code is 200,
|
||||||
|
# so an error page is still never parsed as a blob.
|
||||||
|
p_resp=$(printf 'user = "%s:%s"\nsilent\nwrite-out = "\\n%%{http_code}"\n' "$P_USER" "$P_PW" \
|
||||||
|
| curl -K - --connect-timeout 15 -m 120 "$P_URL/bootstrap.env" 2>/dev/null) || :
|
||||||
|
p_code=$(printf '%s\n' "$p_resp" | tail -n1)
|
||||||
|
P_BLOB=$(printf '%s\n' "$p_resp" | sed '$d')
|
||||||
|
p_resp=''
|
||||||
|
P_PW=''
|
||||||
|
|
||||||
|
case $p_code in
|
||||||
|
200) break ;;
|
||||||
|
401) err "wrong username or password." ;;
|
||||||
|
404) err "reached the host, but no bootstrap.env is there — check the route in the URL." ;;
|
||||||
|
000) err "could not reach that address (DNS, TLS, or the host is down)." ;;
|
||||||
|
*) err "the endpoint answered HTTP $p_code." ;;
|
||||||
|
esac
|
||||||
|
P_BLOB=''
|
||||||
|
say " Try again, or type q at the URL prompt to stay public-only."
|
||||||
|
done
|
||||||
|
unset P_PW p_in p_code p_try 2>/dev/null || :
|
||||||
|
|
||||||
# Contract with the endpoint (phase 4 writes the file this parses):
|
# Contract with the endpoint (phase 4 writes the file this parses):
|
||||||
# two KEY=VALUE lines, no quoting, no shell
|
# two KEY=VALUE lines, no quoting, no shell
|
||||||
@@ -1016,6 +1066,10 @@ usage() {
|
|||||||
--print, -n resolve everything and print the commands, install nothing
|
--print, -n resolve everything and print the commands, install nothing
|
||||||
--yes, -y skip the confirmation after the picker
|
--yes, -y skip the confirmation after the picker
|
||||||
|
|
||||||
|
private ONLY the private tier: prompt for the endpoint and fetch.
|
||||||
|
Everything already installed is left alone, so this is the
|
||||||
|
way back in after a mistyped password -- no reinstall.
|
||||||
|
|
||||||
plumbing, called by the fzf bindings:
|
plumbing, called by the fzf bindings:
|
||||||
render toggle expand expand-all preset explain plan preflight
|
render toggle expand expand-all preset explain plan preflight
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
Reference in New Issue
Block a user