From 40f8e8db594966b0cd55ae1ace1a66d81abce0e1 Mon Sep 17 00:00:00 2001 From: nickrobnett <78187895+nickrobnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:40:23 -0500 Subject: [PATCH] Recognize abbreviated connect commands as credentials Treat nonempty prefixes of connect as login commands when a name and password are supplied. Reuse the merged credential filtering for local echo, transcripts and command history, including command stacks. --- CHANGELOG.md | 3 +++ src/SharpMUTerm.Core/Input/HistorySecrets.cs | 4 +++- .../Input/HistorySecretsTests.cs | 11 +++++++++++ .../Session/PasswordLeakTests.cs | 11 +++++++---- .../HistorySearchEndToEndTests.cs | 16 ++++++++++++---- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5702560..facca6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,9 @@ and a release's notes are its section. The procedure is in [`docs/RELEASING.md`] ### Fixed +- Abbreviated login commands such as `c name password` and `con name password` are hidden from local echo, + transcripts and credential-filtered command history, including when used inside a command stack. + - A password is no longer echoed into the output or written to the session log. When a game takes over echo at its password prompt (telnet `WILL ECHO`), the line you send is not echoed locally or kept in command history, and echo comes back when the game hands it back. A typed `connect `, including one inside a stacked line diff --git a/src/SharpMUTerm.Core/Input/HistorySecrets.cs b/src/SharpMUTerm.Core/Input/HistorySecrets.cs index faed04d..691cd46 100644 --- a/src/SharpMUTerm.Core/Input/HistorySecrets.cs +++ b/src/SharpMUTerm.Core/Input/HistorySecrets.cs @@ -31,8 +31,10 @@ public static class HistorySecrets /// The MU* login verbs, which take <name> <password>. cd is connect-dark and /// ch connect-hidden (PennMUSH/TinyMUX wizard variants), and both take the same two arguments; /// create chooses a password rather than presenting one, which is no less a secret. + /// PennMUSH also accepts every nonempty prefix of connect at login. /// - private static readonly string[] LoginVerbs = { "connect", "cd", "ch", "create" }; + private static readonly string[] LoginVerbs = + { "connect", "c", "co", "con", "conn", "conne", "connec", "cd", "ch", "create" }; /// /// Verbs whose first argument is already the secret — @password old=new, diff --git a/tests/SharpMUTerm.Core.Tests/Input/HistorySecretsTests.cs b/tests/SharpMUTerm.Core.Tests/Input/HistorySecretsTests.cs index 7da5c6c..63ff4de 100644 --- a/tests/SharpMUTerm.Core.Tests/Input/HistorySecretsTests.cs +++ b/tests/SharpMUTerm.Core.Tests/Input/HistorySecretsTests.cs @@ -12,6 +12,12 @@ public class HistorySecretsTests /// The lines that must never be recorded, in the spellings a user actually types. [Test] [Arguments("connect Corvid hunter2")] + [Arguments("c Corvid hunter2")] + [Arguments("co Corvid hunter2")] + [Arguments("Con Corvid hunter2")] + [Arguments("conn Corvid hunter2")] + [Arguments("conne Corvid hunter2")] + [Arguments("connec Corvid hunter2")] [Arguments("CONNECT Corvid hunter2")] // MU* dispatch is case-insensitive; so is this [Arguments("connect Corvid hunter2")] // runs of spaces are one separator [Arguments("cd Corvid hunter2")] // connect-dark @@ -33,6 +39,9 @@ public async Task ACredentialBearingLineIsRecognised(string command) [Test] [Arguments("connect")] [Arguments("connect guest")] + [Arguments("c guest")] + [Arguments("con guest")] + [Arguments("connection Corvid hunter2")] [Arguments("cd")] [Arguments("create")] [Arguments("@password")] @@ -67,6 +76,8 @@ public async Task TheVerbsAreNamed() [Arguments("look;connect Corvid hunter2", ";")] [Arguments("look;#1 connect Corvid hunter2", ";")] [Arguments("look;#101 connect Corvid hunter2", ";")] // a refused stack is still kept out + [Arguments("look;#1 con Corvid hunter2", ";")] + [Arguments("look;#101 con Corvid hunter2", ";")] [Arguments("look | @password old=new", "|")] [Arguments("look;;connect Corvid hunter2", ";;")] public async Task AStackedOrMultiLineLoginCarriesACredential(string line, string? separator) diff --git a/tests/SharpMUTerm.Core.Tests/Session/PasswordLeakTests.cs b/tests/SharpMUTerm.Core.Tests/Session/PasswordLeakTests.cs index 10e917e..246ecc5 100644 --- a/tests/SharpMUTerm.Core.Tests/Session/PasswordLeakTests.cs +++ b/tests/SharpMUTerm.Core.Tests/Session/PasswordLeakTests.cs @@ -147,16 +147,19 @@ await Assert.That(session.Scrollback.Snapshot().Any(l => l.Text.Contains(Secret, } [Test] - [Arguments(true)] - [Arguments(false)] - public async Task ALoginCommandReachesTelnetButIsHiddenInTheEcho(bool typed) + [Arguments(true, "connect")] + [Arguments(false, "connect")] + [Arguments(true, "con")] + [Arguments(false, "con")] + [Arguments(true, "c")] + public async Task ALoginCommandReachesTelnetButIsHiddenInTheEcho(bool typed, string verb) { var log = new RecordingSink(); var telnet = new FakeTelnetSession(); var session = new WorldSession(World(), sessionFactory: _ => telnet, log: log); await session.ConnectAsync(); - var command = $"connect Corvid {Secret}"; + var command = $"{verb} Corvid {Secret}"; if (typed) { await session.SendTypedAsync(command); diff --git a/tests/SharpMUTerm.Tui.Tests/HistorySearchEndToEndTests.cs b/tests/SharpMUTerm.Tui.Tests/HistorySearchEndToEndTests.cs index 9d73634..e954c92 100644 --- a/tests/SharpMUTerm.Tui.Tests/HistorySearchEndToEndTests.cs +++ b/tests/SharpMUTerm.Tui.Tests/HistorySearchEndToEndTests.cs @@ -471,19 +471,27 @@ public async Task ServerEchoKeepsStandalonePasswordOutOfHistory() /// sent (#1) or refused (#101, past the repeat cap), and the echo and the wire behave as they would. /// [Test] - [Arguments("#1")] - [Arguments("#101")] - public async Task AStackedLoginIsKeptOutOfHistoryAndSearch(string repeat) + [Arguments("#1", "connect")] + [Arguments("#101", "connect")] + [Arguments("#1", "con")] + [Arguments("#101", "con")] + public async Task AStackedLoginIsKeptOutOfHistoryAndSearch(string repeat, string verb) { var (app, _) = await Demo(separator: ";"); const string secret = "stacked-secret"; - Send(app, $"look;{repeat} connect Corvid {secret}"); + Send(app, $"look;{repeat} {verb} Corvid {secret}"); Send(app, "inventory"); await Assert.That(app.HistoryEntries(InputBar.Primary).Any(e => e.Contains(secret, StringComparison.Ordinal))) .IsFalse(); await Assert.That(app.HistoryEntries(InputBar.Primary)).Contains("inventory"); + app.SimulateKey(Bare(ConsoleKey.UpArrow)); + app.SimulateKey(Bare(ConsoleKey.UpArrow)); + await Assert.That(app.ArmedInputText).IsEqualTo("inventory"); + app.SimulateKey(Chord(ConsoleKey.R)); + app.SimulateHistorySearchTyping(secret); + await Assert.That(app.HistorySearchRows).IsEmpty(); } ///