Compare commits

..
Author SHA1 Message Date
ksmithandClaude Sonnet 5 34c16cbfa8 Update README and in-app User Guide for VNC, Import/Export, and connect-time prompting
Both docs still described VNC as unimplemented and required usernames
up front for SSH/RDP -- neither has been true for a while. Brings them
in line with the actual current feature set: VNC's encodings/clipboard/
cursor sync/display modes and honest Apple Screen Sharing auth status,
blank-username connect-time prompting across all three protocols,
profile Import/Export and mRemoteNG import, and the new prompt
visibility behavior (issue #22). docs/USER_GUIDE.md also drives the
in-app Help -> User Guide dialog and the PDF shipped with releases.

Bump version to v2026.9.16.7.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 11:20:18 -06:00
ksmithandClaude Sonnet 5 1506d87719 Make username/password connect prompts hard to miss (issue #22)
The inline prompt bar previously had no explicit styling and rendered
in the same color as the rest of the tab; a tab showing the prompt in
the background had no indication anything needed attention. The bar
now uses a solid QPalette::Highlight fill with HighlightedText for the
label and a hand-drawn contrasting badge, and a background tab gets a
"(Needs input)" title suffix plus a distinct tab-bar color.

A first pass at the tab color (#6a1b9a) was reported unreadable in
dark mode; replaced with #ab47bc, tuned to match the visibility of the
existing connection-state colors.

Bump version to v2026.9.16.6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 09:18:43 -06:00
ksmithandClaude Sonnet 5 b3cedcfa48 Fix connect-time username prompt never reaching SSH/RDP authentication
The username entered at the connect-time prompt (added for issue #21)
was updating SessionTab's own in-memory Profile copy, but
SshSessionBackend/RdpSessionBackend are constructed with -- and only
ever read from -- their own separate Profile copy on a worker thread,
which never saw that edit. Authentication was still built from the
original (blank) username regardless of what was typed into the
prompt.

SessionConnectOptions gains a username field, populated by SessionTab
on every connect attempt and threaded through the same way password
already is; both backends now prefer options.username over
profile().username. Covered by a new SSH regression test using an
exact-match fixture host that only succeeds for a specific
user@host target.

Bump version to v2026.9.16.5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:40:34 -06:00
ksmithandClaude Sonnet 5 0cdf930303 Fix connect-time username prompt being unreachable for SSH/RDP
validateProfileForConnect() still hard-failed with a blocking
QMessageBox for a blank SSH/RDP username, running before
requestConnectOptions() ever got a chance to prompt for it inline --
so the connect-time username prompt added for issue #21 was dead code
in practice; users just got told to go edit the profile instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:28:41 -06:00
13 changed files with 419 additions and 38 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.21)
project(OrbitHub VERSION 2026.9.16.3 LANGUAGES CXX)
project(OrbitHub VERSION 2026.9.16.7 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+57 -9
View File
@@ -15,16 +15,23 @@ OrbitHub is in active development.
- Milestones completed: M0-M9
- Current milestone: Milestone 10 (v1.0 Stabilization)
- Latest checkpoint tag: `v2026.9.15`
- VNC (M6) covers standard VNC Authentication and no-auth servers; see
[docs/PROGRESS.md](docs/PROGRESS.md) for known gaps (Apple Screen
Sharing auth, compression encodings, resize, cursor sync, clipboard)
- Latest published release: `v2026.9.16`; a `v2026.9.16.6` release is
drafted with several post-`.16` fixes and is pending a macOS installer
before publishing
- VNC (M6) is fully built out: Raw/CopyRect/Hextile/ZRLE/Tight encodings,
bidirectional clipboard sync, remote cursor shape sync, and both
scale-to-fit and actual-size display modes. Standard VNC Authentication
and no-auth servers (TigerVNC, x11vnc, TightVNC, etc.) work end to end.
macOS's built-in Screen Sharing uses two undocumented, reverse-engineered
authentication schemes that are implemented but not yet confirmed working
against a real macOS server — see
[docs/PROGRESS.md](docs/PROGRESS.md) for details.
Progress and milestone details:
- [docs/PROGRESS.md](docs/PROGRESS.md)
Latest release (installers for Windows, Linux, and macOS):
- [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15)
Latest published release (installers for Windows, Linux, and macOS):
- [v2026.9.16](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.16)
User Guide:
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
@@ -50,6 +57,8 @@ An embedded RDP session in a tab.
- SQLite-backed profile storage
- Create, edit, delete profiles
- Protocol-aware profile validation (SSH/RDP/VNC)
- Username is optional on any protocol's profile — if left blank, OrbitHub
asks for one inline at connect time instead of requiring it up front
- Profile search and sorting
- Tags support
- Folder/subfolder support
@@ -58,13 +67,20 @@ An embedded RDP session in a tab.
- New Folder
- New Connection
- Drag-and-drop profile moves between folders with persistence
- Import/Export of the whole profile list as JSON, and one-way import from
mRemoteNG connection XML files (passwords are never imported)
### Session Experience
- Multi-tab session window
- Auto-connect on tab open
- Disconnect on tab close
- Session state indicators on tabs
- Session state indicators on tabs, colored distinctly per state
(connecting/connected/disconnected/failed)
- A tab awaiting a username/password prompt is clearly marked — its title
gets a "(Needs input)" suffix and its tab color changes — even when it
isn't the tab currently in view, and the inline prompt itself uses a
solid highlighted banner rather than blending into the rest of the tab
- Timestamped event log with filtering and export
### SSH
@@ -82,14 +98,36 @@ An embedded RDP session in a tab.
- Domain-aware authentication support
- RDP security/performance profile options
### VNC
- Embedded in-window VNC rendering surface (no external launcher)
- Raw, CopyRect, Hextile, ZRLE, and Tight rectangle encodings
- Bidirectional clipboard sync
- Remote cursor shape sync
- Scale-to-fit and actual-size (scrollable) display modes, toggled
per-tab and remembered across sessions
- Standard VNC Authentication and no-auth servers (TigerVNC, x11vnc,
TightVNC, etc.)
- No dynamic remote-desktop resizing (VNC has no real equivalent of RDP's
MS-RDPEDISP)
- macOS Screen Sharing's two undocumented Apple auth schemes are
implemented but not yet confirmed working against a real macOS server —
see [docs/PROGRESS.md](docs/PROGRESS.md)
### App UX
- App icon and themed About dialog
- In-app User Guide (`Help -> User Guide`), matching
[docs/USER_GUIDE.md](docs/USER_GUIDE.md)
- `File` menu:
- New Profile
- New Folder
- Import Profiles...
- Export Profiles...
- Import from mRemoteNG...
- Quit
- `Help` menu:
- User Guide
- About OrbitHub
## Build and Run
@@ -140,6 +178,9 @@ Core dependencies:
- Qt 6 (Widgets, SQL)
- CMake 3.21+
- C++17 toolchain
- OpenSSL (RDP/VNC transport security and Apple VNC auth)
- zlib (VNC's ZRLE and Tight encodings)
- libjpeg-turbo (VNC's Tight encoding's JPEG sub-mode)
Protocol/runtime dependencies:
- SSH client (`ssh`) available on `PATH` for SSH sessions
@@ -176,12 +217,19 @@ See in-app `Help -> About OrbitHub` for license links and third-party inventory.
- `src/` - application source code
- `docs/` - build guide, spec, and progress tracking
- `packaging/` - per-platform installer/package build scripts
- `tests/` - CTest unit/integration tests and fixtures
- `tools/` - standalone build-time tools (e.g. the User Guide PDF generator)
- `third_party/` - vendored third-party dependencies
- `build/` - local build output (generated)
- `dist/` - packaged installer/deb/flatpak/dmg output (generated)
## Notes
- Passwords are requested at connect time and are not stored in the profile database.
- A profile's username is optional for every protocol; if left blank,
OrbitHub asks for one inline the first time you connect that profile.
- VNC support covers standard VNC Authentication and no-auth servers (e.g. TigerVNC, x11vnc,
TightVNC); it doesn't yet reach macOS's built-in Screen Sharing server, which uses a different
authentication scheme (see docs/PROGRESS.md, Milestone 6).
TightVNC). macOS's built-in Screen Sharing server uses two undocumented, reverse-engineered
authentication schemes that are implemented but not yet confirmed working against a real
macOS server (see docs/PROGRESS.md, Milestone 6).
+42
View File
@@ -184,6 +184,48 @@ Delivered:
through the dialog), so importing any SSH/RDP entry without a
username still failed outright until this second check was found and
removed too
- A third, independent username check was still live even after the two
above were removed: `SessionTab::validateProfileForConnect()` (run at
the very top of `connectSession()`/`reconnectSession()`, before
`requestConnectOptions()` ever gets a chance to run its async prompt)
had its own hard-fail "SSH/RDP username is required" `QMessageBox`,
so a blank-username profile still couldn't connect at all -- it just
told the user to go edit the profile instead of ever prompting inline.
Removed; connect-time prompting is now the only username gate for
SSH/RDP
- Even with the three checks above gone, a username entered at the
connect-time prompt still never actually reached SSH or RDP
authentication: `SshSessionBackend`/`RdpSessionBackend` are constructed
with their own `Profile` copy up front (moved to a worker thread) and
read `profile().username` directly, which never sees `SessionTab`'s
later edit to its own in-memory profile once the user answers the
prompt. `SessionConnectOptions` (which already carries `password` the
same way) gained a `username` field, populated by `SessionTab` from
its profile copy on every connect attempt; both backends now prefer
`options.username` over `profile().username` when building the actual
connect target/auth call. Covered by a new SSH regression test
(`tests/fixtures/fake_ssh.sh`'s `requireuser` host only accepts an
exact `prompted-user@requireuser` target, so the test fails unless the
option, not the stale profile copy, is actually used) -- RDP has no
equivalent fake-server test harness, so that side relies on mirroring
the already-tested `m_activeOptions.password` pattern exactly
- Issue #22: the inline username/password prompt bar used to just be a
plain `QWidget` with `setAutoFillBackground(true)` and no explicit
color, which meant it rendered in the same color as everything else
around it and was easy to miss -- especially on a tab that wasn't the
active one, where there was previously no indication anything needed
attention at all. Now uses a solid `QPalette::Highlight` fill with
`QPalette::HighlightedText` for the label (the OS theme's own
guaranteed-contrasting pair, so it stays correct under both light and
dark themes without a hardcoded color) plus a hand-drawn "?" badge
(not a themed `QStyle` icon, whose own colors are outside our control
and could land close in hue to the bar's background); a background
tab showing the prompt gets its title suffixed "(Needs input)" and its
tab-bar text colored distinctly from the four connection-state colors.
A first pass at the tab color (`#6a1b9a`) was reported unreadable in
dark mode -- its perceived luminance was well below the four existing
state colors -- and was replaced with `#ab47bc`, tuned to roughly
match their visibility
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
used to abort the connection generically; `kAnnouncedEncodings` is now
the single source of truth for what `SetEncodings` announces and what
+113 -15
View File
@@ -1,16 +1,19 @@
## Introduction
OrbitHub is a native desktop application for organizing connection profiles
and launching SSH and RDP sessions from one place, in a single tabbed
and launching SSH, RDP, and VNC sessions from one place, in a single tabbed
window. It runs on Windows, Linux, and macOS.
This guide covers everyday use: creating and organizing profiles,
connecting over SSH and RDP, managing active sessions, and what to do when
something goes wrong. It does not cover installation or building from
connecting over SSH, RDP, and VNC, managing active sessions, and what to do
when something goes wrong. It does not cover installation or building from
source — see the project's `README.md` and `docs/BUILDING.md` for that.
VNC support is planned but not yet implemented; profiles can be tagged for
it, but connecting will show an "unsupported protocol" message for now.
VNC support covers standard VNC Authentication and no-auth servers
(TigerVNC, x11vnc, TightVNC, and similar). macOS's built-in Screen Sharing
server uses two undocumented, reverse-engineered authentication schemes
that are implemented but not yet confirmed working end to end against a
real macOS server — see [Connecting via VNC](#connecting-via-vnc).
## Getting Started
@@ -43,15 +46,19 @@ A profile stores everything needed to reach one remote host. Open **New**
- **Name** — a label for the profile; shown on its tab and in the list.
- **Host** — hostname or IP address.
- **Port** — defaults to `22` for SSH, `3389` for RDP.
- **Username** — the account to log in as.
- **Port** — defaults to `22` for SSH, `3389` for RDP, `5900` for VNC.
- **Username** — the account to log in as. Optional for every protocol —
leave it blank and OrbitHub asks for one inline the first time you
connect that profile, the same way it already asks for a password. (VNC
servers mostly ignore username entirely; macOS Screen Sharing is the
exception and requires one — see
[Connecting via VNC](#connecting-via-vnc).)
- **Domain** — Windows domain for RDP logins (leave blank for local/
workgroup accounts or SSH profiles).
workgroup accounts, SSH, or VNC profiles).
- **Tags** — free-form, comma-separated labels for filtering and grouping
(e.g. `prod, linux, db`).
- **Folder** — where the profile lives in the Folders view.
- **Protocol** — `SSH`, `RDP`, or `VNC` (VNC is accepted but not yet
connectable — see Introduction).
- **Protocol** — `SSH`, `RDP`, or `VNC`.
Fields below Protocol change based on what you pick:
@@ -63,6 +70,9 @@ policy means.
**RDP**: **RDP Security** and **RDP Performance** — see
[Connecting via RDP](#connecting-via-rdp).
**VNC** has no protocol-specific fields beyond Host/Port/Username above —
see [Connecting via VNC](#connecting-via-vnc).
Passwords are never saved in the profile — OrbitHub asks for them each
time you connect (unless you've set up private-key SSH auth, which needs
no password to be entered per-connection if the key itself has none).
@@ -85,6 +95,24 @@ Combine both with the **Search** box (matches name, host, folder path, or
tags) and the **Sort** control (Name / Protocol / Host) to find what you
need quickly even with a large profile list.
## Importing and Exporting Profiles
The **File** menu has three options for moving profiles in or out of
OrbitHub:
- **Export Profiles...** — saves your entire profile list, including
folder structure and tags, to a JSON file. Passwords are never included,
since OrbitHub never stores them in the first place.
- **Import Profiles...** — reads that same JSON format back in, recreating
folders and profiles. Existing profiles aren't touched; entries missing
a name or host are skipped and reported in the summary.
- **Import from mRemoteNG...** — reads an mRemoteNG connections `.xml`
file and creates equivalent OrbitHub profiles. Passwords are never
imported — you'll be prompted the first time you connect each imported
profile, same as a new one. Connections using a protocol OrbitHub
doesn't support are skipped and listed in the import summary. This is a
one-way conversion; there's no export back to mRemoteNG's format.
## Connecting via SSH
Double-clicking an SSH profile opens a new tab with an embedded, fully
@@ -93,6 +121,8 @@ emulator. A **theme** selector lets you switch between `Dark`, `Light`,
and `Solarized Dark`; your choice is remembered for future sessions.
**Authentication.** Set in the profile itself:
- **Username** — if left blank in the profile, OrbitHub asks for one
inline the first time you connect, right alongside the password prompt.
- **Password** — OrbitHub prompts for a password each time you connect.
It is never stored.
- **Private Key** — point at a key file (via the profile's Browse button);
@@ -105,7 +135,7 @@ silently impersonating a host you've connected to before:
| Policy | Behavior |
|---|---|
| `Ask` | Prompts you to confirm trust the first time a host is seen, and on any later change. Recommended default. |
| `Accept-new` | Silently trusts a host the first time it's seen, but still stops and warns if a previously-trusted host's key later changes. |
| `Accept New` | Silently trusts a host the first time it's seen, but still stops and warns if a previously-trusted host's key later changes. |
| `Strict` | Never trusts an unknown host automatically — the connection fails until you've manually confirmed the host key some other way. |
| `Ignore` | Skips host-key checking entirely. Only use this for throwaway/test environments — it removes protection against on-path attacks. |
@@ -123,7 +153,8 @@ window renegotiates the remote resolution to match. Clipboard content
syncs between your machine and the remote session automatically.
**Authentication** uses the profile's Username/Domain fields; OrbitHub
prompts for the password at connect time.
prompts for the password at connect time, and for a username too if the
profile's own Username field was left blank.
**RDP Security** controls which transport-security layer is used to
negotiate the connection:
@@ -151,6 +182,51 @@ expected — you rotated the server's certificate yourself — reconnecting
after clearing the old entry from FreeRDP's certificate store will trust
the new one.
## Connecting via VNC
VNC sessions render in an embedded display surface inside the tab, like
RDP — no external VNC client window opens.
**Authentication.** VNC servers don't have a fixed username/password
convention the way SSH and RDP do, so OrbitHub always prompts for a
password at connect time (leave it blank if the server doesn't require
one — no-auth servers exist and are supported). Most VNC servers ignore
username entirely; the one common exception is **macOS's built-in Screen
Sharing**, which does require one — see the Apple Screen Sharing note
below if you're connecting to a Mac.
**Display mode.** Right-click a VNC session's tab to choose:
- **Scale to Fit** (default) — the whole remote screen is scaled to fit
the tab, like RDP.
- **Actual Size (Scrollbars)** — renders at the remote's native pixel
size, with scrollbars for panning. Useful when scaling would make small
text illegible.
Your choice is remembered across sessions. Unlike RDP, VNC has no way to
renegotiate the remote screen's resolution to match your window size —
resizing the OrbitHub window changes how much of a scaled-down remote
screen you can see, not the remote resolution itself.
**Clipboard and cursor.** Clipboard content syncs between your machine and
the remote session in both directions, and the remote's cursor shape (not
just position) is mirrored locally when the server supports it.
**Encodings.** OrbitHub negotiates whichever of Raw, CopyRect, Hextile,
ZRLE, or Tight the server prefers — this is automatic and requires no
configuration; more efficient encodings (Tight, ZRLE) simply mean better
performance over slower links.
**Apple Screen Sharing (macOS).** macOS's built-in Screen Sharing server
doesn't use a standard, documented VNC authentication method — it uses two
undocumented schemes that OrbitHub implements based on reverse-engineering
work, but neither is yet confirmed to authenticate successfully against a
real macOS server. If you're connecting to a Mac's Screen Sharing and see
an authentication failure, this is the likely cause rather than a wrong
username or password; check
[docs/PROGRESS.md](https://git.darksingularity.org/DarkSingularity/orbithub/blob/main/docs/PROGRESS.md)
(Milestone 6) for the current status, or a VNC server you install yourself
on the Mac (e.g. TigerVNC) as a workaround in the meantime.
## Managing Sessions
Every open connection lives in its own tab in the same window, alongside
@@ -158,6 +234,14 @@ the Profiles tab. A tab's title and a colored state indicator show
whether it's connecting, connected, disconnected, or failed. Closing a
tab disconnects that session; opening a profile again starts a fresh one.
**When OrbitHub needs a username or password from you**, the tab shows a
highlighted prompt bar with a Connect/Cancel pair — this happens for a
blank-username SSH/RDP profile, any RDP or SSH password prompt, and any
VNC connection (VNC always asks for a password, even if blank). If that
prompt appears on a tab you're not currently viewing, the tab's title
gets a `(Needs input)` suffix and its color changes so it's easy to spot
among several open sessions.
Each session tab includes a collapsible **event log** beneath the
connection surface — a timestamped record of connection state changes,
warnings, and errors for that session. Controls above the log let you:
@@ -176,8 +260,9 @@ settings screen — they're saved automatically as you use the app:
- Profile list: search text, view mode (List/Folders), Protocol/Tag
filters, and sort order.
- Session tabs: terminal theme choice, and whether the events panel is
shown or hidden for new tabs.
- Session tabs: terminal theme choice, whether the events panel is shown
or hidden for new tabs, and VNC display mode (Scale to Fit / Actual
Size).
Profile data itself (names, hosts, tags, folder structure, and so on) is
stored in a local SQLite database — see the README for its exact path on
@@ -209,10 +294,23 @@ certificate is replaced.
**SSH: connection hangs at "Ask" waiting for host-key trust** — check
that policy's behavior under
[Connecting via SSH](#connecting-via-ssh); switching to `Accept-new` avoids
[Connecting via SSH](#connecting-via-ssh); switching to `Accept New` avoids
the prompt for genuinely new hosts while still protecting against a later
key change.
**VNC: "Authentication or authorization failure"** — for most VNC
servers this means a wrong password. If you're connecting to **macOS
Screen Sharing** specifically, this is the expected result right now — see
the Apple Screen Sharing note under
[Connecting via VNC](#connecting-via-vnc).
**VNC: connection closes immediately with no prompt** — some servers
(including macOS Screen Sharing in some configurations) close the
connection outright rather than negotiating; confirm the server is
actually running and reachable on the profile's port (default `5900`),
and that no allowlist on the server side is blocking your account or
machine.
If none of this covers what you're seeing, a session's exported event log
(see Managing Sessions) is the most useful thing to include when asking
for help or filing an issue.
+7 -1
View File
@@ -1640,7 +1640,13 @@ void RdpSessionBackend::workerMain()
const Profile& p = profile();
const QString host = p.host.trimmed();
QString username = p.username.trimmed();
// m_activeOptions.username carries a value prompted for at connect time
// (see SessionTab::requestConnectOptions()) when the saved profile's
// own username was blank; profile().username never sees that edit
// since this backend's Profile copy was captured at construction time.
QString username = m_activeOptions.username.trimmed().isEmpty()
? p.username.trimmed()
: m_activeOptions.username.trimmed();
QString domain = p.domain.trimmed();
if (domain.isEmpty()) {
const int domainSeparator = username.indexOf(QLatin1Char('\\'));
+6
View File
@@ -12,6 +12,12 @@
class SessionConnectOptions
{
public:
// Only set when the profile's own username was blank and SessionTab
// prompted for one inline at connect time (see issue #21); empty means
// "use the backend's own profile().username" as before. SSH/RDP need
// this up front, unlike VNC's Apple auth which discovers the need for
// one mid-connection via usernameRequested()/provideUsername() instead.
QString username;
QString password;
QString privateKeyPath;
QString knownHostsPolicy;
+83 -8
View File
@@ -17,10 +17,14 @@
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPainter>
#include <QPalette>
#include <QPixmap>
#include <QPlainTextEdit>
#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <QColor>
#include <QComboBox>
#include <QProcessEnvironment>
#include <QPushButton>
@@ -59,6 +63,34 @@ TerminalTheme themeForName(const QString& themeName)
return TerminalTheme::loadKonsoleTheme(
QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme"));
}
// A filled circle with a bold "?", used on the username/password prompt
// bar (issue #22). Drawn by hand rather than pulled from a QStyle standard
// icon because a themed icon's own internal colors are outside our
// control and could end up close in hue to the bar's own background,
// undermining the contrast the bar is trying to achieve; painting it
// ourselves guarantees fillColor/textColor are exactly the same
// guaranteed-contrasting pair used for the rest of the bar.
QPixmap questionMarkBadgePixmap(const QColor& fillColor, const QColor& textColor, int diameter)
{
QPixmap pixmap(diameter, diameter);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::NoPen);
painter.setBrush(fillColor);
painter.drawEllipse(0, 0, diameter, diameter);
QFont font = painter.font();
font.setBold(true);
font.setPixelSize(static_cast<int>(diameter * 0.65));
painter.setFont(font);
painter.setPen(textColor);
painter.drawText(QRect(0, 0, diameter, diameter), Qt::AlignCenter, QStringLiteral("?"));
return pixmap;
}
}
SessionTab::SessionTab(const Profile& profile,
@@ -93,10 +125,12 @@ SessionTab::SessionTab(const Profile& profile,
m_exportEventsButton(nullptr),
m_eventsPanel(nullptr),
m_passwordPromptBar(nullptr),
m_passwordPromptIcon(nullptr),
m_passwordPromptLabel(nullptr),
m_passwordPromptInput(nullptr),
m_passwordPromptConnectButton(nullptr),
m_passwordPromptCancelButton(nullptr),
m_awaitingUserInput(false),
m_eventSeverityFilter(EventSeverity::Info),
m_eventsPanelExpanded(preferences.eventsPanelExpanded)
{
@@ -341,9 +375,17 @@ SessionTab::~SessionTab()
QString SessionTab::tabTitle() const
{
if (m_awaitingUserInput) {
return QStringLiteral("%1 (Needs input)").arg(m_profile.name);
}
return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix());
}
bool SessionTab::awaitingUserInput() const
{
return m_awaitingUserInput;
}
void SessionTab::connectSession()
{
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
@@ -806,21 +848,42 @@ void SessionTab::setupUi()
applyTerminalTheme(m_terminalThemeName);
// Styled distinctly (issue #22: this bar used to blend straight into
// the plain window background and was easy to miss, especially on a
// tab you weren't actively looking at). A solid QPalette::Highlight
// fill with QPalette::HighlightedText for the label is used rather
// than a subtle tint -- a faint tint proved to still be easy to miss,
// and Highlight/HighlightedText are the OS theme's own guaranteed-
// contrasting pair, so this stays readable under both light and dark
// themes without hardcoding a color.
auto* passwordPromptLayout = new QHBoxLayout();
m_passwordPromptIcon = new QLabel(this);
const QColor highlight = palette().color(QPalette::Highlight);
const QColor highlightedText = palette().color(QPalette::HighlightedText);
m_passwordPromptIcon->setPixmap(questionMarkBadgePixmap(highlightedText, highlight, 22));
m_passwordPromptLabel = new QLabel(this);
m_passwordPromptLabel->setObjectName(QStringLiteral("passwordPromptLabel"));
m_passwordPromptInput = new QLineEdit(this);
m_passwordPromptInput->setEchoMode(QLineEdit::Password);
m_passwordPromptConnectButton = new QPushButton(QStringLiteral("Connect"), this);
m_passwordPromptCancelButton = new QPushButton(QStringLiteral("Cancel"), this);
passwordPromptLayout->addWidget(m_passwordPromptIcon);
passwordPromptLayout->addWidget(m_passwordPromptLabel);
passwordPromptLayout->addWidget(m_passwordPromptInput, 1);
passwordPromptLayout->addWidget(m_passwordPromptConnectButton);
passwordPromptLayout->addWidget(m_passwordPromptCancelButton);
passwordPromptLayout->setContentsMargins(10, 8, 10, 8);
m_passwordPromptBar = new QWidget(this);
m_passwordPromptBar->setObjectName(QStringLiteral("passwordPromptBar"));
m_passwordPromptBar->setLayout(passwordPromptLayout);
m_passwordPromptBar->setAutoFillBackground(true);
m_passwordPromptBar->setVisible(false);
m_passwordPromptBar->setStyleSheet(
QStringLiteral("QWidget#passwordPromptBar { background-color: %1; }"
"QWidget#passwordPromptBar QLabel#passwordPromptLabel "
"{ color: %2; font-weight: bold; font-size: 11pt; }")
.arg(highlight.name(), highlightedText.name()));
rootLayout->addWidget(m_passwordPromptBar);
connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() {
@@ -998,6 +1061,10 @@ void SessionTab::requestConnectOptions(
{
SessionConnectOptions baseOptions;
baseOptions.knownHostsPolicy = m_profile.knownHostsPolicy;
// The backend's own Profile copy was captured when it was constructed
// and never sees later edits to m_profile (e.g. the username prompt
// below) -- it has to travel through here instead.
baseOptions.username = m_profile.username.trimmed();
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
@@ -1157,12 +1224,24 @@ void SessionTab::showPasswordPrompt(const QString& labelText,
m_passwordPromptInput->setEchoMode(maskInput ? QLineEdit::Password : QLineEdit::Normal);
m_passwordPromptBar->setVisible(true);
m_passwordPromptInput->setFocus();
if (!m_awaitingUserInput) {
m_awaitingUserInput = true;
emit awaitingUserInputChanged(true);
emit tabTitleChanged(tabTitle());
}
}
void SessionTab::hidePasswordPrompt()
{
m_passwordPromptBar->setVisible(false);
m_passwordPromptCallback = nullptr;
if (m_awaitingUserInput) {
m_awaitingUserInput = false;
emit awaitingUserInputChanged(false);
emit tabTitleChanged(tabTitle());
}
}
bool SessionTab::validateProfileForConnect()
@@ -1181,14 +1260,10 @@ bool SessionTab::validateProfileForConnect()
return false;
}
if ((m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0
|| m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0)
&& m_profile.username.trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("%1 username is required.").arg(m_profile.protocol));
return false;
}
// SSH/RDP no longer hard-require a username here -- a blank one is
// handled by requestConnectOptions() prompting for it inline at connect
// time (see issue #21). Do not re-add a check here without also
// updating that flow.
return true;
}
+8
View File
@@ -67,10 +67,16 @@ public:
bool supportsVncScaleToggle() const;
void setVncScaleToFit(bool scaleToFit);
bool vncScaleToFit() const;
bool awaitingUserInput() const;
signals:
void tabTitleChanged(const QString& title);
void tabStateChanged(SessionState state);
// Fires whenever the inline username/password prompt bar is shown or
// hidden -- independent of tabStateChanged(), since the backend is
// still just "Connecting" while it's up. Lets SessionWindow mark a
// background tab that needs the user's attention (issue #22).
void awaitingUserInputChanged(bool waiting);
void terminalThemeChanged(const QString& themeName);
void terminalFontSizeChanged(int pointSize);
void eventsPanelVisibilityChanged(bool expanded);
@@ -129,11 +135,13 @@ private:
QToolButton* m_exportEventsButton;
QWidget* m_eventsPanel;
QWidget* m_passwordPromptBar;
QLabel* m_passwordPromptIcon;
QLabel* m_passwordPromptLabel;
QLineEdit* m_passwordPromptInput;
QPushButton* m_passwordPromptConnectButton;
QPushButton* m_passwordPromptCancelButton;
std::function<void(std::optional<QString>)> m_passwordPromptCallback;
bool m_awaitingUserInput;
enum class EventSeverity {
Info,
Warning,
+38
View File
@@ -34,6 +34,19 @@ QColor tabColorForState(SessionState state, const QPalette& palette)
return palette.color(QPalette::WindowText);
}
// Distinct from all four tabColorForState() colors -- a tab awaiting a
// username/password prompt response needs to stand out even from a
// tab that's merely "Connecting" (issue #22), including when it isn't
// the one currently in view. #6a1b9a (a much darker violet) was tried
// first and reported unreadable against a dark-theme tab bar -- its
// perceived luminance is well below the other three colors above, which
// this one is tuned to roughly match so it reads about as well as they
// do in both light and dark themes.
QColor awaitingInputTabColor()
{
return QColor(QStringLiteral("#ab47bc"));
}
QStringList terminalThemeNames()
{
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
@@ -57,6 +70,7 @@ SessionWindow::SessionWindow(QWidget* parent)
QWidget* tab = m_tabs->widget(index);
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
sessionTab->disconnectSession();
m_tabStates.remove(sessionTab);
}
m_tabs->removeTab(index);
delete tab;
@@ -261,6 +275,7 @@ void SessionWindow::addSessionTab(const Profile& profile)
} else {
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
}
m_tabStates.insert(tab, SessionState::Disconnected);
m_tabs->tabBar()->setTabTextColor(
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
@@ -272,6 +287,13 @@ void SessionWindow::addSessionTab(const Profile& profile)
&SessionTab::tabStateChanged,
this,
[this, tab](SessionState state) {
m_tabStates.insert(tab, state);
if (tab->awaitingUserInput()) {
// Keep the "needs input" color on top -- it'll be
// restored to reflect this state once the prompt
// resolves (see awaitingUserInputChanged below).
return;
}
for (int i = 0; i < m_tabs->count(); ++i) {
if (m_tabs->widget(i) == tab) {
m_tabs->tabBar()->setTabTextColor(
@@ -280,6 +302,22 @@ void SessionWindow::addSessionTab(const Profile& profile)
}
}
});
connect(tab,
&SessionTab::awaitingUserInputChanged,
this,
[this, tab](bool waiting) {
for (int i = 0; i < m_tabs->count(); ++i) {
if (m_tabs->widget(i) != tab) {
continue;
}
const QColor color = waiting
? awaitingInputTabColor()
: tabColorForState(m_tabStates.value(tab, SessionState::Disconnected),
m_tabs->palette());
m_tabs->tabBar()->setTabTextColor(i, color);
return;
}
});
connect(tab,
&SessionTab::terminalThemeChanged,
this,
+5
View File
@@ -4,6 +4,7 @@
#include "profile_repository.h"
#include "session_tab.h"
#include <QHash>
#include <QMainWindow>
class QTabWidget;
@@ -21,6 +22,10 @@ private:
QTabWidget* m_tabs;
ProfilesWindow* m_profilesWidget;
SessionUiPreferences m_preferences;
// Last known connection state per tab, so the tab color can be
// restored correctly once an awaitingUserInputChanged(false) fires
// (that signal is orthogonal to SessionState -- see session_tab.h).
QHash<SessionTab*, SessionState> m_tabStates;
void addSessionTab(const Profile& profile);
void updateTabTitle(SessionTab* tab, const QString& title);
+9 -2
View File
@@ -388,9 +388,16 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
<< QStringLiteral("PasswordAuthentication=no");
}
const QString target = p.username.trimmed().isEmpty()
// options.username carries a value prompted for at connect time (see
// SessionTab::requestConnectOptions()) when the saved profile's own
// username was blank; profile().username never sees that edit since
// the backend's Profile copy was captured at construction time.
const QString username = options.username.trimmed().isEmpty()
? p.username.trimmed()
: options.username.trimmed();
const QString target = username.isEmpty()
? p.host.trimmed()
: QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed());
: QStringLiteral("%1@%2").arg(username, p.host.trimmed());
args << target;
m_process->setProcessEnvironment(environment);
+16
View File
@@ -6,6 +6,22 @@
# host, optionally as user@host, as the final argument).
for arg in "$@"; do
case "$arg" in
prompted-user@requireuser)
# Only the exact user@host below is accepted -- used to prove a
# username supplied via SessionConnectOptions (prompted for at
# connect time because the saved profile's own username was
# blank) actually reaches the ssh command line, not just that
# *some* connection to this host succeeds.
echo "Welcome to the fake host."
while IFS= read -r line; do
echo "$line"
done
exit 0
;;
*@requireuser|requireuser)
echo "Permission denied (publickey,password)." >&2
exit 255
;;
*@succeed|succeed)
echo "Welcome to the fake host."
# Stay alive echoing stdin back (simulates an interactive
+34 -2
View File
@@ -21,6 +21,13 @@ Profile makeProfile(const QString& fixtureHost)
return profile;
}
Profile makeBlankUsernameProfile(const QString& fixtureHost)
{
Profile profile = makeProfile(fixtureHost);
profile.username.clear();
return profile;
}
SessionConnectOptions makeOptions()
{
SessionConnectOptions options;
@@ -50,10 +57,12 @@ private slots:
void connectionRefusedReachesFailedState();
void sendInputEchoesThroughOutputReceived();
void reconnectRestartsAndReachesConnectedAgain();
void connectOptionsUsernameReachesProcessWhenProfileUsernameIsBlank();
private:
QString fixturePath() const;
void createBackend(const QString& fixtureHost);
void createBackend(const Profile& profile);
std::unique_ptr<SshSessionBackend> m_backend;
SessionState m_lastState = SessionState::Disconnected;
@@ -69,8 +78,12 @@ QString TestSshSessionBackend::fixturePath() const
void TestSshSessionBackend::createBackend(const QString& fixtureHost)
{
m_backend =
std::make_unique<SshSessionBackend>(makeProfile(fixtureHost), fixturePath(), nullptr);
createBackend(makeProfile(fixtureHost));
}
void TestSshSessionBackend::createBackend(const Profile& profile)
{
m_backend = std::make_unique<SshSessionBackend>(profile, fixturePath(), nullptr);
connect(m_backend.get(),
&SessionBackend::stateChanged,
this,
@@ -232,5 +245,24 @@ void TestSshSessionBackend::reconnectRestartsAndReachesConnectedAgain()
QTRY_COMPARE(m_lastState, SessionState::Connected);
}
void TestSshSessionBackend::connectOptionsUsernameReachesProcessWhenProfileUsernameIsBlank()
{
// Regression test for a bug where a username entered at the
// connect-time prompt (SessionTab::requestConnectOptions(), for a
// profile with no saved username -- issue #21) never actually reached
// the ssh process: SshSessionBackend built its target purely from
// profile().username, which is a separate copy captured when the
// backend was constructed and never sees SessionTab's later edit.
// fixtures/fake_ssh.sh's "requireuser" host only accepts the exact
// target "prompted-user@requireuser", so this fails unless
// SessionConnectOptions::username is actually used.
createBackend(makeBlankUsernameProfile(QStringLiteral("requireuser")));
SessionConnectOptions options = makeOptions();
options.username = QStringLiteral("prompted-user");
m_backend->connectSession(options);
QTRY_COMPARE(m_lastState, SessionState::Connected);
}
QTEST_GUILESS_MAIN(TestSshSessionBackend)
#include "test_ssh_session_backend.moc"