Commit Graph
92 Commits
Author SHA1 Message Date
ksmithandClaude Sonnet 5 1f026dde70 Add Apple Screen Sharing authentication for VNC (security type 30)
Apple's macOS Screen Sharing server doesn't speak standard VNC
Authentication -- it uses a Diffie-Hellman key exchange followed by
AES-128-ECB-encrypted credentials, security type 30. Apple never
published this scheme (it's not part of RFC 6143); this implements
the well-established reverse-engineered wire format: the server sends
a generator, prime, and its own DH public key; the client generates
an ephemeral keypair, derives the shared secret, MD5-hashes it into
an AES key, and sends back its public key plus a 128-byte encrypted
username+password buffer.

The DH/AES math lives in new src/vnc_apple_dh_auth.h/.cpp as a pure,
socket-free helper (mirroring vncAuthResponse()'s shape for standard
VNC Auth), built entirely on modern EVP_PKEY-based OpenSSL 3.0 APIs --
no deprecated low-level DH_* calls, unlike VNC Authentication's
necessary use of classic DES. Reuses Profile::username (already a
shared field) since Apple's scheme needs an actual macOS account name,
unlike password-only VNC Authentication.

Security-type preference when multiple are offered is now None >
AppleDH > VNCAuth, since DH+AES is strictly stronger than static-
challenge DES. Adds a DH round-trip test (generates a real 512-bit
group at test time, computes the response, then independently
re-derives the shared secret as the server would and decrypts the
credentials back out -- proving self-consistency without needing a
hand-computed expected value), a fake-server integration test for the
full RFB 3.8 handshake sequencing, and a preference-order test.

Not yet live-verified against a real macOS Screen Sharing server --
that's next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 21:27:17 -06:00
ksmithandClaude Sonnet 5 2fe2022182 Document and package the new libjpeg-turbo dependency
Tight's JPEG sub-mode (previous commit) links libjpeg-turbo directly.
Adds it to docs/BUILDING.md for all three platforms (apt
libjpeg-turbo8-dev, brew jpeg-turbo, vcpkg libjpeg-turbo:x64-windows)
and to the .deb package's Depends: line (libjpeg-turbo8). No changes
needed for Windows (the Inno Setup script already wildcards *.dll) or
macOS (macdeployqt bundles non-system dylibs automatically). Left the
Flatpak manifests unchanged on the assumption that the KDE runtime
already bundles libjpeg-turbo as a standard Qt JPEG-plugin dependency
-- flagged in PROGRESS.md as worth confirming next time a Flatpak
build actually runs, since that wasn't independently verified here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 21:14:29 -06:00
ksmithandClaude Sonnet 5 4fca8fce41 Add VNC Tight decoding
Implements RFC 6143's Tight encoding: a compression-control byte (low
4 bits reset one of 4 persistent zlib streams; high nibble selects
Fill/JPEG/Basic mode) followed by Fill's 3-byte solid color, JPEG's
compact-length-prefixed baseline JPEG covering the whole rectangle
(decoded via libjpeg-turbo directly, not QImage's plugin, to avoid a
packaging-dependent runtime failure mode), or Basic mode's
compact-length-prefixed zlib payload plus a filter (Copy, Palette, or
Gradient) applied after decompression. Unlike Hextile/ZRLE, Tight has
no internal tiling -- one rectangle is one filtered/compressed unit.

The three filters live in vnc_pixel_codecs.h/.cpp alongside the
Hextile/ZRLE decoders. Adds find_package(JPEG REQUIRED) + JPEG::JPEG
as a new build dependency (confirmed available via libjpeg-turbo on
this dev machine). 5 new tests cover Fill, Basic+Copy, Basic+Palette,
JPEG (round-tripped through a real libjpeg-turbo-encoded fixture,
compared with tolerance since JPEG is lossy), and the stream-reset
flag correctly tearing down and reinitializing a targeted stream
rather than erroring on stale state.

Known, documented gap: this decoder always treats Basic-mode payloads
as zlib-compressed; the real protocol allows very small payloads to
skip compression, which couldn't be verified with confidence against
the RFC text alone and is narrow enough in practice (tiny solid areas
are virtually always sent as Fill instead) to leave unhandled for now
-- it fails that one rectangle's decode cleanly rather than
misinterpreting it silently. The Gradient filter is implemented from
the spec description but is the least exercised of the three in this
pass.

Live-verified against the TightVNC test server that nothing
regressed; that server still consistently chose Raw for actual
framebuffer content regardless of announced encodings, so Tight's live
decode path isn't independently confirmed against a real server here
either -- the unit tests are the primary evidence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 21:13:17 -06:00
ksmithandClaude Sonnet 5 bb022edcf2 Add VNC ZRLE decoding
Implements RFC 6143 SS7.7.6: a ZRLE rectangle is a 4-byte compressed
length followed by that many zlib-compressed bytes, decompressing to
64x64 tiles each using one of five subencodings (Raw, Solid, packed
palette, Plain RLE, Palette RLE). The zlib stream persists for the
whole connection rather than being reset per-rectangle or per-update,
so VncSessionBackend now owns a lazily-initialized, persistent
z_stream torn down only in resetProtocolState() on a fresh
connect/reconnect.

Since the entire rectangle's compressed data decompresses into memory
in one shot, tile parsing is a plain synchronous loop rather than
needing its own RfbState values -- only the compressed-length and
compressed-data reads are actual protocol states. Tile decoding (the
five subencodings, including the continuation-byte run-length
encoding shared by two of them) lives in vnc_pixel_codecs.h/.cpp
alongside the Hextile decoder, unit-tested with 6 new tests covering
each subencoding plus a persistence test that splits one continuous
deflate stream across two separate FramebufferUpdate messages -- it
only decodes correctly if the connection's inflate stream is retained
between them.

Adds a top-level find_package(ZLIB REQUIRED) + ZLIB::ZLIB link
(previously only pulled in transitively via vendored FreeRDP's own
smartcard-emulation feature, which happened to have it enabled but
shouldn't be relied on for that).

Live-verified against the TightVNC test server that nothing regressed
(connect, cursor, clipboard); that server consistently sends Raw for
actual framebuffer content regardless of announced encodings, so
Hextile/ZRLE's live decode path isn't independently confirmed against
a real server -- the unit tests are the primary evidence here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 21:02:26 -06:00
ksmithandClaude Sonnet 5 e49fa0cf26 Add VNC Hextile decoding
Implements RFC 6143 SS7.7.4: rectangles announced as Hextile (type 5)
tile the update into 16x16 blocks, each either raw pixels or a
background fill plus an optional list of foreground/individually-
colored subrects, with background/foreground persisting across tiles
within one rectangle when not re-specified.

The pure byte-decode logic (tile metadata, subrect list) lives in new
src/vnc_pixel_codecs.h/.cpp, kept separate from
VncSessionBackend's wire-sequencing state machine so it's unit-testable
without a socket -- the pattern the plan calls for continuing into the
ZRLE/Tight work still ahead. Adds 5 fake-server tests covering a raw
tile, a background-only solid fill, uncoloured and individually-colored
subrects, and a 4-tile rectangle proving background persistence and
correct tile-cursor wraparound.

Verified against the live TightVNC test server (connect, frame,
cursor, clipboard all still work); that particular server always
chose Raw for the actual framebuffer content during this session, so
Hextile's real-world path isn't independently confirmed live -- the
unit tests are the primary correctness evidence for this phase.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 20:49:08 -06:00
ksmithandClaude Sonnet 5 9dd1af21d6 Add VNC remote cursor shape sync
Implements RFB's Cursor pseudo-encoding (RFC 6143 SS7.8.2, type -239):
a FramebufferUpdate rectangle carrying a cursor shape instead of
screen content (x/y are the hotspot, not position; width/height are
the cursor image size), decoded into an ARGB32 QImage using the
rectangle's RGB pixel data plus its opacity bitmask, then never
painted into the framebuffer. A 0x0 rectangle means "hide the
cursor" per spec.

VncDisplayWidget gains RdpDisplayWidget's setCursorImage/Hidden/
Default() + applyCursor() shape, reusing its own renderRect()/
effectiveRemoteSize() so cursor scaling works correctly in both the
scale-to-fit and actual-size display modes with no special-casing.
VNC never emits cursorReset() (RFB's Cursor pseudo-encoding has no
"reset to system default" signal, unlike RDP's SetDefault callback) --
setCursorDefault() exists for symmetry but is unused today.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 20:34:20 -06:00
ksmithandClaude Sonnet 5 3dd894407a Add VNC clipboard sync
RFB's ServerCutText/ClientCutText messages are much simpler than RDP's
CLIPRDR channel (no format-list/format-request negotiation -- text is
just sent directly in both directions), so ServerCutText now emits
remoteClipboardTextChanged instead of being discarded, and
VncSessionBackend overrides setClipboardText to send ClientCutText
immediately. Extends session_tab.cpp's clipboard-sync gate to cover
VNC alongside RDP; the QClipboard wiring itself was already
protocol-agnostic. Latin-1 only, per RFB's wire format -- no Unicode
clipboard extension is in scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 20:31:03 -06:00
ksmithandClaude Sonnet 5 35d4daec7f Fix VNC robustness gap: unannounced encodings now fail clearly, not silently
Previously an unrecognized rectangle encoding in a FramebufferUpdate
aborted the connection with a generic message, and there was nothing
tying the set of encodings we announce via SetEncodings to the set we
actually know how to decode. Introduces kAnnouncedEncodings as the
single source of truth for both, converts the rectangle dispatch to a
switch keyed off it, and gives the (still intentionally fatal --
there's no safe way to skip an unknown-length payload) fallback a
message that identifies it as a protocol violation rather than "not
supported". Adds a regression test asserting every announced encoding
has a working dispatch case, so future encodings (Hextile/ZRLE/Tight/
Cursor) can't be added to the announced list without matching decode
support. Also replaces scattered inline magic numbers for RFB
message-type constants with named constants, in prep for the
clipboard/cursor/compression work that follows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 20:27:18 -06:00
ksmithandClaude Sonnet 5 0b5454197c Add VNC-only scale-to-fit vs actual-size display toggle
VNC has no equivalent of RDP's MS-RDPEDISP to request a different
resolution from the guest, so a high-resolution remote desktop
previously always got shrunk to fit the window, making text
illegible. Adds a per-tab "Display Mode" choice (tab-bar right-click)
between the existing scale-to-fit behavior and a new actual-size mode
that renders the framebuffer at its native pixel size inside a
QScrollArea. Reuses VncDisplayWidget's existing scale-to-fit render
math unchanged -- it degenerates to an exact 1:1 mapping once the
widget is fixed to the remote's own size. Persisted like the terminal
theme preference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 18:13:43 -06:00
ksmithandClaude Sonnet 5 6da9dc6ca5 Add VNC support (Milestone 6, issue #3)
Implements RFB (RFC 6143) directly against QTcpSocket. No permissively
licensed VNC client library exists to vendor the way FreeRDP was for RDP:
LibVNCClient is GPLv2, gtk-vnc is LGPL but GTK-tied, and vendoring either
would force a licensing decision on the whole (MIT) project. This is
from-scratch protocol code instead, threaded like SshSessionBackend (a
QObject on its own QThread driven by Qt's own async socket signals)
rather than RdpSessionBackend's manual worker-thread/blocking-loop
pattern, since QTcpSocket is already async.

Scope, matching SessionTab's existing SSH/RDP dispatch pattern
(session_backend_factory.cpp, session_tab.cpp's widget construction and
signal wiring) and VncDisplayWidget mirroring RdpDisplayWidget's
scale-to-fit rendering:
- Protocol handshake: RFB 3.3/3.7/3.8 negotiated explicitly (the
  SecurityResult message only exists in 3.8; pre-3.8 servers signal auth
  failure by closing the socket, which the disconnect handler accounts
  for)
- VNC Authentication (DES challenge-response, via OpenSSL's classic DES
  API) and no-auth security types
- Raw + CopyRect framebuffer decoding into a persistent QImage, requesting
  a fixed 32bpp format whose byte layout matches QImage::Format_RGB32
  directly (same zero-conversion trick RdpSessionBackend uses for
  FreeRDP's GDI buffer)
- Keyboard (Qt key -> X11 keysym, including the Unicode-beyond-Latin-1
  keysym convention) and mouse/wheel input forwarding

Explicit non-goals for this pass (see docs/PROGRESS.md for the full
list): Apple's Screen Sharing auth (so this can't yet reach macOS's
built-in VNC server), compression encodings beyond Raw/CopyRect, dynamic
resize, remote cursor shape sync, clipboard sync.

19 unit tests (tests/test_vnc_session_backend.cpp): pure-function
coverage (DES key prep verified against an independently documented test
vector for password "COW", X11 keysym mapping, socket-error mapping) plus
state-machine coverage against a scripted in-process fake RFB server
covering all three protocol-version handshake shapes, auth success/
failure, unsupported security types, and pixel-accurate Raw decoding.
That harness caught a real re-entrancy bug: QAbstractSocket::abort()
synchronously re-emits disconnected() before returning, so
failConnection() calling it was silently letting a second, generic
disconnected-socket handler overwrite an already-correct, specific error
message.

Also verified live against a real, independently implemented VNC server
(TightVNC on Windows): connect with VNC Authentication, correct
framebuffer dimensions and pixel data, clean disconnect, reconnect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 18:02:49 -06:00
ksmithandClaude Sonnet 5 dbcc20d155 Add File -> Import from mRemoteNG...
Parses mRemoteNG's confCons.xml export format directly -- schema verified
against mRemoteNG's own XmlConnectionsDeserializer.cs source and a real
exported sample, not guessed at. Maps nested <Node Type="Container">
folders and <Node Type="Connection"> entries onto OrbitHub profiles:
RDP stays RDP, SSH1/SSH2 collapse to OrbitHub's single SSH protocol,
anything else (VNC, Telnet, HTTP, PowerShell, ...) is skipped and listed
in the import summary rather than silently dropped.

Passwords are never read, not even for the common case where they're
technically readable without a master password (mRemoteNG only encrypts
the Password attribute itself, everything else -- Hostname, Username,
Domain, Protocol -- is plaintext). A FullFileEncryption="true" export
encrypts the whole node tree instead and genuinely can't be read without
the user's master password; that case is detected and refused with a
clear message rather than failing confusingly.

The parser (src/mremoteng_importer.h/.cpp) is a pure function decoupled
from any file/UI I/O, matching this session's established pattern of
keeping business logic separately testable from the Qt Widgets shell that
calls it (ProfilesWindow::importFromMRemoteNG() is the thin wrapper:
QFileDialog, call the parser, write results via ProfileRepository, show a
summary). 11 test cases against realistic sample XML.

Hit a real moc gotcha along the way: a literal "//" inside a raw string
literal (the xmlns URL) makes moc's lexer think a line comment started
there, silently desyncing its parse so it never finds the QObject-derived
test class at all (no error, just a missing vtable at link time). Fixed
by moving the XML fixtures into a plain non-QObject header moc never
scans, split across two adjacent literals as a second safeguard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 16:08:02 -06:00
ksmithandClaude Sonnet 5 04ce6f7904 Add RdpSessionBackend test coverage (#1)
RdpSessionBackend can't reasonably get the same fixture-driven
state-machine tests SshSessionBackend got: it's driven by FreeRDP's own
event loop and a raw worker thread against a real freerdp_connect(), not
a QProcess we can point at a stand-in binary. What it does have is a
large amount of pure, regression-prone logic -- exactly the kind that
already caused a real historical bug here (the X11-keycode/PC-AT-scancode
mixup fixed in Milestone 7) -- so that's what gets covered instead.

Twelve functions promoted from free functions / private members to
public statics purely so tests can call them without a live connection:
security-mode/performance-profile normalization, the HiDPI scale-value
mapping, desktop-size clamping, both scancode-mapping functions, and the
five FreeRDP error-code interpretation functions. UINT32 is surfaced as
quint32 in the public signatures to keep FreeRDP/WinPR types out of the
header, matching how rdp_freerdp* is already only forward-declared there.

27 test cases, including a couple of direct regression guards: verifying
scancodeFromNativeScanCode() is a faithful passthrough to FreeRDP's X11
table (not a reimplementation), and that it does NOT reproduce the old
"X11 keycode treated as PC/AT scancode" bug for a documented example key.

This closes out #1's originally scoped work (CTest wiring, ProfileRepository,
SshSessionBackend, RdpSessionBackend coverage). Deeper state-machine
coverage for the two session backends remains future work if ever needed,
but isn't blocking here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:32:02 -06:00
ksmithandClaude Sonnet 5 9842a44de0 Add SshSessionBackend test coverage (#1)
Adds two kinds of coverage, continuing #1's remaining scope:

1. Pure-function tests for mapSshError() and escapeForShellSingleQuotes(),
   promoted from private members to public statics purely so tests can
   call them without spinning up a process. escapeForShellSingleQuotes()
   is the actual security boundary for password auth (it's what stops a
   password containing a single quote from breaking out of the askpass
   script's quoting), so it gets a real adversarial test, not just a
   happy-path one.

2. State-machine tests (connect -> Connected, auth failure -> Failed with
   the right mapped message, connection refused -> Failed, input
   round-tripping, reconnect) driven against tests/fixtures/fake_ssh.sh,
   a small controllable stand-in for the real ssh binary, instead of a
   real network/SSH server. This needed one small testability seam: a new
   constructor overload that overrides the launched program ("ssh" in
   production, the fixture script in tests).

POSIX-only for now: the fixture is a shell script, so the state-machine
tests QSKIP on Windows until an equivalent fixture exists there; the
pure-function tests run everywhere.

RdpSessionBackend coverage is still open -- it's a bigger lift again
(FreeRDP's own event loop, not just a QProcess), left for a follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:16:32 -06:00
ksmithandClaude Sonnet 5 eb63bde870 Add Delete Folder to the profile tree context menu
ProfileRepository::deleteFolder() removes a folder without ever deleting
the profiles or subfolders inside it -- everything directly under the
deleted folder shifts up to take its place (its parent, or the top level
if it had none), exactly as if that one path segment were removed from
each affected path. This is a labels-only operation (a "folder" is just a
grouping string on each profile, not a container that owns them), so
that's the least-surprising behavior versus silently bulk-deleting saved
connections.

Right-clicking a folder in the tree now offers "Delete Folder"; if it
isn't empty, a confirmation states exactly how many profiles/subfolders
will move and to where.

Covered by 7 new unit tests, which caught the same class of bug fixed in
3fab2f9: the new code's own folder-path remap also bound an unguarded
null QString (a folder moving to root) against the `folder_path NOT
NULL` column.

Fixes #20.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 15:08:37 -06:00
ksmithandClaude Sonnet 5 3fab2f9de3 Wire up CTest and add unit test coverage for ProfileRepository
Adds a Qt6::Test-based unit test target (tests/test_profile_repository.cpp,
21 cases), gated behind an ORBITHUB_BUILD_TESTS option that no-ops
gracefully if Qt6::Test isn't available, so it can't break app-only
builds. Covers profile CRUD, validation rules, search/sort, tag
normalization, and folder handling, each against an isolated temporary
SQLite file (new ProfileRepository(databasePathOverride) constructor
overload added for exactly this).

Caught and fixed a real bug along the way: normalizedTags()'s result was
bound directly without the nonNullTrimmed() null-guard every other field
already uses, so creating a profile with no tags at all hit the `tags
NOT NULL` constraint and silently failed -- including via the Import
Profiles feature for any export where a profile has no tags key.

Partial progress on #1 (RdpSessionBackend/SshSessionBackend state-machine
coverage still open -- much larger lift, needs a testability pass on
those backends first).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 06:39:20 -06:00
ksmithandClaude Sonnet 5 4f5cf8ecd9 packaging: read project version after building, not before
build-deb.sh and build-dmg.sh both read VERSION from CMakeCache.txt
before calling cmake --build -- but that build step is exactly what
reconfigures CMakeCache.txt if CMakeLists.txt changed since the build
dir was last configured. If the version was bumped and the script is
run without an explicit reconfigure first, it silently packages the
stale version (observed: v2026.9.15's macOS build produced
OrbitHub-2026.9.14.2.dmg). Move the VERSION read to after the build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 06:26:15 -06:00
ksmithandClaude Sonnet 5 df99c78998 Bump version to v2026.9.15
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v2026.9.15
2026-09-15 06:22:53 -06:00
ksmithandClaude Sonnet 5 dffca3afef Add Import/Export for profile lists
File -> Export Profiles... writes all profiles (plus explicit, including
empty, folders) to a JSON file. File -> Import Profiles... reads one back,
recreates folders, and inserts profiles as new rows so IDs never collide
with the destination database. No credentials are ever persisted on a
Profile in the first place (only privateKeyPath, a filesystem path), so
nothing sensitive is exposed by an exported file.

Verified with a standalone headless round-trip test against isolated
app-data databases (SSH + RDP profiles, nested folders, all fields);
caught and fixed a bug where import was redundantly creating an explicit
folder row per profile that didn't exist in the original export.

Fixes #17.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 06:22:26 -06:00
ksmithandClaude Sonnet 5 4f8fa3272b packaging: pin vendored FreeRDP's own version, stop git-tag bleed-through
third_party/FreeRDP is vendored in-tree rather than as a separate git
submodule, so its build-time git_get_exact_tag() call (in
cmake/GetProjectVersion.cmake) was resolving against OrbitHub's own git
tags instead of any real FreeRDP release tag. Its version-extraction regex
then greedily matched the last three dot-separated numbers of our tag
(e.g. v2026.9.14.2 -> "9.14.2"), mislabeling FreeRDP's own libraries in
packaged builds (libfreerdp9.so.9.14.2 instead of the real
libfreerdp3.so.3.23.1) -- self-consistent within a build, but misleading
and drifting release to release.

Adding .source_tag makes GetProjectVersion.cmake take its file-based
branch (which it already prefers over the git-tag branch) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v2026.9.14.2
2026-09-14 22:00:51 -06:00
ksmithandClaude Sonnet 5 96c8403f3b Bump version to v2026.9.14.2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:57:43 -06:00
ksmithandClaude Sonnet 5 3e621219f1 RDP: fix resize corruption by proactively resizing the local GDI buffer
update->DesktopResize (the only place gdi_resize() was called) only fires
during a full Deactivation-Reactivation sequence or a GFX ResetGraphics
PDU, neither of which our Display Control channel resize path (SendMonitorLayout,
MS-RDPEDISP) triggers. The client's own display buffer was left stuck at
its initial-connect size for the rest of the session, and FreeRDP's surface-bits
handling silently drops updates outside those stale bounds -- producing
the missing/misplaced taskbar and stale composited-looking content.

sendDisplayResize now calls gdi_resize() itself right after a successful
SendMonitorLayout, rather than waiting on a callback that structurally
never fires for this channel/codec configuration.

Fixes #18.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:57:08 -06:00
ksmith e0969041a7 packaging: pin Flathub manifest to v2026.9.14 About-dialog-fix commit 2026-09-14 21:32:56 -06:00
ksmithandClaude Sonnet 5 c4a62e8fb6 About dialog: fix unreadable dark-mode text and missing version
The subtitle and version line used palette(mid), a role meant for 3D
bevel/shadow decoration, not text -- it has poor contrast against the
window background in dark themes. Replaced with a color blended from
the widget's actual text/window palette colors, so it stays readable
(de-emphasized but never low-contrast) in either theme.

Also: QCoreApplication::applicationVersion() was never being set
anywhere, so the dialog always showed "Development build" regardless
of the actual built version. CMake's PROJECT_VERSION is now exposed
to the app via a compile definition and wired into
setApplicationVersion() at startup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v2026.9.14
2026-09-14 21:29:36 -06:00
ksmithandClaude Sonnet 5 9a22597d4e packaging: pin Flathub manifest to v2026.9.14
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:17:33 -06:00
ksmithandClaude Sonnet 5 e90e9b5abf Bump version to v2026.9.14
RDP HiDPI text fix and resize-glitch mitigations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:17:18 -06:00
ksmithandClaude Sonnet 5 92d8b62820 RDP: fix distorted text on HiDPI monitors, reduce resize glitches
The RDP session pipeline never accounted for display scale: it
requested a desktop canvas sized in Qt logical pixels (never
multiplied by devicePixelRatio()), and FreeRDP_DesktopScaleFactor/
DeviceScaleFactor were read but never actually set anywhere. On a
HiDPI monitor this meant the remote session rendered assuming a
96 DPI / 100% display, and the resulting canvas got stretched
locally — ClearType's subpixel hinting doesn't survive that kind of
resampling, producing distorted glyph shapes and color fringing
rather than plain blur.

RdpDisplayWidget now reports physical pixel dimensions and the real
devicePixelRatio (recomputed on resize and on screen changes, e.g.
dragging the window to a different-DPI monitor). RdpSessionBackend
maps that to the nearest FreeRDP-legal scale value ({100, 140, 180},
per MS-RDPEDISP and FreeRDP's own reference client) and sets it at
both connect time and on every dynamic resize, including the
FreeRDP_MonitorOverrideFlags required for the values to actually be
honored rather than silently ignored.

While testing this against real infrastructure, found and fixed two
related (pre-existing, not caused by this change) resize issues:
- A stale-frame race where the old frame could be drawn at the wrong
  scale for a moment after a resize, before a correctly-sized one
  arrives — now the frame is cleared during that transition instead.
- No debounce on outgoing resize requests — every single resize event
  fired an immediate request to the server, which can visibly
  contribute to host-side redraw glitches during rapid layout churn
  (e.g. right after connecting). Coalesced into one request per burst,
  plus an explicit refresh-rect request after each resize completes
  as a best-effort nudge for hosts that don't fully repaint on their
  own.

A separate, deeper issue was also found during testing (the remote
guest's actual resolution sometimes not changing despite the resize
channel reporting success) and is tracked separately, not fixed here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:16:41 -06:00
ksmithandClaude Sonnet 5 7ee930693e packaging: pin Flathub manifest to v2026.9.8.3
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 18:32:09 -06:00
ksmithandClaude Sonnet 5 8e98c208c9 Bump version to v2026.9.8.3
Same-day patch release: adds the in-app User Guide and standalone
User Guide PDF.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v2026.9.8.3
2026-09-08 18:31:50 -06:00
ksmithandClaude Sonnet 5 ce1e40a12d Add an in-app User Guide and standalone PDF
Adds docs/USER_GUIDE.md, a 10-section end-user guide (getting
started, managing/organizing profiles, SSH and RDP connections,
session management, settings, troubleshooting). It's embedded into
the app binary via a Qt resource file and rendered by a new
Help -> User Guide window: a topic sidebar plus content pane, not a
single scrolling document, with cross-reference links between
sections routed to sidebar selection rather than relying on Qt's
Markdown importer's lack of heading anchors.

A separate, non-shipped tool (tools/user-guide-pdf/) renders the
same source to a standalone PDF via QTextDocument + QPrinter,
wrapped by packaging/docs/build-user-guide-pdf.sh. Kept fully
outside the main CMake target so Qt6::PrintSupport never becomes a
runtime dependency of the shipped app (confirmed via ldd). The PDF
itself isn't committed -- generated per release like the platform
installers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 18:17:21 -06:00
ksmithandClaude Sonnet 5 80bc50e54c packaging: fix license install path, document Flathub AI-policy risk
License files were installing to share/licenses/orbithub instead of
the path Flathub's own docs specify for this app
(share/licenses/org.darksingularity.OrbitHub, i.e. $FLATPAK_ID).
Also installs FreeRDP's and KodoTerm's bundled LICENSE files there
alongside OrbitHub's own, since previously only the latter was
installed at all.

docs/FLATHUB.md now documents two things found by checking Flathub's
current requirements directly rather than assuming prior packaging
work was sufficient: the vendored libvterm copy has no LICENSE file
at all (needs to come from upstream, not fabricated here), and
Flathub's Generative AI disclosure policy is a real, reviewer-
discretion acceptance risk for this project given its development
history — not something further packaging work resolves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:56:22 -06:00
ksmithandClaude Sonnet 5 1c66adb646 packaging: pin Flathub manifest to v2026.9.8.2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:27:47 -06:00
ksmithandClaude Sonnet 5 4c649f727f Bump version to v2026.9.8.2
Same-day patch release: RDP TLS certificate verification fix and
Flathub submission prep.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v2026.9.8.2
2026-09-08 16:27:22 -06:00
ksmithandClaude Sonnet 5 18f234105d packaging: fix Flathub linter findings
Runs Flathub's own flatpak-builder-lint against both manifests and
the metainfo, and fixes what it found:

- only-arches doesn't belong in the manifest itself (linter: manifest
  unknown property); it belongs in a separate flathub.json, which is
  what Flathub's own build infrastructure actually reads it from.
- Bumps the KDE runtime from 6.10 to 6.11 per the linter's outdated-
  runtime warning; verified the app still builds and launches against
  it before committing to the bump.

The one remaining linter finding, finish-args-ssh-filesystem-access,
is a deliberate policy flag rather than a bug — Flathub requires a
written justification for any ~/.ssh access in the submission PR,
which docs/FLATHUB.md now documents with precedent from already-
approved apps in the same situation.

Also confirms (via a real interactive test with xdotool) that the
private-key Browse button correctly triggers the desktop portal
file chooser inside the sandbox, closing the last open verification
item from the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:26:14 -06:00
ksmithandClaude Sonnet 5 186480dcf5 packaging: prepare Flatpak for Flathub submission
Adds a production manifest (packaging/flatpak/flathub/) using a
pinned git source instead of the local-dir source the dev manifest
uses, so Flathub's build infrastructure can reproduce the build
independently.

Narrows the dev and production manifests' filesystem permission from
--filesystem=home to --filesystem=~/.ssh (read-write, needed for SSH
known_hosts/config) after confirming in the actual sandbox that: SSH
already works there (provided by the KDE runtime base, no extra
packaging needed), RDP needs no filesystem access at all (its
certificate trust never touched disk even before today's fix), and
QFileDialog's private-key/export pickers route through the desktop
portal rather than needing static filesystem access.

Expands AppStream metainfo with bugtracker/vcs-browser URLs, a
developer block, a releases entry, an OARS content rating, and three
screenshots (profiles view, an active SSH session, an active RDP
session) using real test-system profiles.

Adds docs/FLATHUB.md tracking overall submission readiness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:19:09 -06:00
ksmithandClaude Sonnet 5 dd974c684a RDP: enforce TLS certificate verification (was fully disabled)
IgnoreCertificate was hardcoded TRUE, meaning FreeRDP's entire
certificate-verification pipeline was bypassed: every RDP server's
TLS certificate was silently accepted, including certificates that
had changed since a prior trusted connection to the same host. That
is precisely the scenario TLS verification exists to catch — an
active MITM presenting a different certificate was indistinguishable
from a legitimate server.

Switches to FreeRDP's own trust-on-first-use certificate store
(AutoAcceptCertificate) so first-time connections still connect
without a prompt, matching SSH's "accept-new" known-hosts policy.
Certificate changes now correctly refuse the connection by default,
via VerifyChangedCertificateEx, with a clear message (host, port,
old/new SHA256 fingerprints) surfaced through the existing
connection-failure event log rather than adding a new, redundant
logging path.

Also fixes CertificateCallbackPreferPEM, which handed the full PEM
certificate to the verify callbacks instead of a short fingerprint —
harmless while those callbacks were dead code, but would have
flooded the event log with multi-KB certificate dumps once actually
exercised.

Verified end-to-end against real infrastructure: first connection
trusts and stores the certificate silently, a simulated changed
certificate (server key swapped) is correctly refused with a clear
message, and restoring the original certificate reconnects normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:18:57 -06:00
ksmithandClaude Sonnet 5 8c56d489af docs: update latest checkpoint tag to v2026.9.8
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 14:20:07 -06:00
ksmithandClaude Sonnet 5 d7910f1631 docs: link the v2026.9.8 release
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 14:17:09 -06:00
ksmithandClaude Sonnet 5 68214db744 docs: record v0-m9-done tag
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v2026.9.8
2026-09-08 14:05:53 -06:00
ksmithandClaude Sonnet 5 6c8146e79c docs: close out Milestone 9 and document Windows/macOS packaging
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v0-m9-done
2026-09-08 14:04:59 -06:00
ksmithandClaude Sonnet 5 aa812b0da7 macOS: fix launch crash, missing dylibs, and dmg volume icon
The app crashed at launch with "cannot be opened because of a
problem" because macdeployqt invalidates the code signature when it
rewrites library load paths; re-sign (ad hoc) after macdeployqt runs.

Also fixes the underlying cause of a Library-missing crash: orbithub's
INSTALL_RPATH used the Linux linker token $ORIGIN, which dyld does not
understand, and the bundled FreeRDP/WinPR/KodoTerm dylibs installed to
a sibling lib/orbithub/ directory outside OrbitHub.app rather than
Contents/Frameworks, so they were never copied into the dmg at all.
Both are now APPLE-specific, matching macdeployqt's own layout
(Contents/Frameworks, @executable_path/../Frameworks).

build-dmg.sh now also sets a custom volume icon (via SetFile or the
fileicon brew formula, whichever is available) instead of the
generic disk image icon, degrading gracefully if neither is present.

Also includes the StartupWMClass desktop-file fix from the prior
commit's message, which was never actually staged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 14:00:46 -06:00
ksmithandClaude Sonnet 5 3840ea9f62 Linux: fix taskbar pin matching and launcher icon mismatch
Add StartupWMClass=OrbitHub to the desktop file so panels (GNOME
Shell, Cinnamon, KDE) can match the running window to the pinned
launcher instead of creating a duplicate taskbar entry.

Replace the hand-authored, stale launcher SVG with PNG icons
rendered directly from createOrbitHubAppIcon() at each hicolor
theme size, so the pinned/menu icon matches the actual app icon
shown in the running window, Windows .ico, and macOS .icns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:50:25 -06:00
ksmithandClaude Sonnet 5 7ce7260305 packaging: rename app ID to org.darksingularity.OrbitHub
The reverse-DNS app ID should reflect a domain we actually control.
Renames the desktop file, AppStream metainfo, icon, Flatpak manifest,
and macOS bundle identifier from io.orbithub.OrbitHub to
org.darksingularity.OrbitHub. Also fixes a stale homepage URL in the
metainfo file pointing at an old git host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:39:06 -06:00
ksmithandClaude Sonnet 5 48adcf33ee packaging: add macOS dmg build script
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:29:35 -06:00
ksmithandClaude Sonnet 5 21e7a39c53 docs: add screenshots section to README
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:23:32 -06:00
ksmithandClaude Sonnet 5 f126f4b55c Add Windows installer via Inno Setup
New packaging/windows/orbithub.iss packages the build directory's
top-level contents directly (orbithub.exe, *.dll, platforms/,
sqldrivers/) rather than going through cmake --install: vcpkg's own
applocal DLL deployment already drops every required runtime dependency
flat into build/ on every build (confirmed repeatedly this session), and
the existing private-runtime-lib install() rule uses an RPATH-style Unix
convention (lib/orbithub/) that doesn't work on Windows, where DLLs must
sit next to the exe. Produces a single OrbitHub-Setup-<version>.exe with
a Start Menu entry, optional desktop shortcut, and uninstaller.

build-installer.ps1 reads the project version from build/CMakeCache.txt
(same approach build-deb.sh already uses) and drives ISCC.exe.

Code signing is out of scope for now (no certificate available), so the
installer will trigger a SmartScreen warning until one is purchased.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:57 -06:00
ksmithandClaude Sonnet 5 4daf5cfc65 docs: close out Milestone 7 (cross-platform protocol hardening)
Linux, macOS, and Windows have all been validated against the current
feature set (SSH, RDP display/keyboard/clipboard/cursor, Tab forwarding,
key repeat, profile dialog, single-window UI), with the platform-specific
bugs found along the way fixed and tracked individually on the issue
tracker (#8-#16). VNC validation remains out of scope while Milestone 6
stays deferred.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:50 -06:00
ksmithandClaude Sonnet 5 0990049241 Fix Flatpak build issues: EOL runtime and icon naming mismatch
- Bump the KDE runtime/SDK from 6.8 (now end-of-life per Flathub) to the
  current stable 6.10.
- The .desktop file's Icon=orbithub didn't match the app ID
  (io.orbithub.OrbitHub), so Flatpak's export step skipped it entirely
  ("Icon referenced in desktop file but not exported"). Use the app-ID-
  matching icon name everywhere instead, and drop the now-redundant
  renamed orbithub.svg install() rule that existed only for the old name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:45 -06:00
ksmithandClaude Sonnet 5 6d5133e1bd Switch to date-based version scheme
Release versions now follow v<year>.<month>.<day>, with a .N suffix
appended only if a second release happens the same day (e.g. v2026.9.8,
then v2026.9.8.2 for a same-day follow-up). Replaces the placeholder
0.1.0 the project started with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:14:33 -06:00
ksmithandClaude Sonnet 5 9d88b74c04 Fix macOS icon showing as unrecognized-file placeholder
The .icns generated via Pillow's ICNS writer wasn't rendering in Finder
(showed the "unavailable" circle-slash placeholder instead), likely due
to Pillow's ICNS encoding not matching what Icon Services expects. Rebuilt
the file by hand instead, using Apple's documented icns binary format
directly (magic header + OSType-tagged PNG chunks: icp4/icp5/icp6 for
16/32/64px, ic07-ic10 for 128/256/512/1024px) with the exact same PNGs
already rendered from createOrbitHubAppIcon(), sidestepping Pillow's
ICNS writer entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 12:41:08 -06:00
ksmithandClaude Sonnet 5 49ee12f5a1 Fix macOS configure error: add BUNDLE DESTINATION for install(TARGETS)
CMake requires an explicit BUNDLE DESTINATION for install(TARGETS) when
the target has MACOSX_BUNDLE set, or configure fails with "install
TARGETS give no BUNDLE DESTINATION for MACOSX_BUNDLE executable target".
The RUNTIME clause still covers Linux/Windows; BUNDLE only applies when
the target actually is a bundle (macOS), so no platform guard is needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 12:37:47 -06:00