Compare commits

...
Author SHA1 Message Date
atlas
93d28ce0b1 fix: strip code-span backticks from extracted matrix password
mara observed the tuwunel reply renders the new password as a code span,
so the plain message body carries literal backticks:

  Successfully reset password for @user:server to: `<password>`

The previous extraction stopped at the first whitespace, capturing the
surrounding backticks ("`<password>`") and producing a login string that
doesn't match the password the bot actually set — recovery would still
fail after parsing.

Strip a leading backtick after the marker and stop the token at the first
whitespace OR closing backtick. Generated passwords contain neither, so a
real password is never truncated mid-token. Two regression tests cover the
backtick-wrapped form, including trailing prose after the closing backtick.
2026-06-05 00:47:37 +02:00
atlas
defface0a5 fix: parse tuwunel 'reset password for X to: <pw>' admin reply
extract_new_password had markers for 'is:', 'changed to:', 'reset to:',
'set to:' etc. but none match tuwunel's actual reset-password reply:

  Successfully reset password for @user:server to: <password>

Here the verb ('reset') is not adjacent to 'to:', so every marker missed
and the function returned None for every polled event. The admin-room poll
then ran its full 15s loop without a match and the auto-recovery timed out
on every agent — even though the bot replied correctly and the backward
poll found the message. This is why matrix password auto-recovery kept
looping despite the poll-strategy fix.

Add a generic ' to: ' marker (with surrounding spaces) placed after the
specific verb markers and before the bare 'password:' last resort. Matrix
user ids and server names can't contain ' to: ', so it only ever anchors
on the prose delimiter. Two regression tests cover the exact production
wording.
2026-06-05 00:47:37 +02:00

View file

@ -378,6 +378,14 @@ fn extract_new_password(bot_message: &str) -> Option<String> {
"reset to:",
"set to: ",
"set to:",
// Generic "… to: <pw>" form. tuwunel's actual reset-password reply is
// "Successfully reset password for @user:server to: <password>" — the
// password follows " to: " but no recognised verb sits adjacent to it,
// so the markers above miss it. Matrix user ids / server names can't
// contain " to: ", so this only ever anchors on the prose delimiter.
// Placed after the specific verb markers and before the bare
// "password:" last resort.
" to: ",
// Bare "new password:" without "is":
"new password: ",
"new password:",
@ -387,10 +395,15 @@ fn extract_new_password(bot_message: &str) -> Option<String> {
] {
if let Some(pos) = lower.find(marker) {
let rest = &bot_message[pos + marker.len()..];
let rest = rest.trim_start();
// Stop at first whitespace or newline; password must be non-empty.
// Strip leading whitespace, then a leading code-span backtick:
// tuwunel renders the password as a code span, so the plain
// `body` carries literal backticks ("… to: `<pw>`").
let rest = rest.trim_start().trim_start_matches('`');
// The password ends at the first whitespace OR the closing
// backtick. Generated passwords contain neither, so this never
// truncates a real password mid-token.
let end = rest
.find(char::is_whitespace)
.find(|c: char| c.is_whitespace() || c == '`')
.unwrap_or(rest.len());
let pw = rest[..end].trim();
if !pw.is_empty() {
@ -459,6 +472,37 @@ mod extract_new_password_tests {
assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2"));
}
#[test]
fn tuwunel_reset_password_for_to_variant() {
// The actual tuwunel admin-room reply observed in production — the
// verb ("reset") is not adjacent to "to:", so only the generic
// " to: " marker catches it.
let msg = "Successfully reset password for @atlas:pr1ma.darkest.space to: N3wP@ssw0rd";
assert_eq!(extract_new_password(msg).as_deref(), Some("N3wP@ssw0rd"));
}
#[test]
fn to_marker_not_confused_by_user_id() {
// The user id contains no " to: " so the marker only fires on the
// real delimiter; the password is the token right after it.
let msg = "Successfully reset password for @sock:pr1ma.darkest.space to: abc123XYZ";
assert_eq!(extract_new_password(msg).as_deref(), Some("abc123XYZ"));
}
#[test]
fn backtick_wrapped_password() {
// tuwunel renders the password as a code span; the plain body
// carries literal backticks. Strip them, don't capture them.
let msg = "Successfully reset password for @atlas:pr1ma.darkest.space to: `N3wP@ssw0rd`";
assert_eq!(extract_new_password(msg).as_deref(), Some("N3wP@ssw0rd"));
}
#[test]
fn backtick_wrapped_with_trailing_text() {
let msg = "Done. New password is: `hunter2` (store it now)";
assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2"));
}
#[test]
fn bare_new_password_colon() {
let msg = "New password: P@ssword1";