Allow blank usernames, asking for one at connect time instead

Closes #21. SSH and RDP profiles previously hard-required a username
to even save the profile; that validation is dropped, and
SessionTab::requestConnectOptions() now prompts for it at connect
time when blank, reusing the existing password-prompt bar in
unmasked mode -- the same pattern already used for a blank password.

VNC's username is trickier: most VNC servers never use one (plain VNC
Authentication and no-auth don't), only the two Apple auth schemes
(security types 30/33) do, and which auth method gets used isn't known
until mid-connection, after the server's security-type list has been
negotiated -- too late for the pre-connect prompt SSH/RDP uses. Adds a
new async request/response pair to SessionBackend, usernameRequested()
signal / provideUsername() slot, mirroring the existing SSH host-key-
confirmation pattern. VncSessionBackend pauses its state machine right
before computing an Apple-auth response if no username is available --
without consuming the already-buffered prime/host-key bytes, so
resuming re-parses them identically -- emits the request, and resumes
via provideUsername(). Cancelling (or submitting blank) fails the
connection cleanly instead of sending Apple auth an empty username.

The username is kept on the tab's in-memory profile copy for its
lifetime, not written back to the saved profile, matching how
passwords are already handled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 08:01:57 -06:00
co-authored by Claude Sonnet 5
parent d7f9d4966b
commit 776db5ec04
8 changed files with 221 additions and 16 deletions
+61
View File
@@ -420,6 +420,7 @@ private slots:
void connectsWithAppleDhAuthenticationRfb38();
void appleDhAuthIsPreferredOverVncAuthWhenBothOffered();
void appleDhAuthAcceptsRealCapturedMacOsServerParameters();
void cancellingUsernamePromptFailsConnectionCleanly();
private:
std::unique_ptr<FakeVncServer> m_server;
@@ -2036,8 +2037,20 @@ void TestVncSessionBackend::connectsWithAppleDhAuthenticationRfb38()
}
});
// makeVncProfile() sets no username, and Apple DH is the one VNC auth
// method that needs one -- expect the backend to pause and ask,
// exactly like a real profile with a blank username would hit at
// connect time (see issue #21 / SessionBackend::usernameRequested).
QString requestedPrompt;
connect(m_backend.get(), &SessionBackend::usernameRequested, this,
[this, &requestedPrompt](const QString& prompt) {
requestedPrompt = prompt;
m_backend->provideUsername(QStringLiteral("tester"));
});
m_backend->connectSession(makeOptions(password));
QTRY_COMPARE(m_lastState, SessionState::Connected);
QVERIFY(!requestedPrompt.isEmpty());
// Verify wire order: encrypted credentials (128 bytes) MUST come
// before the client's public key, per neatvnc's authoritative
@@ -2085,7 +2098,9 @@ void TestVncSessionBackend::connectsWithAppleDhAuthenticationRfb38()
EVP_CIPHER_CTX_free(decCtx);
QByteArray expected(128, char(0));
const QByteArray userBytes = QByteArrayLiteral("tester").left(64);
const QByteArray passBytes = password.toLatin1().left(64);
std::memcpy(expected.data(), userBytes.constData(), static_cast<size_t>(userBytes.size()));
std::memcpy(expected.data() + 64, passBytes.constData(), static_cast<size_t>(passBytes.size()));
QCOMPARE(plain, expected);
@@ -2172,5 +2187,51 @@ void TestVncSessionBackend::appleDhAuthAcceptsRealCapturedMacOsServerParameters(
QCOMPARE(response.encryptedCredentials.size(), 128);
}
void TestVncSessionBackend::cancellingUsernamePromptFailsConnectionCleanly()
{
ToyDhKeypair serverKeypair = generateToyDhKeypair();
const QByteArray authMessage = appleDhAuthServerMessage(
serverKeypair.generatorBytes, serverKeypair.primeBytes, serverKeypair.publicKeyBytes);
bool gotUsernameRequest = false;
connect(m_backend.get(), &SessionBackend::usernameRequested, this,
[this, &gotUsernameRequest](const QString&) {
gotUsernameRequest = true;
// An empty response mirrors the prompt being cancelled
// (see SessionTab::onBackendUsernameRequested).
m_backend->provideUsername(QString());
});
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this, authMessage]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(30)); // Apple's scheme -- no other option offered
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(authMessage);
break;
default:
break;
}
});
// No username in the profile (makeVncProfile() sets none) and no
// other security type to fall back to.
m_backend->connectSession(makeOptions(QStringLiteral("s3cret-pass")));
QTRY_COMPARE(m_lastState, SessionState::Failed);
QVERIFY(gotUsernameRequest);
QVERIFY(m_lastErrorDisplay.contains(QStringLiteral("username"), Qt::CaseInsensitive));
BN_free(serverKeypair.privateExponent);
BN_free(serverKeypair.prime);
}
QTEST_GUILESS_MAIN(TestVncSessionBackend)
#include "test_vnc_session_backend.moc"