Skip to content

Architecture

Finnegan's Owner edited this page Jul 13, 2026 · 4 revisions

Architecture

Launcher is a Tauri v2 desktop application with a Rust backend and a Nuxt 4 / Vue 3 frontend.

Project Structure

launcher/
├── app/                          # Frontend (Nuxt 4)
│   ├── app.vue                   # Root: renders the main UI, or the console
│   │                             #   when running in a console-* webview window
│   ├── assets/css/main.css       # Tailwind CSS v4 design tokens
│   ├── components/               # Vue components
│   │   ├── BriefServerInfo.vue   # Server row in the list
│   │   ├── ConnectionInput.vue   # Reusable form input
│   │   ├── ConsoleLog.vue        # Native console window UI (stdout/stderr)
│   │   ├── InsertableDropdown.vue # Dropdown with inline creation
│   │   └── TrustCertModal.vue    # Certificate trust / cert-change dialog
│   ├── composables/              # Vue composables
│   │   ├── useConfirmRejectModal.ts # Imperative modal mounting
│   │   ├── useConsole.ts         # Subscribes a console window to its stream
│   │   ├── useConsoleTheme.ts    # Per-console light/dark toggle
│   │   ├── useTheme.ts           # Main-window theme
│   │   └── useZoom.ts            # Keyboard zoom (Cmd/Ctrl +/-/0)
│   ├── enums.ts                  # Shared frontend enums
│   ├── pages/                    # File-based routing
│   │   ├── index.vue             # Main server list
│   │   └── connections/[id].vue  # Connection editor
│   └── types.d.ts                # TypeScript interfaces
├── src-tauri/                    # Backend (Rust)
│   ├── src/
│   │   ├── main.rs               # Tauri commands and app setup
│   │   ├── connection.rs         # ConnectionStore, persistence, cert pins
│   │   ├── webstart.rs           # JNLP parsing, JAR downloading, Java process
│   │   ├── tls.rs                # Per-connection TLS cert pinning (TOFU)
│   │   └── console.rs            # Native console: streams stdout/stderr
│   ├── capabilities/
│   │   ├── default.json          # Main-window permissions
│   │   └── console.json          # Console-window permissions (console-*)
│   ├── Cargo.toml                # Rust dependencies
│   └── tauri.conf.json           # Tauri app configuration
└── nuxt.config.ts                # Nuxt configuration

Backend (Rust)

Tauri Commands

Commands are the IPC bridge between frontend and backend. They use rename_all = "snake_case", so the JavaScript side must use snake_case parameter names.

Command Returns Description
launch Result<String, String> Checks Java, verifies the server cert against the connection's pin, downloads JARs, launches the administrator. Takes a force flag that acknowledges a cache collision (see code 4) and overwrites the cache. Ok value is a JSON object with a code (see below)
set_pin Result<(), String> Stores the operator-approved leaf-cert fingerprint as the connection's pin
save Result<String, String> Saves a connection entry
delete Result<String, String> Deletes a connection
import Result<String, String> Imports connections from a JSON file (with duplicate detection)
load_connections String Returns all connections as JSON array
load_single_connection Result<Value, String> Returns one connection by ID
get_default_connectionentry Result<Value, String> Returns a default connection template
get_all_groups Result<Value, String> Returns all group names
get_all_engine_types Result<Value, String> Returns all engine-type names
console_subscribe Result<(), String> A console window subscribes to its output: replays the backlog, then streams live lines over a Channel
console_save Result<(), String> Writes the console contents to a chosen file path
get_launcher_info String Returns app version info

The launch Ok string carries a result code so the frontend knows what to do next:

Code Meaning
0 Administrator launched
2 First connect to this server — cert details returned for the trust prompt
3 Server certificate changed — cert details returned for a confirm-or-abort warning
4 Cache collision — the shared cache for this engine type and version holds jars whose hashes don't match what the server sent. Details (engine type, version, differing jars) are returned so the frontend can warn and, on confirmation, retry with force = true
-1 Error — msg describes it (e.g. Java not found, server unreachable)

Launch Flow

  1. Frontend calls launch with a connection ID and a progress Channel
  2. Backend runs a fail-fast java -version check (using the connection's Java Home, else java on PATH). If Java cannot run it returns code -1 with a "Java (with JavaFX) not found" message — before any network work
  3. Captures the server's leaf TLS certificate via a handshake to /webstart.jnlp and computes its SHA-256
  4. Compares that fingerprint to the connection's stored pin (pinnedCertSha256):
    • No pin yet → returns code 2 with cert details so the operator can trust it (TOFU)
    • Fingerprint differs from the pin → returns code 3 with cert details (the cert-changed warning)
    • Fingerprint matches → proceeds. On a trust prompt the frontend calls set_pin and retries launch
  5. Downloads the JNLP and JARs over a pinned rustls client that rejects any leaf whose SHA-256 does not match the pin, sending progress over the Channel. The vendor/version shared cache is reused, skipping downloads whose SHA-256 already matches. While checking the cache, a single pass classifies each cached jar as needs-download and/or foreign; if the version matches but jars are foreign, it returns code 4 (cache collision) unless force was passed
  6. Sanitizes JVM arguments (blocks dangerous flags like -javaagent:) and builds the classpath in the JNLP-declared order — it does not re-sort the jars. Preserving that order is what lets an engine's patched overlay jars (e.g. Mirth 4.7.x *-mc-modifications.jar) load ahead of their stock counterparts; sorting alphabetically loaded stock first and broke administrator login with an IllegalAccessError on org.mozilla.javascript.NativeDate
  7. Launches the JavaFX administrator, passing the username and password as trailing launch arguments to the main class
  8. If "Show console" is enabled, captures the process's stdout and stderr and opens a native console window — but only after the process spawns successfully, so a failed launch never pops an empty console

Data Storage

  • ConnectionStore manages all connections in a Mutex<HashMap<String, Arc<ConnectionEntry>>>
  • Persisted to ~/.launcher/launcher-data.json (written to a temp file, fsynced, then atomically renamed so a crash can't truncate it). Each entry carries its own pinnedCertSha256. On unix the data file is created 0600 and the ~/.launcher directory 0700, since the file can hold saved passwords; the permissions are re-applied on write in case a stale temp file predates the fix
  • JAR cache stored in ~/.launcher/cache/ (vendor/version shared cache)
  • Diagnostic logs in ~/.launcher/logs/
  • There is no separate trust-store file — the pin lives inline on the connection

Security Model

  • The trust boundary is per-connection TLS certificate pinning (trust-on-first-use), implemented in tls.rs. Integration engine servers commonly present self-signed certs, so chain validation is the wrong model
  • On first connect, the launcher captures the server's leaf-cert SHA-256 and prompts the operator to trust that fingerprint. The approved value is pinned to the connection (pinnedCertSha256)
  • On every later connect, the pinned rustls client accepts only a leaf whose SHA-256 equals the pin; a changed cert is rejected and surfaced as the cert-changed warning (code 3)
  • The verifier still validates the TLS handshake signature (proof the server holds the private key) via the ring crypto provider — pinning the public cert bytes alone would let anyone replay them
  • Hostname/SAN is intentionally not checked; the pin replaces it, which is what self-signed engine certs (which rarely carry a matching SAN) require
  • native-tls (vendored) is retained only for the connectivity status probe, which needs reqwest's acceptInvalidHostnames; the launch download path uses the pinned rustls client, not danger_accept_invalid_certs
  • JNLP version attributes are sanitized to prevent path traversal
  • JVM arguments from JNLP are filtered to block code execution flags (-javaagent:, -agentpath:, etc.)

Defense-in-depth measures added alongside the pinning core:

  • A restrictive Content-Security-Policy is set in tauri.conf.json (default-src 'self', script-src 'self', object-src 'none', base-uri 'self', frame-ancestors 'none', no remote connect-src). Tauri injects per-build hashes of the app's inline scripts so script-src 'self' still works. All icons are bundled locally (via the @nuxt/icon client bundle) so nothing is fetched from the internet at runtime and the CSP can stay tight
  • The connections file and ~/.launcher directory are created owner-only (0600 / 0700) on unix
  • Tauri v2's capability ACL gates only core and plugin commands, not application commands — any window can call a #[tauri::command]. So console_subscribe derives its target from the calling window's own label rather than a caller-supplied argument, which stops one console window from subscribing to another console's output

Native Console

When "Show console" is enabled, output is streamed to a dedicated Tauri webview window (label console-<id>) instead of a bundled second Java process. console.rs reads the administrator's stdout and stderr on background threads and pushes each line into a per-window buffer. A window that attaches late replays a capped backlog (MAX_BACKLOG_LINES) and then receives live lines over a Channel, with the buffer lock held across the handover so no line is lost or duplicated. The window closes automatically on a clean exit (status 0) and stays open on a non-zero exit so the error stays readable. Relaunching into an open console bumps a generation counter and shows a "relaunched" separator, so a superseded process's later exit can't flip a live console to "exited".

Tauri Permissions

The main window's capabilities are declared in src-tauri/capabilities/default.json:

Permission Purpose
core:default Core IPC and event system
dialog:default Native file picker and confirm dialogs
core:webview:allow-set-webview-zoom Keyboard zoom support
http:default HTTP requests (scoped to http/https URLs, for server connectivity checks)
shell:allow-open Open URLs in default browser (restricted to the wiki URL only)

Console windows have their own capability in src-tauri/capabilities/console.json (matched by the console-* window glob): core:default, dialog:default, clipboard-manager:allow-write-text, and core:webview:allow-set-webview-zoom.

Frontend (Nuxt 4 / Vue 3)

Design System

Tailwind CSS v4 with @theme design tokens in app/assets/css/main.css:

Token Value Usage
surface-0 #24272e Page background
surface-1 #2c2f37 Cards, inputs
surface-2 #353840 Hover, selected states
surface-3 #3e414a Highest elevation
accent #4f7df7 Primary actions
text-primary #e2e5eb Main text
text-secondary #9ca3b0 Supporting text

Icons use Phosphor Icons via the ph: prefix with the @nuxt/icon module.

Key Patterns

  • Channel API: launch uses tauri::ipc::Channel to stream progress from Rust to the frontend, and console_subscribe uses one to stream console output to a console window
  • Result-code loop: index.vue invokes launch in a bounded loop so a first-use trust (code 2) or cert-change (code 3) can show a modal, call set_pin, and retry, while a cert that keeps changing can't re-prompt forever
  • Imperative modals: useConfirmRejectModal mounts TrustCertModal programmatically for both the first-use trust prompt and the cert-change confirmation, returning a Promise
  • Console windows: each console is a separate Tauri webview window; app.vue detects a console-* window label and renders ConsoleLog.vue, which owns its own light/dark theme and auto-scroll independently of the main window
  • Module-level state: useZoom uses a module-scoped ref for app-wide singleton zoom state
  • CSS tooltips: Custom [data-tooltip] attribute with ::after pseudo-element (native title tooltips are unreliable in Tauri's WKWebView). Use data-tooltip-below for elements near the top of the window
  • localStorage persistence: Sort mode, collapsed groups, zoom level, and console theme all persist to localStorage

Error Handling Conventions

  • Rust: Prefer ? operator and .map_err() over .unwrap(). Return errors to frontend via Result<T, String>. Use .expect("descriptive message") only for mutex locks (poisoning is unrecoverable).
  • Frontend: Wrap invoke calls in try/catch and display errors inline via the error bar at the bottom of the main screen.

Clone this wiki locally