From 4d3c85fd0600988da8ab2a789ca41e059bf6588e Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Wed, 23 Sep 2026 19:45:26 +0800 Subject: [PATCH] fix(bootstrap): [smtp] carry stops hoarding the auth_source comment block (#50) --- deploy/bootstrap.sh | 40 ++++++++++++--------- deploy/bootstrap_test.sh | 77 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/deploy/bootstrap.sh b/deploy/bootstrap.sh index 802590a..15c1be4 100644 --- a/deploy/bootstrap.sh +++ b/deploy/bootstrap.sh @@ -2087,24 +2087,30 @@ EOF # family as the root_domain loss fixed in ecbeb20 -- generated file, hand-set # value, no carry-forward. # -# Cached on first call because write_felis_toml clobbers felis.host.toml before -# it is called again for felis.pod.toml: by then the file this would read from -# no longer has the block. The pod toml is the fallback for exactly that window. +# The carry is the section's header and key lines only. Printing every line up to +# the next section header hoarded the generated [[auth_source]] comment block +# that sits below [smtp] into this carry: each re-run then re-emitted the hoard +# plus a fresh template copy, growing both config files by one comment block per +# run (audit #50). The extraction is idempotent, which is also why re-reading the +# freshly rewritten host file on the pod pass is safe. The pod toml remains the +# fallback for a host file with no [smtp] section at all. persisted_smtp_block() { - if [ -z "${SMTP_BLOCK_CACHED:-}" ]; then - SMTP_BLOCK_CACHED=1 - SMTP_BLOCK="" - local f - for f in "${STATE_DIR}/felis.host.toml" "${STATE_DIR}/felis.pod.toml"; do - [ -r "$f" ] || continue - # Print from [smtp] up to (not including) the next section header. - SMTP_BLOCK="$(awk '/^[[:space:]]*\[smtp\]/ { f=1 } - f && /^[[:space:]]*\[/ && !/^[[:space:]]*\[smtp\]/ { exit } - f { print }' "$f")" - [ -n "$SMTP_BLOCK" ] && break - done - fi - printf '%s' "$SMTP_BLOCK" + local f out + for f in "${STATE_DIR}/felis.host.toml" "${STATE_DIR}/felis.pod.toml"; do + [ -r "$f" ] || continue + out="$(awk ' + /^[[:space:]]*\[/ { + if (insmtp) exit + insmtp = ($0 ~ /^[[:space:]]*\[smtp\][[:space:]]*$/) + if (insmtp) print + next + } + insmtp && /^[[:space:]]*("[A-Za-z_][A-Za-z0-9_]*"|[A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=/ { print } + ' "$f")" + [ -n "$out" ] || continue + printf '%s' "$out" + return 0 + done } # persisted_auth_source_blocks echoes the [[auth_source]] tables an earlier run left diff --git a/deploy/bootstrap_test.sh b/deploy/bootstrap_test.sh index 456c0ce..b2157e4 100644 --- a/deploy/bootstrap_test.sh +++ b/deploy/bootstrap_test.sh @@ -192,6 +192,83 @@ for hdr in '[[ auth_source ]]' '[["auth_source"]]' "[['auth_source']]"; do esac done +# --- [smtp] carry-forward does not hoard the auth_source comment block ------------------- +# persisted_smtp_block used to print every line between [smtp] and the next section +# header -- which includes the generated Yggdrasil comment block that sits above +# [[auth_source]]. Each re-run re-emitted that hoard plus a fresh template copy, so both +# config files grew by one comment block per run (audit #50). The carry must be the +# section's header and keys only, and must be byte-stable when written back. + +sblock="$(awk '/^persisted_smtp_block\(\) \{/,/^}/' "$BS")" +[ -n "$sblock" ] || { echo "FAIL: no persisted_smtp_block found in $BS"; exit 1; } +[ "$(printf '%s\n' "$sblock" | wc -l)" -lt 40 ] \ + || { echo "FAIL: the extracted block is not the function -- did its closing brace move?"; exit 1; } + +sfn="$(mktemp)" +printf '%s\n' "$sblock" > "$sfn" +smtp_dir="$(mktemp -d)" +trap 'rm -f "$jar" "$sfn"; rm -rf "$vdir" "$sdir" "$smtp_dir"' EXIT + +run_smtp() { # state-dir + STATE_DIR="$1" SBLOCK_FILE="$sfn" bash -c '. "$SBLOCK_FILE"; persisted_smtp_block' +} + +cat > "$smtp_dir/felis.host.toml" <<'TOML' +[server] +listen = "0.0.0.0:8080" + +[smtp] + host = "mail.example" + port = 587 + from = "felis@example.net" + username = "relay-user" + password_ref = "smtp-password" + +# Third-party Yggdrasil sources federated by the hasJoined multiplexer. Mojang is +# always the code-owned identity anchor (premium-first), prepended in Go; sources here +# append as namespace-rewritten guests. A fresh install federates LittleSkin. Edit the +# list in /etc/felis/felis.host.toml and rerun the installer; re-runs keep it as it +# is, and with no [[auth_source]] at all the server is Mojang-only. + +[[auth_source]] + tag = "littleskin" + prefix = "LS" + url = "https://littleskin.cn/api/yggdrasil/sessionserver/session/minecraft/hasJoined" +TOML + +out="$(run_smtp "$smtp_dir")" +expect "a configured [smtp] relay is carried" 'host = "mail.example"' "$out" +expect "its port survives the carry" 'port = 587' "$out" +expect "its credentials reference survives" 'password_ref = "smtp-password"' "$out" +case "$out" in + *"#"*) + echo "FAIL: the carry hoards comment lines:"; printf '%s\n' "$out"; fails=$((fails + 1)) ;; + *) echo "PASS the carry is header and keys only -- no comment hoard" ;; +esac +case "$out" in + *"[["*) + echo "FAIL: the carry ran into the next section:"; printf '%s\n' "$out"; fails=$((fails + 1)) ;; + *) echo "PASS the carry stops at the next section header" ;; +esac + +# Write the carry back the way write_felis_toml does (carry + one fresh template block + +# the tables) and extract again: a second re-run must add nothing. +{ + printf '%s\n' "$out" + printf '\n%s\n' '# Third-party Yggdrasil sources federated by the hasJoined multiplexer. Mojang is' + printf '%s\n' '[[auth_source]]' ' tag = "littleskin"' ' prefix = "LS"' \ + ' url = "https://littleskin.cn/api/yggdrasil/sessionserver/session/minecraft/hasJoined"' +} > "$smtp_dir/felis.host.toml" +out2="$(run_smtp "$smtp_dir")" +printf '%s\n' "$out" > "$smtp_dir/first" +printf '%s\n' "$out2" > "$smtp_dir/second" +if cmp -s "$smtp_dir/first" "$smtp_dir/second"; then + echo "PASS a carried-forward [smtp] converges (a second re-run adds nothing)" +else + echo "FAIL: carrying [smtp] is not idempotent:"; diff "$smtp_dir/first" "$smtp_dir/second" | head + fails=$((fails + 1)) +fi + # --- write_nano_config leaves the unit able to read its config --------------------------- # felis-nano runs as a DynamicUser, so the directory must be searchable by others under a # hardened umask too, including one an older installer left at 0750 -- but the full