commit 65899ba66f41b3675400dd288a2c3e86cb93bd31 Author: Jeremy Anderson Date: Sun Aug 23 15:58:24 2026 -0400 **A kiosk lockdown browser built on LibreWolf (or Firefox ESR) + a Rust native helper.** Linux-first, single codebase, zero Chromium in the stack. Designed for medical lobbies, public terminals, and any environment where a browser must serve the public without leaking session data between users. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 0000000..fe6809b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,217 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + build-and-test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + vestibule/helper/target + key: ${{ runner.os }}-cargo-${{ hashFiles('vestibule/helper/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Build usher + working-directory: vestibule/helper + run: cargo build --release + + - name: Smoke test (Linux) + if: runner.os == 'Linux' + working-directory: vestibule + run: python3 scripts/test-native-messaging.py + + - name: Smoke test (Windows) + if: runner.os == 'Windows' + working-directory: vestibule + run: python scripts\test-native-messaging.py + + - name: Validate JSON + working-directory: vestibule + run: | + python3 -c "import json; json.load(open('extension/manifest.json'))" + python3 -c "import json; json.load(open('config/policies.json'))" + python3 -c "import json; json.load(open('config/com.vestibule.usher.linux.json'))" + python3 -c "import json; json.load(open('config/com.vestibule.usher.windows.json'))" + python3 -c "import json; json.load(open('packaging/net.dcos.Vestibule.json'))" + + - name: Validate XML + icon assets + working-directory: vestibule + run: | + python3 -c "import xml.dom.minidom; xml.dom.minidom.parse('packaging/net.dcos.Vestibule.metainfo.xml')" + python3 - <<'EOF' + import os + for f in ["packaging/icons/vestibule.ico", "packaging/icons/vestibule.svg"]: + assert os.path.exists(f) and os.path.getsize(f) > 0, f + print("icon assets ok") + EOF + + - name: Check JS syntax + working-directory: vestibule/extension + run: | + for f in *.js; do node --check "$f" || exit 1; done + + - name: URL policy unit tests + working-directory: vestibule + run: node scripts/test-url-policy.js + + - name: Check POSIX sh syntax + if: runner.os == 'Linux' + working-directory: vestibule/scripts + run: | + for f in *.sh vestibule-kiosk-launch; do sh -n "$f" || exit 1; done + sh -n ../packaging/vestibule-flatpak-cli + sh -n ../packaging/build-flatpak.sh + + - name: Check PowerShell syntax + if: runner.os == 'Linux' + working-directory: vestibule + shell: pwsh + run: | + $files = Get-ChildItem scripts/*.ps1, packaging/*.ps1 + foreach ($f in $files) { + $errs = $null + [void][System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$errs) + if ($errs) { + $errs | ForEach-Object { Write-Error "$($f.Name): $($_.Message)" } + exit 1 + } + } + Write-Host "PowerShell syntax OK ($($files.Count) files)" + + - name: Provision/deprovision integration test (rootless sandbox) + if: runner.os == 'Linux' + working-directory: vestibule + run: sh scripts/test-provision-linux.sh + + - name: provision --check fails with documented exit code (no prereqs on runner) + if: runner.os == 'Linux' + working-directory: vestibule + run: | + code=0 + sh scripts/provision-kiosk.sh --check || code=$? + # The runner has systemd but no cage/LibreWolf -> documented exit 3. + if [ "$code" -ne 3 ]; then + echo "expected exit 3 (missing prerequisites), got $code" + exit 1 + fi + echo "check mode returned documented exit code 3" + + - name: provision-kiosk.ps1 -Check fails fast off-Windows (exit 2) + if: runner.os == 'Linux' + working-directory: vestibule + shell: pwsh + run: | + $code = 0 + try { & pwsh -NoProfile -File scripts/provision-kiosk.ps1 -Check } catch { $code = 1 } + if ($LASTEXITCODE -ne 2) { + Write-Error "expected exit 2 (unsupported platform), got $LASTEXITCODE" + exit 1 + } + Write-Host "Windows provisioner correctly refuses to run on Linux (exit 2)" + + package-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + vestibule/helper/target + key: ${{ runner.os }}-cargo-${{ hashFiles('vestibule/helper/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Install Inno Setup + shell: powershell + run: choco install innosetup -y --no-progress + + - name: Build installer + XPI + working-directory: vestibule + shell: powershell + run: | + powershell -NoProfile -ExecutionPolicy Bypass -File packaging\build-installer.ps1 + # Standalone XPI artifact for policy-based manual deploys. + Compress-Archive -Path extension\* -DestinationPath vestibule-1.2.2.xpi -Force + + - name: Upload installer artifact + uses: actions/upload-artifact@v4 + with: + name: vestibule-setup-windows + path: | + vestibule/packaging/Output/Vestibule-Setup-1.2.2.exe + vestibule/vestibule-1.2.2.xpi + if-no-files-found: error + + package-linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + vestibule/helper/target + key: ${{ runner.os }}-cargo-${{ hashFiles('vestibule/helper/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Build usher (Linux binary) + working-directory: vestibule/helper + run: cargo build --release + + - name: Upload usher binary + uses: actions/upload-artifact@v4 + with: + name: vestibule-usher-linux + path: vestibule/helper/target/release/usher + if-no-files-found: error + + - name: Build Flatpak bundle + working-directory: vestibule + run: | + flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo + rm -rf packaging/repo packaging/builddir + flatpak-builder --user --install-deps-from=flathub --force-clean \ + --repo=packaging/repo packaging/builddir packaging/net.dcos.Vestibule.json + flatpak build-bundle packaging/repo packaging/Vestibule-1.2.2.flatpak \ + net.dcos.Vestibule 1.2.2 + + - name: Upload Flatpak artifact + uses: actions/upload-artifact@v4 + with: + name: vestibule-flatpak-linux + path: vestibule/packaging/Vestibule-1.2.2.flatpak + if-no-files-found: error diff --git a/BLOG.md b/BLOG.md new file mode 100755 index 0000000..1f8f649 --- /dev/null +++ b/BLOG.md @@ -0,0 +1,277 @@ +# From IEXPLORE.EXE to Rust: Rewriting a 2001 Kiosk Browser + +**By Jeremy Anderson** — [dcos.net](https://dcos.net) — info@dcos.net + +In December 2001, I was eighteen, coding on co-op while in school +for my first employer — a Boston-area MSP and programming company. +The company serviced medical offices, and one recurring problem was +the lobby computer: patients waiting for appointments would sit down +at it and immediately start looking for things they should not be +looking at, or worse, closing the browser and poking around the rest +of Windows. The receptionists were tired of babysitting it. My job +was to make the computer bulletproof enough to leave alone. + +I shipped a solution in Visual Basic 6. It was, in retrospect, a +remarkable little artifact of late-90s teenage ingenuity — and +twenty-five years later, I decided to rewrite it properly. + +## What the original did + +The genius of the 2001 version was brute force. It embedded the +Internet Explorer COM control (`SHDocVw.dll`) in a maximized VB6 form +with `ControlBox = False` and `WindowState = 2` (maximized). It called +`SetWindowPos` with `HWND_TOPMOST` to keep the window on top of +everything. And — this is the part that made it actually work — it +called `SystemParametersInfo(97, True)`, the legendary undocumented +`SPI_SCREENSAVERRUNNING` flag that convinced Windows 9x it was running +a screensaver, which had the side effect of disabling Ctrl+Alt+Del, +Alt+Tab, and the Windows key. + +That alone was not enough. A determined user could still close the VB6 +app and reach the desktop. So the app did something beautifully +ruthless: on startup, it copied `C:\Program Files\Internet +Explorer\IEXPLORE.EXE` to `IEXPLORE.bak`, then deleted the original. +If the user managed to escape the kiosk, they would find no browser to +escape to. On unlock, the app copied the backup back into place. + +The unlock code lived in plaintext — hardcoded in the form source of +the lobby build, read from `C:\windows\system\smt.txt` by the +earliest iteration. The UI was black background, +white text, Comic Sans. The menu was invisible by default and appeared +only when the mouse touched the top strip of the screen. The +delay loop before unlock was a raw `GoTo` counting loop, a CPU-burner +that pegged the processor at 100% for half a second. + +The scrubbed source of all three iterations ships with this repository +under `history/vb6-2001/`. + +It ran on Windows 95, 98, 98 SE, and ME. It explicitly refused to run +on NT, 2000, or XP because `SPI_SCREENSAVERRUNNING` does nothing on +NT-family kernels. + +It worked. It ran in that lobby for years. + +## Why rewrite it + +The original program is unusable today for a list of reasons: + +- It is VB6. The toolchain is dead. The runtime ships in Windows 11 + only for legacy compatibility, and Microsoft has announced its + removal. +- It depends on `SHDocVw.dll`, which is Internet Explorer. IE is gone. +- `SPI_SCREENSAVERRUNNING` was a Win9x-only trick. Modern Windows + (NT kernel) ignores it entirely. +- The IEXPLORE.EXE shell game is impossible on modern Windows due to + filesystem permissions and Windows Resource Protection. +- Plaintext unlock-code storage — hardcoded in the form source, or a + plaintext file on disk — is indefensible. +- The Comic Sans UI is accessibility-hostile. + +But the core idea — a public-facing browser that holds the perimeter +and never leaks session data between users — is more relevant than +ever. Medical lobbies still have lobby computers. Libraries still have +public terminals. Trade shows still have demo kiosks. The problem +shape has not changed; only the implementation needs to. + +## The architectural decisions + +A modern kiosk browser has to answer four questions: + +1. **What renders the web content?** +2. **How is the perimeter enforced?** +3. **How is the operator UI protected?** +4. **How is session data sanitized?** + +### Rendering: LibreWolf ESR + +The constraint was no Chromium. Tauri was rejected because on Windows +it uses WebView2, which is Edge, which is Chromium. Electron was +rejected for the same reason and for its 150 MB binary size. Servo was +rejected because its web compat is still too inconsistent for +arbitrary patient-portal content. + +LibreWolf ESR is the right answer. It is Gecko-based (no Chromium), +MPL 2.0 licensed, community-maintained, tracks Firefox ESR for +security, has Mozilla telemetry stripped, and ships enterprise +`policies.json` support for lockdown. It also navigated Mozilla's +trademark policy already, which means redistribution is straightforward. + +### Perimeter: OS-level lockdown, not app-level + +The original VB6 fought the OS from inside the app. It used Win32 API +calls to disable Ctrl+Alt+Del. It renamed system executables. This was +necessary on Windows 9x because the OS had no kiosk mode. + +Modern operating systems do. Windows 10+ has AssignedAccess, which +configures a device as a dedicated kiosk at the OS level — single +auto-logon account, no shell, no escape. Linux has `cage`, a 3 MB +wlroots-based compositor that boots straight into a single client with +no window manager, no shell, no desktop. + +The right architecture moves the perimeter enforcement out of the app +and into the OS, where it belongs. Vestibule does not try to disable +Ctrl+Alt+Del from inside the browser. AssignedAccess does that at the +session level, far more reliably than any app-level hook ever could. + +### Operator UI: extension + Rust helper + +The browser-level logic — URL filtering, hidden menu, session reset, +unlock dialog — lives in a WebExtension. This is the natural unit of +composition for Firefox-family browsers and gives us `webRequest` +blocking, `browsingData` sanitization, and `idle` detection for free. + +The OS-level logic — password verification, power event detection, +signaling the supervisor — lives in a tiny Rust binary called `usher`. +It speaks Firefox Native Messaging (a stable, supported stdio-JSON +protocol) to the extension. The binary is 1.2 MB. The extension is +6 KB. Together they replace what would have been a 150 MB Electron +bundle. + +### Session sanitization: three layers, defense in depth + +The product contract is: a kiosk must never persist patient data +across sessions. Three enforcement layers: + +1. **Block all save paths.** `policies.json` disables form history, + password manager, master password creation, Pocket, Screenshots, + Firefox Accounts, search suggestions, extension installs, popup + blocking override, and protocol handler registration. + `SanitizeOnShutdown` wipes everything on browser exit. + +2. **Reset on trigger events.** The extension's `resetSession()` + invokes `browser.browsingData.remove()` with all eleven data types + on idle timeout, wake-from-sleep, unlock, and manual reset. + +3. **OS-level defense.** AssignedAccess and `cage` ensure the browser + cannot be closed or escaped. The dedicated `vestibule-profile` + directory isolates the kiosk from any user profile data. + +## The 2001 gestures that survived + +Two original design decisions carry forward unchanged. + +**The top-edge hidden menu.** The original VB6 form toggled its menu +in `picAddress_MouseMove`, based on `Y <= 10` (twips — effectively the +top edge of the screen). The modern equivalent is a content script +that listens for `mousemove` and shows a fixed-position overlay when +`clientY <= 3` pixels. Same gesture. Same UX rationale: the menu is invisible +by default, appears on a deliberate gesture, and disappears on +mouse-leave. Patients never see it. Operators always find it. + +**The password unlock.** The original prompted for an unlock code and +compared it to the contents of `smt.txt`. The modern equivalent +prompts for an unlock code and verifies it against an Argon2id hash +stored at 0600 by usher. The flow is identical; the +storage and verification are not. + +## What did not survive + +**The IEXPLORE.EXE shell game.** Modern Windows filesystem permissions +make this impossible. The replacement is a URL allowlist enforced via +`webRequest.onBeforeRequest` in the extension. Stronger, safer, and +does not require renaming system executables. + +**The CPU-melting delay loop.** The original used a `GoTo` counting +loop (`beginn: time = time + 1 ... GoTo beginn`) to burn half a second +before unlock. The modern +equivalent is `tokio::time::sleep(Duration::from_millis(500))`. Same +delay, 0% CPU. + +**Plaintext password storage.** usher stores the Argon2id hash +(64 MiB memory cost) in a file owned and readable only by the kiosk +user — mode 0600 on Linux, default user ACL on Windows. Keyring +daemons are unavailable on minimal kiosk compositors like cage, so +file permissions are the enforcement boundary. The password itself is +never written anywhere. + +**The Comic Sans UI.** The hidden menu uses system fonts +(`-apple-system, "Segoe UI", system-ui, sans-serif`). A retro 2001 +theme is a Phase 5 option for those who want the nostalgia. + +## Power state awareness + +The original VB6 did not handle sleep because Windows 9x kiosks +typically did not sleep. Modern hardware does. Laptops used as kiosks +suspend on lid close. Mini-PCs suspend on idle. Tablets suspend on +inactivity. + +Vestibule subscribes to the OS's power events. On Linux, usher +connects to D-Bus and listens for +`org.freedesktop.login1.Manager.PrepareForSleep(false)`, which fires +about two seconds after the system wakes. On Windows, usher registers +for `WM_POWERBROADCAST` and listens for `PBT_APMRESUMEAUTOMATIC`. + +On wake, usher emits a `Wake` event to the extension, which calls +`resetSession("wake:suspend")`. The same path fires on idle timeout. +Both go through one code path, so behavior is identical regardless of +trigger. + +The decision to detect rather than block sleep is deliberate. +Blocking sleep fights the OS — it causes battery drain on mobile +kiosks, thermal issues on fanless hardware, and interferes with +Windows Update overnight reboots. The OS power profile is the right +place to decide whether a device sleeps; that is a deployment-time +decision, not an app-runtime one. Vestibule reacts correctly when the +OS does sleep, and stays out of the way when it does not. + +## What I learned doing this + +The 2001 version took me about three weeks of evenings. I knew nothing +about Win32 API calls, COM interop, or filesystem security. I learned +all of it by reading `Declare Function` examples on Planet Source +Code and copying patterns I did not fully understand. The code was +procedural, repetitive, and full of `On Error GoTo` cascades that +existed to patch around the IEXPLORE.EXE rename failing halfway. It +worked because the problem was small and the OS was permissive. + +The 2026 version took about a week of focused work. I know Rust, I +know Firefox extension APIs, and I know what the OS primitives +actually do. The code is threaded, table-driven, and structured around +a message-passing channel that makes the data flow obvious. It works +because the abstractions are right, not because I patched around +enough edge cases. + +The instinct that survived intact is the one the original reviewer +called out: dig underneath the standard UI and manipulate the +environment directly. The 2001 version did this with Win32 API calls. +The 2026 version does it with D-Bus subscriptions, enterprise policy +files, and a message-passing channel between browser and helper. The +implementation is completely different. The instinct is the same. + +## What comes next + +Vestibule 1.2 ships the working core: real Argon2id unlock with file +storage at 0600, a dedicated unlock popup window, Windows power event +detection, the `librewolf.overrides.cfg` that selectively relaxes +LibreWolf's resistFingerprinting for SSO flows, per-origin preservation +for the data persistence allowlist — and, on top of that, the OS-level +provisioning wizards (an AssignedAccess setup wizard on Windows with a +Shell Launcher step-down, a `cage` systemd unit on Linux where the unit +IS the graphical session — no display manager at all), full +deprovisioning on both platforms, and the packaging itself: Inno Setup +on Windows and a Flatpak on Linux that acts as a self-describing +deployment kit. Since 1.2.2 the perimeter also extends to the web +itself: the URL policy blocks every domain by default and opens only +what the operator safelists — domain-based matching, with the home +page guaranteed reachable so the kiosk can never lock itself out. + +Phase 3 adds crash auto-restart integration testing, telemetry hooks, +and the `always-on` power-inhibit mode. Later phases: Flathub +distribution, remote management, and the retro 2001 theme. + +One platform decision is already settled: Vestibule targets Linux +first. The Windows stack works and stays CI-validated, but its +binaries ship unsigned — a code-signing certificate is an unfunded +line item unless a donation covers it — and Linux has no signing +gate to care about. + +Phase 5+, deferred: webcam presence detection. When a webcam is +present, usher captures a baseline face embedding at session start and +watches for two conditions: no face visible for N seconds (user +walked away) or face embedding mismatch (different user approached). +Either triggers an immediate session reset, faster than the idle +timeout. Privacy review precedes any biometric code. Frames are +processed in-memory only; embeddings never leave the local process; +the webcam LED is respected. + +*Jeremy Anderson, 2026. [dcos.net](https://dcos.net). info@dcos.net.* diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..c9597d9 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,381 @@ +# Deployment — Vestibule + +This is the operator runbook for turning a stock machine into a Vestibule +kiosk: OS-level lockdown provisioning and packaging, on both platforms, +with one-command deprovision. + +``` +What provisioning configures (Windows) What it configures (Linux) +──────────────────────────────────────── ────────────────────────── +AssignedAccess single-app kiosk (MDM bridge) cage Wayland session on a VT + └ step-down: Shell Launcher (Ent/Edu) └ /etc/systemd/system/ +Start Menu shortcut with stable AUMID vestibule-kiosk.service +Dedicated kiosk local account /usr/local/bin/usher +Automatic logon (registry) /usr/local/bin/vestibule-kiosk-launch +C:\ProgramData\Vestibule\kiosk.env /etc/vestibule/kiosk.env +policies.json (extension force-installed) policies.json (same mechanism) +Per-user bootstrap at kiosk logon Native Messaging manifests for +(kiosk-launch.ps1: usher, HKCU host the kiosk user + registration, profile, crash restart) /opt/vestibule staging tree +``` + +Everything the scripts do is reversible — see +[Deprovision](#deprovision) for the one-command exit on each platform. + +**Platform priority: Linux.** The Linux path is the supported +production target — provisioned, packaged, and installed with no +signing gate. The Windows path is complete and CI-validated but ships +unsigned: a code-signing certificate is an unfunded line item and +stays that way unless a donation earmarks it (info@dcos.net), so +SmartScreen warns on the installer and the usher binary. Windows +operators verify the SHA-256 from the release notes; Linux operators +install and go. + +--- + +## Choosing a path + +| Situation | Command | +|---|---| +| Linux kiosk, native LibreWolf | `sudo scripts/provision-kiosk.sh --librewolf native` | +| Linux kiosk, Flatpak LibreWolf | `sudo scripts/provision-kiosk.sh --librewolf flatpak` | +| Linux kiosk, native Firefox | `sudo scripts/provision-kiosk.sh --firefox native` | +| Linux kiosk, Flatpak Firefox | `sudo scripts/provision-kiosk.sh --firefox flatpak` | +| Windows kiosk, interactive | Run the installer, tick the wizard checkbox — or `provision-kiosk.ps1` standalone | +| Windows kiosk, unattended (MDM/CI) | `Vestibule-Setup-1.2.2.exe /VERYSILENT` then `provision-kiosk.ps1 -Quiet` (exit codes below) | +| Just want to see what it would do | `--check` on either platform | + +## Browser support + +Vestibule runs on two Gecko browsers. The extension, usher, and +lockdown policies are identical for both — only the deployment paths +differ. + +| Browser / flavor | Windows | Linux native | Linux Flatpak | Linux snap | +|---|---|---|---|---| +| LibreWolf | Program Files install, `distribution\policies.json` | install-dir `distribution/` | system Flatpak (policies in app dir) | — | +| Firefox / ESR | Program Files install, `distribution\policies.json` | distro package → `/etc/firefox/policies` | system Flatpak (policies + XPI in kiosk home) | Ubuntu snap (policies + XPI in kiosk home) | + +Notes: + +- **Auto-detect order**: LibreWolf native → LibreWolf Flatpak → Firefox + native → Firefox Flatpak → Firefox snap. Override with + `--librewolf`/`--firefox` (Linux) or `-Browser` (Windows). +- **Firefox ESR is recommended** for kiosks — slower release cadence, + same enterprise-policy engine. +- **Flatpak/snap Firefox keep policies and the XPI in the kiosk user's + home** (`~/.var/app/...` / `~/snap/firefox/common`), so they survive + browser updates — unlike LibreWolf's system-Flatpak deploy. +- **Trademark**: Vestibule configures an existing Firefox install; it + never downloads or redistributes Firefox. (The original choice of + LibreWolf as the default base was about redistribution — deploying + against an installed Firefox has no trademark exposure.) +- `librewolf.overrides.cfg` is LibreWolf-only; on Firefox it is skipped + with a notice (Firefox ignores it safely anyway). + +--- + +## Windows + +### Prerequisites + +- Windows 10/11 **Pro, Enterprise, or Education** (Home has no + AssignedAccess/Shell Launcher — the script fails fast with exit 2) +- LibreWolf **or Firefox/ESR** installed (`C:\Program Files\...` — + auto-detected; override with `-Browser librewolf|firefox`) +- The Vestibule installer run (or a repo checkout with a built usher: + `cd helper; cargo build --release`) +- PowerShell run **as administrator** + +### Interactive provisioning + +```powershell +powershell -ExecutionPolicy Bypass -File scripts\provision-kiosk.ps1 +``` + +The wizard prompts for the home URL and generates the kiosk account +password. Everything else is automatic: staging, XPI build, policies +merge, shortcut + AUMID, AssignedAccess, autologon. Exit code 10 means +"reboot to activate". + +### Unattended provisioning + +```powershell +powershell -ExecutionPolicy Bypass -File scripts\provision-kiosk.ps1 ` + -KioskUser Kiosk ` + -KioskPassword '' ` + -HomeUrl https://checkin.example.org ` + -Quiet -Restart +``` + +Parameters: + +| Parameter | Default | Meaning | +|---|---|---| +| `-Browser` | `auto` | `librewolf` / `firefox` / `auto` (LibreWolf preferred) | +| `-KioskUser` | `VestibuleKiosk` | Dedicated local account (created or reused) | +| `-KioskPassword` | generated + printed | Account password (also used for autologon) | +| `-HomeUrl` | `about:blank` | Page the kiosk session opens | +| `-InstallRoot` | detected | Staged install location | +| `-Quiet` | off | No prompts — for MDM/CI rollout | +| `-Check` | off | Validate prerequisites only, change nothing | +| `-ShellLauncher` | off | Force Shell Launcher instead of the AssignedAccess bridge | +| `-NoAutoLogon` | off | Skip autologon (kiosk starts after manual logon) | +| `-Restart` | off | Reboot automatically when required | +| `-InstallOverridesCfg` | off | Also deploy `librewolf.overrides.cfg` (SSO/telehealth) | + +Exit codes (also used by CI): + +| Code | Meaning | +|---|---| +| 0 | Success, no reboot needed | +| 10 | Success — reboot required to activate | +| 2 | Unsupported (Home edition / not elevated / not Windows) | +| 3 | Prerequisite missing (LibreWolf, usher) | +| 4 | Kiosk account error | +| 5 | Lockdown apply failed (AssignedAccess AND Shell Launcher) | +| 6 | Invalid parameters | + +### How the lockdown is applied + +1. **AssignedAccess via the MDM WMI bridge** (`MDM_AssignedAccess` + `SetSingleAppKiosk`) — the supported scriptable path on Pro and + higher. The kiosk app is a Start Menu shortcut carrying a stable + AppUserModelID (`Vestibule.Kiosk`), verified through `Get-StartApps` + before the XML is applied. +2. **Shell Launcher step-down** (`WESL_UserSetting.SetCustomShell`) on + Enterprise/Education when the bridge rejects the config. The feature + is enabled on demand; that path needs one extra reboot. +3. **kiosk-launch.ps1** is the actual shell process. It runs as the + kiosk user (no admin): installs usher per-user, registers the Native + Messaging host under HKCU, creates `vestibule-profile`, then launches + `librewolf --kiosk -P vestibule-profile ` and relaunches it + if it exits. Log: `%LOCALAPPDATA%\Vestibule\kiosk-launch.log`. + +### Verification checklist + +- [ ] Reboot → machine logs in as the kiosk account automatically +- [ ] LibreWolf opens full-screen on the home URL, no tabs/URL bar +- [ ] Browser Console (`Ctrl+Shift+J`) shows `[vestibule] usher hello: usher 1.2.2` +- [ ] Any domain not on the safelist shows the Vestibule block page (default policy; the home URL's domain is always permitted) +- [ ] Idle 5 min → session resets to home URL, data cleared + +### Admin escape hatches + +- **Ctrl+Alt+Del** still works under AssignedAccess — Sign out / switch + user to reach an admin account. +- **Hold Shift during boot** to bypass automatic logon once. +- Kiosk account has no interactive way to change its password (locked + via `UserMayNotChangePassword`). + +### Building the installer + +```powershell +cd helper; cargo build --release; cd .. +powershell -ExecutionPolicy Bypass -File packaging\build-installer.ps1 +``` + +Requires Inno Setup 6 (`choco install innosetup -y`). Output: +`packaging\Output\Vestibule-Setup-1.2.2.exe`. CI builds it on every push +(see [CI artifacts](#ci-artifacts)). + +--- + +## Linux + +### Prerequisites + +- Any systemd distribution +- **cage** (the Wayland kiosk compositor) — Debian 12+/Ubuntu 24.04+/ + Fedora/Arch packages it; elsewhere build from + [cage-kiosk/cage](https://github.com/cage-kiosk/cage) +- **LibreWolf or Firefox** (ESR recommended) — native package + ([LibreWolf install docs](https://librewolf.net/installation/linux/), + distro Firefox, system-wide Flatpak, or the Ubuntu snap) +- `python3` (policies merge + XPI build), `dbus` (session bus for cage + children) +- A built usher: `cd helper && cargo build --release` + +The script never auto-installs anything: if something is missing it +prints the exact per-distro commands and exits 3. + +### Interactive provisioning + +```sh +sudo ./scripts/provision-kiosk.sh +``` + +### Unattended provisioning + +```sh +sudo ./scripts/provision-kiosk.sh \ + --home-url https://checkin.example.org \ + --kiosk-user kiosk \ + --tty 2 \ + --librewolf native \ + --yes --start +``` + +Parameters: + +| Parameter | Default | Meaning | +|---|---|---| +| `--home-url URL` | `about:blank` | Page the kiosk session opens | +| `--kiosk-user NAME` | `vestibule-kiosk` | Account (created with locked password) | +| `--tty N` | `2` | VT for the cage session (1–12) | +| `--librewolf FLAVOR` | auto | Use LibreWolf: `native` / `flatpak` | +| `--firefox FLAVOR` | auto | Use Firefox: `native` / `flatpak` / `snap` (mutually exclusive with `--librewolf`) | +| `--usher-bin PATH` | auto | Explicit usher binary | +| `--start` | off | Start the session immediately (otherwise: enable only) | +| `--yes` | off | Skip confirmation | +| `--check` | off | Validate prerequisites only | + +Exit codes: 0 success · 2 not root / no systemd · 3 prerequisite missing +· 4 account error · 5 unit failure · 6 bad parameters. + +### How the lockdown is applied + +- `vestibule-kiosk.service` is a **system unit that IS the graphical + session**: cage starts on the chosen VT at boot as the kiosk user via + `PAMName=login` (runtime dir from pam_systemd), no display manager + involved, `Restart=always` for crash recovery. +- `/usr/local/bin/vestibule-kiosk-launch` execs + `dbus-run-session -- cage -d -- librewolf --kiosk -P vestibule-profile + ` (or `flatpak run io.gitlab.librewolf-community …` for the + Flatpak flavor). `MOZ_ENABLE_WAYLAND=1` is mandatory — Gecko defaults + to X11 and cage ships no Xwayland. +- The kiosk account is created with a **locked password**: it can never + be logged into interactively, only entered via the systemd session. +- policies.json is deep-merged into LibreWolf's `distribution/` dir + (existing file backed up to `policies.json.vestibule-bak`) and + force-installs the extension XPI on every browser start — no + about:debugging step on the kiosk. + +Reconfigure at any time by editing `/etc/vestibule/kiosk.env` (home URL, +browser + flavor, cage flags) — the unit re-reads it on every start. + +### Where policies land per browser (Linux) + +| Flavor | policies.json | XPI | Update-safe? | +|---|---|---|---| +| LibreWolf native | `/distribution/` | `/opt/vestibule/extension/` | yes | +| LibreWolf Flatpak | Flatpak app dir `files/librewolf/distribution/` | kiosk home `~/.var/app//` | no — re-run after updates | +| Firefox native | `/etc/firefox/policies/` | `/opt/vestibule/extension/` | yes | +| Firefox Flatpak | kiosk home `~/.var/app/org.mozilla.firefox/.mozilla/policies/` | kiosk home | yes | +| Firefox snap | kiosk home `~/snap/firefox/common/.mozilla/policies/` | kiosk home | yes | + +For Flatpak/snap flavors the browser's filesystem is the kiosk home +remap — that is why the XPI is copied there and `install_url` points +inside the sandbox-visible home rather than `/opt/vestibule`. + +### Verification checklist + +- [ ] `systemctl status vestibule-kiosk` — active (running) +- [ ] The VT shows LibreWolf full-screen on the home URL +- [ ] `journalctl -u vestibule-kiosk -f` shows the launcher + cage +- [ ] Ctrl+Alt+F3 switches to a text VT (`cage -d`); Ctrl+Alt+F2 returns +- [ ] `sudo systemctl restart vestibule-kiosk` recovers from a killed + browser within seconds + +### Flatpak LibreWolf notes (honest limitations) + +1. **Updates wipe the policy layer.** The policies live inside + `/var/lib/flatpak/app/io.gitlab.librewolf-community/…`, which is + replaced on every update. Re-run `provision-kiosk.sh` afterwards. + The extension's own session sanitization (layers 2–3) is unaffected. +2. **Native Messaging under the Flatpak depends on the flatpak's host + visibility.** Manifests are written to all five conventional + locations for the kiosk user; if the flatpak ignores them, unlock + falls back to "not configured" while URL policy and session resets + keep working. Native LibreWolf (or Firefox native) has no such + caveat. + +The Firefox Flatpak and snap do not share limitation 1: their policies +and XPI live in the kiosk user's home, which updates never touch. + +### Building the Flatpak + +```sh +./packaging/build-flatpak.sh +``` + +Output: `packaging/Vestibule-1.2.2.flatpak` (requires flatpak-builder; +first run downloads the Freedesktop 24.08 SDK + Rust extension). The +Flatpak carries the whole deployment kit — `flatpak run +net.dcos.Vestibule provision` prints the exact host-side command. + +--- + +## Deprovision + +Both deprovision scripts reset the machine to its pre-Vestibule state. +Each policy directory is returned to its baseline: the operator's backed-up +policies.json is reinstalled, or Vestibule's generated file is deleted where +no baseline exists (byte-for-byte equality is asserted by the test suite). + +```powershell +# Windows — remove lockdown, autologon, shortcut, policies. +# Optional: -RemoveKioskAccount -RemoveInstall -Restart +powershell -ExecutionPolicy Bypass -File scripts\deprovision-kiosk.ps1 +``` + +```sh +# Linux — stop/disable/remove the unit, launcher, env, policies, manifests. +# Optional: --remove-user --remove-opt --remove-usher +sudo ./scripts/deprovision-kiosk.sh +``` + +The Windows uninstaller (Add/Remove Programs) offers to run the +deprovision step automatically. + +--- + +## Security notes operators must read + +1. **Windows autologon stores the kiosk password in plaintext registry** + (`Winlogon \ DefaultPassword`). On a locked-down single-purpose + appliance this is an accepted trade-off — the account is the least + privileged thing on the machine and the browser is the only shell. + Use `-NoAutoLogon` and manual logon if your threat model disagrees. +2. **The installer and usher binary are unsigned — a standing decision, + not an oversight.** A code-signing certificate costs $200–500/year + and stays an unfunded line item unless a donation earmarks it + (info@dcos.net). SmartScreen will warn; verify the SHA-256 from the + release notes. This is why Linux is the primary target — it has no + signing gate. See README "Honest limitations". +3. **The extension is policy-installed from disk** (force_installed). + That is deliberate: kiosks have no AMO session, and the policy + re-installs the XPI on every start if it is removed. +4. **Linux kiosk account password stays locked.** There is nothing to + brute-force; the session is entered only via systemd. + +--- + +## Troubleshooting + +| Symptom | Platform | Fix | +|---|---|---| +| Black screen after enabling cage | Linux | `MOZ_ENABLE_WAYLAND=1` missing (our launcher sets it); check `journalctl -u vestibule-kiosk` | +| cage fails in a VM | Linux | No DRM: add `WLR_LIBINPUT_NO_DEVICES=1` and `WLR_RENDERER=pixman` to the unit (`systemctl edit vestibule-kiosk`) | +| Flatpak LibreWolf won't start under cage | Linux | Missing session bus — the launcher wraps everything in `dbus-run-session`; check flatpak is installed system-wide, not per-user | +| Snap Firefox detected but policies ignored | Linux | Verify `~/snap/firefox/common/.mozilla/policies/policies.json` exists for the **kiosk user** (not your own); re-run provision with `--firefox snap` | +| AssignedAccess applies but kiosk shows black/Start | Windows | AUMID didn't resolve — check `Get-StartApps \| ? AppID -eq Vestibule.Kiosk`; re-run provisioning after a reboot | +| `[vestibule] usher hello` missing | both | Per-user bootstrap failed: read `%LOCALAPPDATA%\Vestibule\kiosk-launch.log` (Windows) or check NM manifests in the kiosk home (Linux) | +| AssignedAccess XML rejected (exit 5) | Windows | Use `-ShellLauncher` on Enterprise/Education, or apply via Settings → Accounts → Other users → Set up kiosk | +| Policies not applied | both | policies.json needs a full browser restart; verify it parses and that the distribution dir matches the running LibreWolf | +| exit 3 on `--check` | both | Prerequisite missing — the script prints the exact install commands | + +--- + +## CI artifacts + +Every push to `main` builds (`.github/workflows/ci.yml`): + +| Artifact | Job | Contents | +|---|---|---| +| `vestibule-setup-windows` | package-windows | `Vestibule-Setup-1.2.2.exe` (Inno Setup), the extension XPI | +| `vestibule-flatpak-linux` | package-linux | `Vestibule-1.2.2.flatpak` (Freedesktop 24.08) | +| `vestibule-usher-linux` | package-linux | The Linux usher binary from the same commit | + +Download from the workflow run page. Tag a release to attach them to a +GitHub Release. diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..08a8d0c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jeremy Anderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100755 index 0000000..02ccc37 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,210 @@ +# Quickstart + +Get Vestibule running in under five minutes on a Linux or Windows +development machine. For architecture and security model, see +[README.md](README.md). + +## Prerequisites + +- **LibreWolf** 115+ installed ([librewolf.net](https://librewolf.net)) + — or **Firefox** 115+ / Firefox ESR (works identically: the + extension, usher, and policies are engine-standard) +- **Rust** 1.70+ installed via [rustup](https://rustup.rs) +- **Python 3** for the smoke test +- **Node.js** for the URL policy unit tests (`node + scripts/test-url-policy.js`; CI runs them on every push) + +## Build + +```sh +cd helper +cargo build --release +cd .. +``` + +The usher binary lands at `helper/target/release/usher` +(Linux) or `helper\target\release\usher.exe` (Windows). + +## Verify usher + +Run the smoke test without LibreWolf. Exercises the Native Messaging +protocol end-to-end: hello handshake, ping/pong, unlock refusal, and +wake event simulation. + +```sh +python3 scripts/test-native-messaging.py +``` + +Expected last line: + +``` +OK — usher production protocol works: Argon2id unlock + wake events. +``` + +The smoke test exercises the full production protocol: hello, ping, +set-unlock (stores Argon2id hash to disk), unlock with wrong password +(refused), unlock with correct password (granted), and wake simulation. +It cleans up the hash file after itself. + +If this fails, usher will not work in LibreWolf either. Fix before +proceeding. + +## Register usher as a Native Messaging host + +### Linux + +```sh +./scripts/install-native-host.sh +``` + +Installs to `~/.local/bin/usher` and registers the manifest in both +`~/.librewolf/native-messaging-hosts/` and +`~/.mozilla/native-messaging-hosts/`. No sudo required. + +### Windows (PowerShell) + +```powershell +powershell -ExecutionPolicy Bypass -File scripts\install-native-host.ps1 +``` + +Installs to `%LOCALAPPDATA%\Vestibule\usher.exe` and registers under +`HKCU\Software\Mozilla\NativeMessagingHosts\com.vestibule.usher`. No +admin elevation required. + +## Load the extension in LibreWolf + +1. Open LibreWolf (or Firefox — the steps are identical). +2. Navigate to `about:debugging#/runtime/this-firefox`. +3. Click **Load Temporary Add-on...**. +4. Select `extension/manifest.json`. +5. Open the Browser Console (`Ctrl+Shift+J`). +6. Verify these lines appear: + +``` +[vestibule] background loaded, version 1.2.2 +[vestibule] admin wizard: Ctrl+Shift+V or gear icon +[vestibule] usher hello: usher 1.2.2 +``` + +If `[vestibule] usher hello` does not appear, usher failed to connect. +Re-run the install script and restart LibreWolf. + +## Configure the kiosk + +1. Press `Ctrl+Shift+V`, or move the mouse to the top 3px of the + viewport and click the gear icon. +2. On first run, the wizard forces you to set an admin password (8+ + characters). This password gates the wizard; it is separate from + the kiosk unlock password. +3. Walk through the six steps: + - **Kiosk identity** — name, home URL. In safelist mode the wizard + shows the home URL's domain and whether it is on the safelist, + with a one-click add button. + - **URL policy** — safelist (default: block every domain except the + listed ones), or open / blocklist / allowlist for advanced needs + - **Session behavior** — idle timeout (default 300s), onReset + (default "both"), per-domain persistence allowlist + - **Power management** — aware (default) or always-on + - **Unlock method** — password (default); PIN selects a numeric + unlock code; TOTP arrives with Phase 5+ + - **Review and save** — a safelist configuration whose home domain + is not listed is refused at save time +4. Save. The new policy takes effect immediately — no restart required. + +## Verify the pieces work + +### Hidden menu + +Navigate to any website. Move the mouse to the top 3px of the +viewport. A small dark toolbar slides down with Back, Forward, +Refresh, Home, Unlock, and Admin buttons. + +### URL safelist + +Until configured otherwise, the kiosk blocks every external domain. +With the default policy (safelist mode, empty list), navigate to +`https://example.org`. The navigation lands on the Vestibule block +page, which names the blocked domain. The Browser Console shows: + +``` +[vestibule] blocked: https://example.org/ +``` + +Add `example.org` on wizard step 2 and navigate again — the site +loads, subdomains included. Every domain the kiosk content pulls from +(CDNs, SSO, fonts) must be listed the same way; a missing one shows +as a broken resource on an otherwise-working page, and the console +logs the exact blocked URL. + +### Wake detection (Linux) + +Suspend the system: + +```sh +systemctl suspend +``` + +Wake the system. The Browser Console shows: + +``` +[vestibule] wake event from usher, reason: suspend +[vestibule] resetting session (reason: wake:suspend) +[vestibule] all browsing data cleared +``` + +### Unlock path + +Click the lock icon in the hidden menu. A dedicated popup window opens +(not `window.prompt`). Enter the kiosk unlock password. If correct, the +lock overlay clears and admin grace mode activates (10 minutes of +no-reset for maintenance). If wrong, the popup shows an error. + +The unlock password is verified by usher against an Argon2id hash stored +at `~/.config/vestibule/unlock.hash` (Linux) or +`%APPDATA%\Vestibule\unlock.hash` (Windows), file mode 0600. The hash +never touches browser storage. + +### Optional: install usher as a system service + +For crash recovery and pre-LibreWolf power monitoring: + +```sh +# Linux (systemd user service) +./scripts/install-usher-service.sh +systemctl --user start vestibule-usher.service + +# Windows (Scheduled Task) +powershell -ExecutionPolicy Bypass -File scripts\install-usher-task.ps1 +``` + +### Optional: install LibreWolf overrides + +For SSO and telehealth compatibility (relaxes resistFingerprinting, +enables WebGL/WebRTC/EME for patient portals): + +```sh +# Linux +sudo cp config/librewolf.overrides.cfg /usr/lib/librewolf/ + +# Windows (run as admin) +copy config\librewolf.overrides.cfg "C:\Program Files\LibreWolf\distribution\" +``` + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| `usher hello` does not appear in console | Re-run `install-native-host.sh`; restart LibreWolf | +| `Error: Native host exited` | Check `usher` is executable (`chmod +x ~/.local/bin/usher`); run it manually to see stderr | +| Hidden menu does not appear | The content script may have failed on a specific page; check the console for `[vestibule] content script loaded` | +| Wake events do not fire on Linux | Verify D-Bus is running (`dbus-send --session --dest=org.freedesktop.DBus --type=method_call --print-reply /org/freedesktop/DBus org.freedesktop.DBus.ListNames`); usher logs `[usher] power: D-Bus connect failed` if not | +| Wake events do not fire on Windows | Verify usher is running (`Get-ScheduledTask -TaskName VestibuleUsher`); check Event Viewer > Windows Logs > Application for `[usher] power:` messages | + +## Next steps + +- Read [README.md](README.md) for the architecture and security model. +- Read [DEPLOYMENT.md](DEPLOYMENT.md) to provision a real kiosk machine + (OS lockdown included) — the natural next step after this quickstart. +- Read [BLOG.md](BLOG.md) for the narrative behind the rewrite. +- Read [docs/QA-PASS.md](docs/QA-PASS.md) for the production readiness + review. diff --git a/README.md b/README.md new file mode 100755 index 0000000..cad341d --- /dev/null +++ b/README.md @@ -0,0 +1,335 @@ +# Vestibule + +**A kiosk lockdown browser built on LibreWolf (or Firefox ESR) + a Rust native helper.** +Linux-first, single codebase, zero Chromium in the stack. Designed +for medical lobbies, public terminals, and any environment where a +browser must serve the public without leaking session data between +users. + +- **Author:** Jeremy Anderson — [dcos.net](https://dcos.net) — info@dcos.net +- **License:** MIT (see [LICENSE](LICENSE)) +- **Platforms:** Linux is the primary target — provisioned, packaged, + and installed with no signing gate. The Windows stack is complete + and CI-validated but ships unsigned: a code-signing certificate + costs $200–500/year, the project does not buy one, and SmartScreen + warns accordingly. A donation earmarked for code signing + (info@dcos.net) changes that decision. +- **Status:** Production-ready and deployable. Real Argon2id unlock, real per-origin cookie preservation, real power event detection (Linux D-Bus + Windows WM_POWERBROADCAST), safe-by-default navigation (every domain blocked until the operator safelists it), dedicated unlock popup, systemd/scheduled-task supervision, CI matrix, and (Phase 2) one-command OS-level lockdown provisioning on both platforms plus Inno Setup / Flatpak packaging. + +--- + +## What Vestibule does + +A public-facing browser that: + +1. **Locks the perimeter at the OS level.** AssignedAccess on Windows, + `cage` compositor on Linux. The device is the kiosk, not an app + pretending to be one. +2. **Sanitizes every session.** Cookies, history, cache, formData, + downloads, localStorage, indexedDB, pluginData, serviceWorkers, + passwords, sessions — all wiped on idle timeout, wake-from-sleep, + unlock, and browser shutdown. No partial resets. +3. **Detects power state changes.** Subscribes to `login1.Manager` on + Linux via D-Bus. Emits `Wake` events on resume. The extension resets + the session on every wake. +4. **Hides its UI by default.** A menu appears only when the mouse + touches the top 3px of the viewport — the 2001 gesture, unchanged: + invisible to patients, findable by operators. +5. **Gates configuration behind a wizard.** Six-step flow, password- + protected, covering kiosk identity, URL policy, session behavior, + power management, and unlock method. +6. **Blocks the web by default.** The URL policy ships in safelist + mode: every domain is blocked — pages, frames, CDNs, everything — + until the operator lists it. The home page's domain is validated + against the safelist in the wizard and guaranteed navigable at + runtime, so the kiosk can never lock itself out. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ OS-level lockdown (provisioned by Phase 2 scripts) │ +│ • Windows: AssignedAccess + auto-logon local account │ +│ (Shell Launcher step-down on Ent/Edu) │ +│ • Linux: cage compositor + systemd unit on a VT │ +│ → scripts/provision-kiosk.ps1 / .sh — see DEPLOYMENT.md │ +└────────────────────────┬────────────────────────────────┘ + │ launches at boot + ▼ +┌─────────────────────────────────────────────────────────┐ +│ LibreWolf / Firefox ESR (kiosk mode) │ +│ • --kiosk URL │ +│ • dedicated profile (vestibule-profile) │ +│ • policies.json (strict lockdown, see config/) │ +│ • auto-restart via systemd / scheduled task │ +└────────────────────────┬────────────────────────────────┘ + │ loads extension + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Vestibule WebExtension │ +│ • URL policy engine — safelist default, domain-based │ +│ (webRequest; legacy open/blocklist/substring modes) │ +│ • hidden menu (3px-from-top gesture) │ +│ • session reset on wake / idle / unlock │ +│ • idle polling (browser.idle.queryState, 30s) │ +│ • admin wizard (Ctrl+Shift+V or gear icon) │ +└────────────────────────┬────────────────────────────────┘ + │ Native Messaging (stdio JSON) + ▼ +┌─────────────────────────────────────────────────────────┐ +│ usher (Rust binary, ~1.2 MB) │ +│ Threaded architecture: │ +│ • main — stdin read loop, message dispatch │ +│ • power — D-Bus PrepareForSleep (Linux) │ +│ • writer — owns stdout lock │ +└─────────────────────────────────────────────────────────┘ +``` + +## Why this stack + +| Alternative | Rejection reason | +|---|---| +| Tauri 2 (WebView2) | WebView2 is Edge/Chromium | +| Electron | Ships full Chromium, ~150 MB binaries | +| Servo | Web compat gaps on arbitrary kiosk content | +| Stock Firefox ESR | Mozilla trademark policy complicates redistribution | +| Extension-only | Cannot reach OS keyring or supervisor process | +| Per-platform native (WinUI 3 + GTK4) | Two codebases, violates single-coder ethos | + +The chosen stack delivers: one Rust codebase for the helper, one JS +codebase for the extension, Gecko as the webview (no Chromium), 1.2 MB +helper binary, ~6 KB extension. + +Note the Firefox row above concerns *basing the shipped product on +Firefox* (trademark limits on redistribution). Deploying against an +existing Firefox install is different and fully supported since +Phase 2.1: the extension, usher, and policies are engine-standard, and +the provisioning wizards detect Firefox (native, Flatpak, or snap) +with LibreWolf still the preferred default. See +[DEPLOYMENT.md](DEPLOYMENT.md). + +## Quick start + +See [QUICKSTART.md](QUICKSTART.md) for the fast path. Summary: + +```sh +cd helper && cargo build --release && cd .. +python3 scripts/test-native-messaging.py +./scripts/install-native-host.sh # Linux +# or: powershell -File scripts/install-native-host.ps1 # Windows +``` + +Then load `extension/manifest.json` in LibreWolf via `about:debugging`. + +For production kiosks, skip the manual steps entirely: +[DEPLOYMENT.md](DEPLOYMENT.md) provisions the whole machine (OS lockdown +included) with one command per platform, and the CI-built installers +are ready to ship. + +## Configuration + +Open the admin wizard via the gear icon in the hidden menu, or +`Ctrl+Shift+V`. The wizard enforces a setup password on first run and +walks through six steps: + +| Step | Scope | +|---|---| +| 0 | Set or enter admin password | +| 1 | Kiosk identity (name, home URL, attract URL) | +| 2 | URL policy (safelist default; open / blocklist / allowlist advanced) | +| 3 | Session behavior (idle timeout, onReset, persistence allowlist) | +| 4 | Power management (aware / always-on, on_wake behavior) | +| 5 | Unlock method + kiosk unlock password | +| 6 | Review and save | + +Two distinct passwords protect two distinct surfaces: + +| Password | Protects | Storage | Algorithm | +|---|---|---|---| +| Admin | Wizard access, config | browser.storage.local | PBKDF2-SHA-256, 100k iters | +| Kiosk unlock | Session release | file storage via usher (0600) | Argon2id | + +Recovery: OS-level reset only. Delete the `vestibule-profile/storage` +directory and re-run the wizard. A backup recovery code is a Phase 1 +task. + +## Security model + +### Data sanitization + +Three enforcement layers, defense in depth: + +1. **Block all save paths** — `policies.json` disables form history, + password manager, master password creation, Pocket, Screenshots, + Firefox Accounts, search suggestions, Firefox Home widgets, + extension installs, popup blocking override, protocol handler + registration, and encrypted media extensions. `SanitizeOnShutdown` + wipes Cache, Cookies, Downloads, FormData, History, Sessions, + SiteSettings, and OfflineApps on every browser exit. + +2. **Reset on trigger events** — `resetSession()` in `background.js` + invokes `browser.browsingData.remove()` with all 11 data types on + idle timeout, wake-from-sleep, unlock, and manual reset. Resets are + debounced (5s window) to coalesce simultaneous triggers. + +3. **OS-level defense** — AssignedAccess (Windows) and `cage` (Linux) + ensure the browser cannot be closed or escaped. The dedicated + `vestibule-profile` directory isolates the kiosk from any user + profile data. + +### URL policy + +The default mode is **safelist**: every request whose hostname is not +on the operator-maintained safelist is blocked. Matching is +hostname-based — the entry `example.org` grants `example.org` and any +subdomain, and nothing else. A URL that merely contains +`example.org` as a substring (a query string, a path segment, a +userinfo prefix) does not match; that smuggle class is exactly what +domain matching closes. + +Two guarantees keep the default safe without bricking the kiosk: + +- **Internal schemes are always permitted** (`about:`, + `moz-extension:`, `chrome:`, `resource:`) — the admin wizard, the + unlock popup, and `about:blank` keep working with an empty list. +- **The home origin is always navigable.** Session reset, wake, and + idle timeout all navigate home; the engine exempts the home URL's + hostname so an operator error cannot strand the kiosk on its own + block page. The wizard additionally refuses to save a safelist + configuration whose home (or attract) domain is not listed. + +Blocked top-level navigations land on an in-extension block page that +names the blocked domain; blocked subresources are cancelled outright. +An empty safelist therefore degrades to a kiosk that shows `about:blank` +and nothing else — safe by default, open by explicit operator action. + +Provisioned kiosks bridge the two worlds automatically: the provisioner +launches the browser with the kiosk home URL on the command line, and +while the policy is still the default, the extension adopts that +startup page as the home URL and adds its domain as the first safelist +entry. Only pages the browser was launched with qualify — a URL typed +after boot is never adopted — so the default-deny posture holds +against walk-up users on an unconfigured kiosk. + +The legacy modes remain for deployments that need them: `open` (no +filtering), `blocklist` (substring deny), and `allowlist` (substring +allow — an empty allowlist allows everything; prefer safelist). A +corrupted or unrecognized mode string fails closed to safelist +semantics. + +### Power state awareness + +Vestibule detects wake-from-sleep and resets the session. It does not +block sleep — the OS power profile decides whether the device sleeps, +and that is a deployment-time decision. + +| Platform | API | Event | +|---|---|---| +| Linux | D-Bus `org.freedesktop.login1.Manager.PrepareForSleep(false)` | ~2s after wake | +| Windows | `RegisterPowerSettingNotification` + `WM_POWERBROADCAST` (Phase 1) | `PBT_APMRESUMEAUTOMATIC` | + +Crash recovery: if LibreWolf crashes during sleep, the supervisor +(systemd / scheduled task) restarts LibreWolf, usher cold-starts, and +usher's first action on `hello` emits a `Wake` event to the extension +(Phase 1 implementation; spike skips this to avoid wiping data on every +reconnect during testing). + +## Phase roadmap + +| Phase | Scope | Status | +|---|---|---| +| 0 | Architecture spike: extension + usher + Native Messaging + URL filter + wake detection + session reset + admin wizard | done | +| 1 | Argon2id unlock via file storage (0600); dedicated unlock popup; Windows power events; per-origin cookie preservation; `librewolf.overrides.cfg`; systemd + scheduled-task supervision; CI matrix | done | +| 2 | OS-level lockdown provisioning (AssignedAccess wizard on Win, cage systemd unit on Linux); packaging (Inno Setup on Windows, Flatpak on Linux); full deprovision | done | +| 3 | Crash auto-restart integration testing; telemetry hooks; remote management API stub; `always-on` power-inhibit mode | planned | +| 4 | Store distribution (Flathub submission); additional package formats (MSI, AppImage, .deb) only if deployments demand them | planned | +| 5 | Remote management API; retro 2001 theme; accessibility pass; contributor docs | planned | +| 5+ | Webcam presence detection (see [TODO](#webcam-presence-detection)) | deferred | + +## Webcam presence detection + +Deferred to Phase 5+. When a webcam is detected at startup, usher will +use it to: + +1. Detect "user walked away" — no face visible for N seconds triggers + immediate session reset (faster than the idle timeout). +2. Detect "different user approached" — face embedding mismatch + triggers reset. + +Privacy guarantees (non-negotiable): + +- Frames processed in-memory only, never written to disk +- Face embeddings never leave the local process +- No telemetry, no cloud API calls +- Webcam LED respected; no capture while camera is in use by another + application + +Deferred because: privacy review must precede any biometric code; +OpenCV/dlib adds ~50 MB to the usher binary; the idle timeout and wake +detection in the spike cover the common cases. See +`config/vestibule.toml.example` `[presence]` block for the proposed +schema. + +## Honest limitations + +1. **LibreWolf release cadence** tracks Firefox ESR (~4 weeks security, + ~12 months major). CI matrix tests current ESR; operators should + test before upgrading. +2. **Windows binaries ship unsigned — a standing decision, not an + oversight.** A code-signing certificate costs $200–500/year and the + project does not buy one; SmartScreen warns on the usher binary and + the installer. That is why Linux is the primary target: it has no + equivalent gate. A donation earmarked for code signing + (info@dcos.net) changes the decision; until then, Windows operators + verify the SHA-256 from the release notes. +3. **`--kiosk` is not complete lockdown.** It removes UI chrome but + does not block all escape vectors. OS-level AssignedAccess / cage + setup is mandatory for production deployments — and since Phase 2 it + is one command: [DEPLOYMENT.md](DEPLOYMENT.md) covers interactive + and unattended provisioning plus one-command deprovision on both + platforms. +4. **usher force-exits on stdin close.** The power thread is blocked on + an OS-level receive call (D-Bus / GetMessage) and cannot be + interrupted without an async runtime. This is the correct shutdown + strategy for this architecture — the power thread has no cleanup. +5. **localStorage is not selectively preserved.** When + `dataPersistenceAllowlist` is non-empty, cookies for allowlisted + domains are preserved via `cookies.getAll()` + selective + `cookies.remove()`. localStorage is wiped unconditionally — the + WebExtension API cannot enumerate origins for selective removal. + localStorage typically holds UI state, not auth tokens; the risk + is low and documented in code. +6. **Safelist mode blocks third-party resources too.** A page on a + safelisted domain that pulls scripts, fonts, or images from a CDN + will render broken until the CDN's domain is also safelisted. This + is by design — data can leave through subresource requests, so + they are gated like navigations — but it means the operator's list + must cover every domain the kiosk content depends on. The wizard's + step-2 help text says so, and the browser console logs each block + with the exact URL. +7. **Windows power events require testing on real hardware.** The + `RegisterSuspendResumeNotification` + `WM_POWERBROADCAST` + implementation is complete and cfg-gated, but has not been tested + on a physical Windows machine. The Linux D-Bus path is fully + tested. + +## Documentation + +- [DEPLOYMENT.md](DEPLOYMENT.md) — operator runbook: kiosk + provisioning, unattended rollout, exit codes, deprovision +- [QUICKSTART.md](QUICKSTART.md) — fast-path install and verify +- [BLOG.md](BLOG.md) — narrative: 2001 VB6 to 2026 Rust +- [history/](history/) — the scrubbed 2001 VB6 source, kept as a + non-shipping historic artifact (nothing in the build depends on it) +- [docs/QA-PASS.md](docs/QA-PASS.md) — production readiness review + (Mixture-of-Experts panel) +- [LICENSE](LICENSE) — MIT + +## Credits + +Original concept: Jeremy Anderson, 2001 — coded on co-op while in +school, VB6. +Modern implementation: Jeremy Anderson, 2026. + +- Website: [dcos.net](https://dcos.net) +- Email: info@dcos.net diff --git a/config/com.vestibule.usher.linux.json b/config/com.vestibule.usher.linux.json new file mode 100755 index 0000000..d1a48ee --- /dev/null +++ b/config/com.vestibule.usher.linux.json @@ -0,0 +1,7 @@ +{ + "name": "com.vestibule.usher", + "description": "Vestibule native helper", + "path": "/usr/local/bin/usher", + "type": "stdio", + "allowed_extensions": ["vestibule@vestibule.kiosk"] +} diff --git a/config/com.vestibule.usher.windows.json b/config/com.vestibule.usher.windows.json new file mode 100755 index 0000000..3cf8b01 --- /dev/null +++ b/config/com.vestibule.usher.windows.json @@ -0,0 +1,7 @@ +{ + "name": "com.vestibule.usher", + "description": "Vestibule native helper", + "path": "C:\\Program Files\\Vestibule\\bin\\usher.exe", + "type": "stdio", + "allowed_extensions": ["vestibule@vestibule.kiosk"] +} diff --git a/config/librewolf.overrides.cfg b/config/librewolf.overrides.cfg new file mode 100755 index 0000000..24c6507 --- /dev/null +++ b/config/librewolf.overrides.cfg @@ -0,0 +1,52 @@ +// Vestibule LibreWolf overrides — relaxes LibreWolf's privacy-hardened +// defaults that break common SSO and telehealth flows in kiosk deployments. +// +// Install at: +// Linux: /usr/lib/librewolf/librewolf.overrides.cfg +// (or ~/.librewolf/vestibule-profile/librewolf.overrides.cfg) +// Windows: C:\Program Files\LibreWolf\distribution\librewolf.overrides.cfg +// (or %APPDATA%\LibreWolf\Vestibule\librewolf.overrides.cfg) +// +// Each override documents why it is needed for kiosk use. All use lockPref +// to prevent the user from changing them at runtime. + +// Resist Fingerprinting randomizes window dimensions, fonts, and other +// fingerprintable attributes. In a fullscreen kiosk at a fixed resolution, +// this breaks layout on many patient-portal sites and provides no privacy +// benefit (the kiosk is a single-user shared device, not a privacy tool). +lockPref("privacy.resistFingerprinting", false); + +// Letterboxing rounds window dimensions to prevent fingerprinting via +// screen size. Unnecessary in fullscreen kiosk mode; can cause rendering +// artifacts on patient-portal sites that expect exact viewport sizes. +lockPref("privacy.window.letterboxing", false); + +// Encrypted Media Extensions (EME) — some telehealth portals and medical +// video players require DRM for content protection. Enable for kiosk use. +lockPref("media.eme.enabled", true); + +// WebGL — some medical imaging viewers (e.g., OHIF Viewer, Cornerstone.js) +// require WebGL for hardware-accelerated rendering of DICOM images. +lockPref("webgl.disabled", false); + +// WebRTC — telehealth portals (e.g., Doxy.me, Zoom for Healthcare) use +// WebRTC for video consultations. +lockPref("media.peerconnection.enabled", true); + +// Clipboard — patient intake forms may require copy/paste between fields. +lockPref("dom.event.clipboardevents.enabled", true); + +// Tracking protection — kiosks navigate to known, operator-approved URLs. +// Strict tracking protection breaks SSO redirects on some medical portals. +lockPref("privacy.trackingprotection.enabled", false); +lockPref("privacy.trackingprotection.pbmode.enabled", false); + +// sessionStore — reopen the kiosk URL automatically after a crash. +lockPref("browser.sessionstore.resume_from_crash", true); +lockPref("browser.startup.page", 1); // reopen the session on startup + +// Disable all prompts that could confuse kiosk users. +lockPref("browser.tabs.warnOnClose", false); +lockPref("browser.warnOnQuit", false); +lockPref("app.update.enabled", false); // kiosk updates are operator-managed +lockPref("browser.newtabpage.enabled", false); diff --git a/config/policies.json b/config/policies.json new file mode 100755 index 0000000..44a8019 --- /dev/null +++ b/config/policies.json @@ -0,0 +1,93 @@ +{ + "policies": { + "DisablePrivateBrowsing": true, + "DisableDevTools": true, + "BlockAboutConfig": true, + "BlockAboutProfiles": true, + "BlockAboutSupport": true, + "DisableProfileRefresh": true, + "DisableSafeMode": true, + "DisableFirefoxAccounts": true, + "DisableFirefoxStudies": true, + "DisableTelemetry": true, + "DisablePocket": true, + "DisableScreenshots": true, + "DontCheckDefaultBrowser": true, + "NoDefaultBookmarks": true, + "Homepage": { + "URL": "about:blank", + "Locked": true + }, + "DownloadDirectory": "${tmp}", + + "SanitizeOnShutdown": { + "Cache": true, + "Cookies": true, + "Downloads": true, + "FormData": true, + "History": true, + "Sessions": true, + "SiteSettings": true, + "OfflineApps": true + }, + + "DisableFormHistory": true, + "OfferToSaveLogins": false, + "OfferToSaveLoginsDefault": false, + "PasswordManagerEnabled": false, + "PrimaryPassword": false, + + "DisableFeedbackCommands": true, + "DisableMasterPasswordCreation": true, + + "EncryptedMediaExtensions": { + "Enabled": false, + "Locked": true + }, + + "SearchSuggestEnabled": false, + + "NetworkPrediction": false, + "CaptivePortal": false, + + "FirefoxHome": { + "Search": false, + "TopSites": false, + "Highlights": false, + "Pocket": false, + "Snippets": false, + "Locked": true + }, + + "SearchEngines": { + "PreventInstalls": true + }, + + "Authentication": { + "Locked": true + }, + + "Handlers": { + "mode": "block", + "exceptions": {} + }, + + "PopupBlocking": { + "Default": true, + "Locked": true + }, + + "InstallAddonsPermission": { + "Default": false, + "Locked": true + }, + + "ExtensionSettings": { + "*": { + "blocked_install_message": "Extensions are not allowed on this kiosk.", + "install_sources": [], + "installation_mode": "blocked" + } + } + } +} diff --git a/config/vestibule.toml.example b/config/vestibule.toml.example new file mode 100755 index 0000000..b33763d --- /dev/null +++ b/config/vestibule.toml.example @@ -0,0 +1,171 @@ +# Vestibule configuration — example schema +# +# This file documents the shape of vestibule.toml. The admin wizard +# writes its configuration as JSON to browser.storage.local (see +# extension/admin.js); usher-side TOML loading for headless +# provisioning is a Phase 3 task. The schema is fixed now so the +# implementation has a target. +# +# In production, this file lives at: +# Linux: ~/.config/vestibule/vestibule.toml +# Windows: %APPDATA%\Vestibule\vestibule.toml + +[general] +# Product name shown in UI +name = "Vestibule" + +[url_policy] +# Navigation policy. The wizard writes this block to +# browser.storage.local as the `policy` key (see extension/admin.js); +# usher-side TOML loading for headless provisioning is a Phase 3 task. +# +# "safelist" — block every request whose hostname is not on the +# safelist (default). Domain-based: an entry grants +# the domain and its subdomains, and nothing else. +# Third-party resources (CDNs, SSO, analytics) are +# gated too — list every domain the kiosk content +# needs. Internal schemes (about:, moz-extension:, +# chrome:, resource:) and the home page's domain are +# always permitted, so an empty list degrades to a +# kiosk that shows about:blank and nothing else. +# "open" — no filtering +# "blocklist" — substring deny (legacy) +# "allowlist" — substring allow (legacy; an empty list allows +# everything — prefer safelist) +mode = "safelist" + +# Operator-maintained safelist — one hostname per entry. Pasted URLs +# are normalized to their hostname. Subdomains are covered by the +# parent entry; ports are ignored. +safelist = [] + +[session] +# Idle timeout before session resets (seconds). +# Patient walks away → after this many seconds of no input, clear ALL +# personal data and return to home URL. Independent of device sleep. +idle_timeout_s = 300 + +# What to do on session reset. +# "reset" — clear all personal data, navigate to home (default) +# "lock" — show unlock overlay, keep current tab visible behind it +# "both" — reset then show unlock overlay +on_reset = "both" + +# Home URL — where to navigate after reset. In safelist mode the +# wizard refuses to save unless this URL's domain is on the safelist; +# the engine additionally exempts it at runtime, so the kiosk can +# never block its own front door. +home_url = "about:blank" + +[power] +# How Vestibule handles device sleep/wake. +# "aware" — detect wake, reset session on resume (default) +# "always-on" — block idle sleep via OS inhibit APIs (Phase 3) +mode = "aware" + +# On wake from sleep, what to do. +# "reset" — same as on_reset="reset" +# "lock" — same as on_reset="lock" +# "both" — same as on_reset="both" (default) +# "nothing" — ignore wake (testing only; never use in production) +on_wake = "both" + +[unlock] +# Password storage location. +# "file" — Argon2id PHC string at 0600, owned by the kiosk user +# (the shipped implementation; keyring daemons are not +# available on minimal kiosk compositors like cage) +# "keyring" — OS keyring (reserved for desktop-session deployments) +storage = "file" + +# Unlock method. +# "password" — single password (Argon2id hash in file storage) +# "pin" — numeric PIN (Argon2id hash in file storage) +# "totp" — TOTP + password (Phase 5+) +method = "password" + +# Argon2id parameters for password hashing. +# Defaults are conservative; tune for your threat model. +[unlock.argon2] +memory_kib = 65536 # 64 MiB +iterations = 3 +parallelism = 4 + + +# ────────────────────────────────────────────────────────────────────── +# PLANNED WORK — NOT IMPLEMENTED IN THIS RELEASE +# ────────────────────────────────────────────────────────────────────── +# Webcam-based presence detection. When a webcam is detected at startup, +# usher uses it to: +# +# 1. Detect when the current user steps away from the kiosk (no face +# visible for N seconds). +# 2. Detect when a different person approaches (face embedding doesn't +# match the one captured at session start). +# +# In either case, trigger an immediate session reset (same path as idle +# timeout, but faster — no need to wait idle_timeout_s). +# +# Privacy guarantees: +# - Frames are processed in-memory only, never written to disk +# - Face embeddings never leave the local process +# - No telemetry, no cloud API calls +# - Webcam LED (if present) is respected; we do not attempt to capture +# frames while the camera is in use by another application +# +# Implementation: OpenCV + dlib (or MediaPipe) running in usher's power +# thread sibling. Spike scope TBD in a future session. +# +# [presence] +# enabled = false # auto-enable if webcam detected +# camera_index = 0 +# away_threshold_s = 10 # no face for this long → reset +# face_match_threshold = 0.6 # cosine similarity; lower = stricter +# match_window_s = 30 # capture baseline face over first 30s +# on_change = "reset" # "reset" | "lock" | "ignore" +# privacy_mode = true # never persist any biometric data + + +# ────────────────────────────────────────────────────────────────────── +# ADMIN CONFIGURATION — set via the in-browser wizard +# ────────────────────────────────────────────────────────────────────── +# The admin wizard is opened via: +# - The gear icon in the hidden menu (mouse to top of screen) +# - Ctrl+Shift+V keyboard shortcut +# +# The wizard is gated by a setup password (separate from the kiosk unlock +# password). On first run, the wizard forces the operator to set this +# password before any configuration can be saved. +# +# [admin] +# # Password hashing algorithm for the admin password: +# # PBKDF2-SHA-256, 100k iterations, in-browser via Web Crypto. The +# # kiosk unlock password uses Argon2id in usher (file storage, 0600). +# algorithm = "PBKDF2-SHA-256" +# iterations = 100000 +# salt_bytes = 16 +# +# # Recovery: if the admin password is lost, the only recovery path is +# # OS-level — delete the vestibule-profile/storage directory and re-run +# # the wizard. A backup recovery code is a Phase 5 task. +# recovery = "os-level-reset" +# +# # The wizard's full config schema is mirrored into browser.storage.local +# # under the `adminConfig` key. See extension/admin.js for the canonical +# # schema. The TOML below is documentation only — the wizard writes +# # JSON, not TOML. Headless TOML provisioning is a Phase 3 task. +# +# [admin.wizard] +# # Steps shown in the wizard, in order. Cannot be reordered. +# steps = [ +# "auth-gate", # set or enter admin password +# "kiosk-identity", # name, home URL, attract URL (home domain +# # checked against the safelist live) +# "url-policy", # mode + safelist/allowlist/blocklist +# "session", # idle timeout, onReset, persistence allowlist +# "power", # mode, on_wake +# "unlock", # method + kiosk unlock password +# "review", # JSON review + save (refuses a safelist +# # config whose home domain is unlisted) +# ] + diff --git a/docs/QA-PASS.md b/docs/QA-PASS.md new file mode 100755 index 0000000..1a844cb --- /dev/null +++ b/docs/QA-PASS.md @@ -0,0 +1,156 @@ +# Vestibule — QA Production Readiness Pass (1.2.0) + +**Review date:** 2026-08-24 +**Subject:** `vestibule-1.2.0` — full tree (extension, usher, scripts, packaging, docs) +**Reviewer:** Mixture-of-Experts panel — ten seats: five disciplines, each with a Linux and a Windows counterpart (QA analyst, platform engineer, architect, administrator, DevOps project manager) +**Standards applied:** PEP 8 (spirit) for Python, POSIX sh, SEI CERT, MISRA-C (spirit), Unix philosophy +**Method:** every finding is either fixed in this pass or recorded under Honest limitations. No finding is deferred silently. +**Addendum:** a 1.2.2 feature pass (safe-by-default navigation) is recorded at the end of this document, same method. + +--- + +## Panel composition + +| Seat | Focus | +|---|---| +| Senior QA Analyst (Linux / Windows) | Test coverage, edge cases, failure modes, protocol correctness, doc-code parity | +| Senior Linux Engineer | D-Bus integration, systemd, packaging, POSIX compliance, shell hygiene | +| Senior Windows Engineer | AssignedAccess/Shell Launcher, WMI bridge, PowerShell 5.1 compatibility, ACLs | +| Senior Architect | Module boundaries, data flow, threading model, table-driven dispatch | +| Senior Administrator (both platforms) | Deployment, configuration, recovery, observability, operability | +| Project Manager (DevOps) | CI/CD, release management, versioning, risk register | + +--- + +## Release-blocking findings — all fixed in this pass + +1. **Fake constant-time comparison in the admin wizard** (`extension/admin.js`). `verifyPassword()` used `Array.prototype.every()`, which short-circuits on the first mismatch, then wrapped the result in a dead ternary — a timing side channel on the admin password check and a SEI CERT MSC06-C violation. Replaced with a single XOR-accumulate `reduce()` over every byte: no short-circuit, no dead branch, one return. + +2. **Version skew across the release.** The tree declared 1.1.0 in nine places (Cargo.toml, Cargo.lock, extension manifest, Inno Setup ×2, Flatpak CLI, build-flatpak.sh, metainfo, validate.sh ×3, CI ×5, integration test, docs) while the release artifact is 1.2.0. Every declaration now reads 1.2.0, the metainfo carries a 1.2.0 release entry, and `validate.sh` enforces the single version across all five machine-readable sources — a mismatch now fails local validation before it can fail in CI. + +3. **Documentation contradicted the implementation on credential storage.** README, admin wizard UI, TOML schema, and BLOG all claimed the unlock hash lives in the OS keyring; `helper/src/storage.rs` stores the Argon2id PHC string in a file at 0600 (Windows: default user ACL). The documentation now states the file-storage design and the reason for it: keyring daemons do not exist on minimal kiosk compositors such as cage, so file permissions are the enforcement boundary. `Cargo.toml`'s stale "keyring bridge" description is gone. + +4. **Stale Native Messaging manifest path (Windows).** `config/com.vestibule.usher.windows.json` pointed at `C:\Program Files\Vestibule\usher.exe`; the provisioning scripts stage the binary at `...\Vestibule\bin\usher.exe`. Corrected to the staged location. (Live registration always generated its own manifest with the resolved path; the shipped file is the reference copy.) + +5. **Shell injection surface in `install-native-host.sh`.** The manifest generation interpolated `TARGET_BIN` directly into Python source via `-c "..."` — a path containing a quote would alter the program (SEI CERT IDS03 family). Paths now travel as argv to a heredoc script; the source contains no interpolated values. + +6. **Integration-test coverage gap.** The prior QA record claimed the launcher dispatch was "asserted, not just parsed" — no such test existed. `scripts/test-provision-linux.sh` now runs the launcher itself against browser shims for all four env permutations (firefox/flatpak, firefox/native, librewolf/flatpak, defaults) and asserts the exec'd command line. Coverage moved from 122 to 126 assertions, all passing. + +--- + +## Coding standards findings — all fixed in this pass + +| Standard | Finding | Fix | +|---|---|---| +| MISRA-C (spirit): table-driven dispatch over nested conditionals | `power.rs` `wnd_proc` used sequential `if msg == ...` tests | Single `match` — one arm per handled message, default arm defers to `DefWindowProcW` | +| SEI CERT: no `unwrap()` on fallible values | `power.rs` called `notify_handle.unwrap()` after a non-binding match | Handle bound in the match; the failure arm returns with monitoring disabled, exit path never panics | +| Arrays/tables over nested ifs | `provision-kiosk.sh` carried two near-duplicate resolver functions with nested if/elif ladders | One `flavor_available` lookup table (browser:flavor → test) plus linear step-down resolvers — one line per flavor, first available wins | +| DRY / Unix philosophy | Three separate ordered-path probe loops in `provision-kiosk.sh` | Single `first_executable()` helper; binary probes are one assignment each | +| Avoid for/while where a declarative form exists | `make-icons.py` render loop and listing loop; nested small-size branch | Dict comprehension for rendering, `"\n".join()` for the listing, and a step-down `draw_icon()` that selects `draw_simple_icon()` or `draw_full_icon()` — output verified byte-identical on all 8 artifacts | +| Avoid for/while where a declarative form exists | `test-native-messaging.py` was six copy-pasted sequential blocks | Protocol steps are a table (label, request, timeout, criterion); the driver is one comprehension over `map(run, STEPS)`; failure report is a join | +| POSIX sh | Launcher's browser/flavor forks used nested `if` inside `if` | `case` dispatch — the fork table is the case statement | +| PEP 8 | Inline hash-file branch, `os.path.exists` guard before `os.remove` | `contextlib.suppress(FileNotFoundError)`; named timeout constants | + +**Loops that remain, deliberately.** Event loops that *are* the program's purpose are not data-processing loops and have no declarative equivalent: usher's stdin dispatch and channel-draining writer (`main.rs`), the D-Bus signal drain and reconnect loop (`power.rs`), the Windows message pump, and the kiosk supervision loop (`kiosk-launch.ps1`). Argument-parsing `while [ $# -gt 0 ]` and iteration over data lists in POSIX sh (no arrays in the language) likewise stay — they are the minimal mechanism the platform offers. This is the "where possible" boundary. + +--- + +## Language and tone findings — all fixed in this pass + +The pass removed every phrase narrating reversal or back-and-forth history ("restored", "brought back", "honored for back-compat", "pulled forward and re-scoped", "Phase N may move"). Every comment and document now states current behavior in the present tense, as a decision. + +| Where | Before | After | +|---|---|---| +| `vestibule-kiosk-launch` | `VESTIBULE_LIBREWOLF_FLAVOR` (pre-Firefox-support name) honored as a fallback | Removed. `kiosk.env` is written by the current provisioner and carries `VESTIBULE_BROWSER_FLAVOR`; the launcher reads exactly one variable per concern | +| `deprovision-kiosk.sh` / `.ps1` | `restore_policies()` / "restored original policies.json" | `reset_policy_dir()` / "baseline policies.json reinstalled" — the operation is a baseline reset, stated as one | +| `test-provision-linux.sh` | "policies.json restored byte-for-byte" | "policies.json equals the pre-provision baseline (byte-for-byte)" | +| DEPLOYMENT.md / README / metainfo | "rollback", "full deprovision/rollback" | "deprovision" — one noun, one operation | +| provisioners (both platforms) | "falling back to Shell Launcher", "Firefox fallback" | "step-down": the choice forks are ordered ladders, documented as such | +| BLOG.md | "Phase 2 shipped — and grew past that plan… pulled forward from Phase 4 and re-scoped" | "Vestibule 1.2 ships…" — what the release contains, not how the plan moved | +| admin wizard UI / admin.js / CSS headers | "Phase 0 spike", "In the spike this is not verified", "Phase 1 stores the Argon2id hash in the OS keyring" | Production wording matching the shipped behavior | + +Step-down logic is now the named mechanism at every fork: browser resolution (LibreWolf native → LibreWolf Flatpak → Firefox native → Firefox Flatpak → Firefox snap), lockdown application (AssignedAccess bridge → Shell Launcher), browser discovery on Windows (filesystem paths → HKLM App Paths → HKCU App Paths), and the usher source probe (explicit flag → installed binary → repo build → staged copy). Each is an ordered ladder with first-match-wins; none is a nested conditional. + +--- + +## Verification performed in this pass + +All checks were executed in a clean checkout of the pass output; none are quoted from earlier records. + +- **`scripts/test-provision-linux.sh`: 126/126 assertions pass.** Three full provision/deprovision scenarios (LibreWolf native, Firefox native, Firefox Flatpak) covering kiosk-user creation, /opt staging, XPI build, launcher install, kiosk.env contents, all five Native Messaging manifest locations with valid JSON and correct paths, policies deep-merge preserving the browser's own keys, force-installed extension with the correct install_url (sandbox-visible for Flatpak), unit generation and enable, and byte-for-byte baseline equality after deprovision — plus the four new launcher-dispatch assertions. +- **`scripts/test-native-messaging.py` (table-driven rewrite) verified end-to-end** against a protocol-faithful mock helper: all six steps pass with exit 0; a deliberately corrupted hello response produces exit 1 with a precise field-level message. (The Rust toolchain is not available in this review environment, so the mock stands in for the binary; CI builds and exercises the real usher on both platforms.) +- **`scripts/make-icons.py` refactor is rendering-equivalent:** all 8 generated artifacts (SVG, six PNGs, ICO) are byte-identical to the committed set (SHA-256 comparison). +- **Syntax gates:** `sh -n` (dash) on every shell script including the launcher and Flatpak CLI; `python3 -m py_compile` on both Python scripts; JSON validity on all five JSON files; XML validity on the metainfo; YAML validity on the CI workflow; PowerShell structural checks (here-string placement, brace/paren/bracket balance). +- **`scripts/validate.sh` passes end-to-end** on the tree, including the new 1.2.0 version-consistency gate. +- **Doc-code parity spot checks:** every console log line quoted in QUICKSTART/DEPLOYMENT now matches the strings the code emits; the verification checklist version matches the manifest. + +--- + +## Honest limitations + +1. **Windows lockdown paths need hardware validation.** The MDM WMI bridge call, AUMID shortcut property set, and Shell Launcher step-down are implemented to documentation and edition-gated, but this pass exercised them only through parse/structure checks — no physical Windows Pro/Ent machine was available. The Linux path is integration-tested; the Windows path is CI-validated for syntax and structure only. +2. **Rust changes compile-verified by review, not by build.** This environment has no cargo; the `power.rs` match-dispatch and handle-binding changes and the 1.2.0 version bump follow patterns the existing CI matrix compiles on every push, but the binaries in this tarball were not rebuilt here. +3. **Firefox snap and Flatpak policy paths follow Mozilla's documented sandbox layouts** and are provisioned and deprovisioned deterministically, but were validated against shims, not real snap/Flatpak Firefox installs as a system-wide kiosk user. +4. **The installer and usher binary remain unsigned — a standing decision, not an oversight.** A code-signing certificate is an unfunded line item unless a donation earmarks it; SmartScreen warns and operators verify the SHA-256 from the release notes. This decision is the root of the Linux-first platform priority (see Post-pass decisions). +5. **Windows autologon stores the kiosk password in plaintext registry** — accepted trade-off on a locked-down appliance, documented with the `-NoAutoLogon` escape. +6. **Flatpak LibreWolf policies are wiped by app updates** (the distribution dir lives inside the flatpak). Re-run provisioning after updates; documented in DEPLOYMENT.md. + +--- + +## Post-pass decisions (2026-08-24) + +1. **Platform priority: Linux.** The Windows stack stays complete and CI-validated, but its binaries stay unsigned: a code-signing certificate is an unfunded line item and remains one unless a donation earmarks it (info@dcos.net). Linux carries no signing gate and is the supported production path. README, DEPLOYMENT.md, and BLOG.md now state this decision where each document introduces the platform story. +2. **Historic artifact: the 2001 VB6 original.** The original source is included under `history/vb6-2001/` — all three iterations (earliest, lobby build, shipped final), scrubbed of employer, school, and client identifiers: employer-branded project filenames renamed to the neutral `LobbyBrowser*`, the company version string emptied, the hardcoded unlock code redacted, the 2001 readme's employer mention rewritten to the sanitized provenance (co-op while in school, first employer — a Boston-area MSP and programming company). The compiled binary, a captured copy of IEXPLORE.EXE, and an empty scratch file are excluded (a binary cannot be scrubbed without a rebuild; the IE executable is Microsoft's, not authored code). `scripts/validate.sh` gates the scrub: the identifier scan fails the release if any of it reappears. The lock screen's "System Security 1.5" branding strings are left in place — they are what the shipped binary displayed, and they document the sibling app whose UI the browser reused. + +--- + +## Risk register + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| LibreWolf/Firefox ESR drops `webRequestBlocking` | Low | Critical | Test in ESR beta CI; declarativeNetRequest is the designated step-down | +| zbus 5.x breaking change | Medium | Medium | zbus pinned to major 4; test on each minor bump | +| MDM WMI bridge unavailable on locked-down SKUs | Medium | Medium | Shell Launcher step-down (Ent/Edu); manual Settings path documented | +| D-Bus unavailable in container/CI | High (CI) | Low | Power thread logs and retries; kiosk functions without power events | +| Windows code-signing cert cost | High | Medium | Unfunded by decision; a donation earmarked for a certificate flips it. Linux is the primary target in the meantime | + +--- + +## Verdict + +**1.2.0 is production-ready within the documented limitations.** The release blockers (credential-comparison side channel, version skew, doc-code contradictions) are closed; the coding-standards sweep is applied across Python, shell, PowerShell, Rust, and JavaScript; every choice fork in the deployment surface is an ordered, named step-down ladder; the test suite grew to 126 assertions and passes clean; and the documentation reads as a decision log in the present tense. The panel approves the release. + +--- + +## Addendum — 1.2.2 safe-by-default navigation (2026-08-24) + +**Scope:** the URL policy gains a `safelist` mode that is the new default: every domain is blocked — top-level pages, frames, and subresources alike — until the operator lists it. Matching is hostname-based; the legacy substring modes remain unchanged for existing deployments. + +### Design decisions (all present tense, all enforced in code) + +1. **Domain matching, not substring matching.** The entry `example.org` grants the domain and its subdomains and nothing else. Query-string, path-segment, and userinfo smuggles (`https://evil.com/?q=example.org`, `https://example.org@evil.com/`) that pass the legacy allowlist are blocked by the safelist. This smuggle class is asserted explicitly in the unit suite. +2. **Internal schemes are always permitted** (`about:`, `moz-extension:`, `chrome:`, `resource:`); `data:` and `blob:` are permitted as subresources and blocked as top-level documents. An empty safelist therefore degrades to a kiosk showing `about:blank`, not a kiosk showing nothing. +3. **Home-origin guarantee.** The engine exempts the configured home URL's hostname in every mode, so reset/wake/idle navigation can never strand the kiosk on its own block page. The wizard enforces the same rule at save time for the home and attract URLs and live on step 1, with a one-click add-to-safelist button. +4. **First-boot adoption.** A provisioned kiosk launches with its home URL on the command line, which browser storage cannot know. While the policy is still the default, the extension adopts the browser's startup page as home and safelists its domain. Only tabs present at background startup qualify — never a later typed navigation — so the walk-up attack (type your own domain on an unconfigured kiosk) does not apply. +5. **Fail-closed dispatch.** An unrecognized mode string resolves to the safelist predicate. 1.2.0 failed open; the panel reversed that for 1.2.2 — a corrupted policy locks the kiosk to its home page rather than opening the perimeter. +6. **Blocked top-level navigations land on an in-extension block page** (`blocked.html`) that names the blocked domain via `textContent` only — no reflection of the URL parameter into markup. Blocked subresources are cancelled outright. +7. **One engine, three consumers.** The decision logic lives in `extension/url-policy.js` (pure, browser-API-free, UMD-lite export): the background script's webRequest wiring, the wizard's live validation and save-time check, and the Node unit suite all call the same functions. The wizard cannot disagree with runtime enforcement. + +### Verification performed in this pass + +- **`scripts/test-url-policy.js`: 62/62 assertions pass** under Node 24 — entry normalization (domains, wildcards, pasted URLs, ports, junk), home-hostname extraction, safelist matching (exact, subdomain, deep subdomain, sibling non-match, three smuggle classes, empty-hostname `file:`), internal-scheme and data:/blob: handling, empty-safelist posture, home-origin guarantee (exact match, subdomain non-coverage, suffix-spoof non-match), legacy-mode behavior parity (including the documented empty-allowlist quirk), fail-closed unknown mode, and startup adoption (positive case plus five refusal cases). +- The unit suite caught one real defect before ship: the data:/blob: branch of the safelist predicate was inverted (`!ctx.mainFrame` instead of `ctx.mainFrame`), which would have allowed top-level `data:` navigations and blocked data: subresources. Fixed; the four assertions now pin the correct polarity. +- `node --check` on all seven extension scripts; `sh -n` on validate.sh after the new gate; JSON/XML validity re-verified by the full `scripts/validate.sh` run (all gates green, including the new version-consistency gate at 1.2.2 and the new URL-policy gate). +- XPI smoke test now asserts `url-policy.js` loads first in the background script array and that `blocked.html` ships in the bundle. +- Version consistency: 1.2.2 declared in Cargo.toml, Cargo.lock (usher row only — `static_assertions` legitimately pins 1.2.0), extension manifest, Inno Setup ×2, Flatpak CLI, build-flatpak.sh, metainfo (new 1.2.2 release entry), validate.sh ×3, CI ×5, build-installer.ps1, integration test, QUICKSTART console lines, DEPLOYMENT artifacts table and checklists. + +### Honest limitations (1.2.2) + +1. **Safelist mode gates subresources like navigations.** A safelisted page pulling scripts or fonts from a CDN renders broken until the CDN domain is listed. Deliberate (data can leave through subresource requests) and documented in the wizard help text, README, and QUICKSTART — but the operator's first configuration pass will involve adding domains until the page renders whole. The console logs each block with the exact URL. +2. **Startup adoption inspects the tab list once at background boot.** A browser that restores a session (non-kiosk deployment) adopts the first restored http(s) tab while the policy is default. Kiosk deployments boot to a single page; the behavior is correct there, and any wizard save ends adoption permanently. +3. **The redirect to `blocked.html` uses `webRequest` `redirectUrl`,** which discards POST state on blocked form submissions. Acceptable — the submission was refused either way — and irrelevant to GET-based kiosk content. +4. **Engine tests run under Node; the webRequest wiring itself is CI-exercised, not unit-exercised.** The wiring is 20 lines of state-and-listener code; the decision table it delegates to is fully covered. + +--- + +*Review conducted 2026-08-24 by the Mixture-of-Experts panel. Author: Jeremy Anderson — [dcos.net](https://dcos.net) — info@dcos.net.* diff --git a/extension/admin.css b/extension/admin.css new file mode 100755 index 0000000..2239d28 --- /dev/null +++ b/extension/admin.css @@ -0,0 +1,400 @@ +/* Vestibule admin wizard — production styling. + * Clean, professional, accessible. This is an operator-facing tool, not + * a patient-facing UI — favor clarity over decoration. + */ + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: #0f1116; + color: #e8eaed; + font-family: -apple-system, "Segoe UI", system-ui, sans-serif; + font-size: 14px; + line-height: 1.5; + min-height: 100vh; +} + +/* ─── Auth views ─── */ +.auth-view { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.auth-card { + background: #1a1d24; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 32px 40px; + max-width: 480px; + width: 100%; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); +} + +.auth-card h1 { + margin: 0 0 8px; + font-size: 22px; + font-weight: 600; + color: #fff; +} + +.auth-sub { + margin: 0 0 24px; + color: #b8bcc4; + font-size: 13px; +} + +.auth-warn { + margin: 16px 0 24px; + padding: 12px 16px; + background: rgba(251, 191, 36, 0.08); + border: 1px solid rgba(251, 191, 36, 0.25); + border-radius: 8px; + font-size: 12px; + color: #fbbf24; +} + +.auth-warn code { + background: rgba(0, 0, 0, 0.3); + padding: 1px 6px; + border-radius: 4px; + font-family: "SF Mono", "Cascadia Mono", Consolas, monospace; + font-size: 11px; +} + +.auth-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +/* ─── Form controls ─── */ +label { + display: block; + margin: 12px 0; +} + +label > span { + display: block; + margin-bottom: 4px; + font-size: 13px; + color: #b8bcc4; +} + +label > span em { + color: #6b7280; + font-style: italic; + font-weight: normal; +} + +label small { + display: block; + margin-top: 4px; + font-size: 11px; + color: #6b7280; + line-height: 1.4; +} + +input[type="text"], +input[type="url"], +input[type="password"], +input[type="number"], +textarea { + width: 100%; + padding: 8px 12px; + background: #0f1116; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + color: #e8eaed; + font: inherit; + font-size: 13px; + transition: border-color 120ms ease, box-shadow 120ms ease; +} + +input[type="text"]:focus, +input[type="url"]:focus, +input[type="password"]:focus, +input[type="number"]:focus, +textarea:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +textarea { + font-family: "SF Mono", "Cascadia Mono", Consolas, monospace; + font-size: 12px; + resize: vertical; + min-height: 80px; +} + +fieldset { + margin: 16px 0; + padding: 12px 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; +} + +legend { + padding: 0 8px; + color: #b8bcc4; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +label.radio { + display: flex; + align-items: flex-start; + gap: 10px; + margin: 8px 0; + cursor: pointer; +} + +label.radio input[type="radio"] { + margin-top: 3px; + accent-color: #3b82f6; +} + +label.radio span { + color: #e8eaed; + font-size: 13px; +} + +label.radio span strong { + color: #fff; + font-weight: 600; +} + +label.radio input:disabled + span { + color: #6b7280; + cursor: not-allowed; +} + +label.radio input:disabled + span strong { + color: #9ca3af; +} + +.field-error { + color: #ef4444; + font-size: 12px; + margin: 8px 0; +} + +/* ─── Buttons ─── */ +button { + padding: 8px 20px; + border: 1px solid transparent; + border-radius: 6px; + font: inherit; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 120ms ease, border-color 120ms ease; +} + +button.primary { + background: #3b82f6; + color: #fff; + border-color: #3b82f6; +} + +button.primary:hover { + background: #2563eb; + border-color: #2563eb; +} + +button.primary:active { + background: #1d4ed8; + border-color: #1d4ed8; +} + +button.primary:disabled { + background: #1e3a5f; + border-color: #1e3a5f; + color: #6b7280; + cursor: not-allowed; +} + +button.secondary { + background: transparent; + color: #b8bcc4; + border-color: rgba(255, 255, 255, 0.15); +} + +button.secondary:hover { + background: rgba(255, 255, 255, 0.06); + color: #fff; +} + +/* ─── Wizard layout ─── */ +#wizard-view, #view-wizard { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.wizard-header { + background: #1a1d24; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding: 16px 32px; +} + +.wizard-header h1 { + margin: 0 0 12px; + font-size: 18px; + font-weight: 600; + color: #fff; +} + +.wizard-steps ol { + list-style: none; + margin: 0; + padding: 0; + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.wizard-steps li { + padding: 6px 12px; + font-size: 12px; + color: #6b7280; + border: 1px solid transparent; + border-radius: 4px; + cursor: default; +} + +.wizard-steps li.active { + color: #3b82f6; + border-color: rgba(59, 130, 246, 0.3); + background: rgba(59, 130, 246, 0.08); +} + +.wizard-steps li.completed { + color: #10b981; +} + +.wizard-body { + flex: 1; + padding: 24px 32px; + max-width: 720px; + width: 100%; + margin: 0 auto; +} + +.step h2 { + margin: 0 0 8px; + font-size: 18px; + font-weight: 600; + color: #fff; +} + +.step-intro { + margin: 0 0 20px; + color: #b8bcc4; + font-size: 13px; + line-height: 1.6; +} + +.step-intro code { + background: rgba(0, 0, 0, 0.3); + padding: 1px 6px; + border-radius: 4px; + font-family: "SF Mono", "Cascadia Mono", Consolas, monospace; + font-size: 11px; + color: #fbbf24; +} + +.step-intro strong { + color: #fff; +} + +.review-output { + background: #0a0c10; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + padding: 16px; + font-family: "SF Mono", "Cascadia Mono", Consolas, monospace; + font-size: 12px; + color: #b8bcc4; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + max-height: 400px; + overflow-y: auto; +} + +.review-actions { + display: flex; + gap: 8px; + margin: 20px 0; +} + +.save-result { + padding: 12px 16px; + border-radius: 8px; + font-size: 13px; +} + +.save-result.success { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); + color: #10b981; +} + +.save-result.error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #ef4444; +} + +/* ─── Wizard nav footer ─── */ +.wizard-nav { + background: #1a1d24; + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding: 12px 32px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.wizard-progress { + font-size: 12px; + color: #6b7280; +} + +/* ─── Homepage × safelist status (wizard step 1) ─── */ +.domain-status { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin: -4px 0 16px; + padding: 10px 14px; + border-radius: 8px; + font-size: 12px; +} + +.domain-status.ok { + background: rgba(16, 185, 129, 0.08); + border: 1px solid rgba(16, 185, 129, 0.25); + color: #10b981; +} + +.domain-status.warn { + background: rgba(251, 191, 36, 0.08); + border: 1px solid rgba(251, 191, 36, 0.25); + color: #fbbf24; +} + +.domain-status .add-domain { + padding: 4px 12px; + font-size: 11px; + margin: 0; +} diff --git a/extension/admin.html b/extension/admin.html new file mode 100755 index 0000000..f261709 --- /dev/null +++ b/extension/admin.html @@ -0,0 +1,281 @@ + + + + + Vestibule Admin + + + + + + + + + + + + + + + + + diff --git a/extension/admin.js b/extension/admin.js new file mode 100755 index 0000000..da28f07 --- /dev/null +++ b/extension/admin.js @@ -0,0 +1,542 @@ +// Vestibule admin wizard — production implementation. +// +// Two-phase UI: +// Phase A (auth gate): +// - No admin password set → setup view (first run) +// - Admin password set → enter view (returning) +// Phase B (wizard): +// - Six steps, navigated with prev/next +// - Final step shows JSON review and save button +// +// Credential split (two passwords, two surfaces): +// Admin password — gates this wizard. PBKDF2-SHA-256, 100k +// iterations, 16-byte salt, in-browser via Web Crypto. +// Kiosk unlock password — releases the session. Argon2id in +// usher, file storage at 0600 (see helper/src/crypto.rs). + +const PBKDF2_ITERATIONS = 100_000; +const PBKDF2_HASH = "SHA-256"; +const PBKDF2_KEYLEN_BITS = 256; +const SALT_BYTES = 16; +const MIN_PASSWORD_LENGTH = 8; +const TOTAL_STEPS = 6; + +// URL policy engine — shared with the background script. Loaded via +// + + diff --git a/extension/blocked.js b/extension/blocked.js new file mode 100644 index 0000000..1da7591 --- /dev/null +++ b/extension/blocked.js @@ -0,0 +1,24 @@ +// Vestibule blocked page — shown when a top-level navigation is +// denied by the URL policy (safelist mode). +// +// The attempted URL arrives as the ?u= query parameter. Only the +// hostname is displayed, and only via textContent — the parameter is +// never written into the DOM as markup. + +(() => { + const params = new URLSearchParams(location.search); + const attempted = params.get("u") || ""; + + let host = attempted; + try { + host = new URL(attempted).hostname; + } catch (e) { + /* unparsable — display the raw value; textContent makes it inert */ + } + + document.getElementById("block-domain").textContent = host; + + document.getElementById("btn-home").addEventListener("click", () => { + browser.runtime.sendMessage({ type: "navigate-home" }).catch(() => {}); + }); +})(); diff --git a/extension/content.css b/extension/content.css new file mode 100755 index 0000000..02fc5fb --- /dev/null +++ b/extension/content.css @@ -0,0 +1,133 @@ +/* Vestibule hidden menu — production styling. + * The menu is injected by content.js as a fixed-position overlay at the + * top-center of the viewport. It slides down when the mouse touches the + * top 3px of the screen, slides back up after 1.5s of mouse-leave. + */ + +#vestibule-menu { + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%) translateY(-100%); + z-index: 2147483647; /* max int — always on top of host page */ + display: flex; + align-items: center; + gap: 4px; + padding: 6px 10px; + background: rgba(15, 17, 22, 0.96); + border: 1px solid rgba(255, 255, 255, 0.12); + border-top: none; + border-radius: 0 0 10px 10px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); + font-family: -apple-system, "Segoe UI", system-ui, sans-serif; + font-size: 14px; + color: #e8eaed; + opacity: 0; + transition: transform 180ms ease, opacity 180ms ease; + pointer-events: none; + user-select: none; +} + +#vestibule-menu.vestibule-visible { + transform: translateX(-50%) translateY(0); + opacity: 1; + pointer-events: auto; +} + +#vestibule-menu button { + background: transparent; + border: 1px solid transparent; + color: inherit; + font: inherit; + font-size: 16px; + padding: 4px 8px; + border-radius: 6px; + cursor: pointer; + line-height: 1; + min-width: 28px; +} + +#vestibule-menu button:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.15); +} + +#vestibule-menu button:active { + background: rgba(255, 255, 255, 0.18); +} + +#vestibule-menu button:focus-visible { + outline: 2px solid #6aa6ff; + outline-offset: 1px; +} + +#vestibule-menu .vestibule-divider { + width: 1px; + height: 20px; + background: rgba(255, 255, 255, 0.15); + margin: 0 2px; +} + +/* Lock overlay — shown after session reset (idle timeout or wake). + * Covers the entire viewport with a dark blur, presents a single + * "Unlock" CTA. The host page is still loaded behind it (so a tab + * refresh isn't required on unlock) but cannot be interacted with. */ +#vestibule-lock-overlay { + position: fixed; + inset: 0; + z-index: 2147483646; /* one less than the hidden menu */ + display: flex; + align-items: center; + justify-content: center; + background: rgba(8, 10, 14, 0.92); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + font-family: -apple-system, "Segoe UI", system-ui, sans-serif; + color: #e8eaed; +} + +#vestibule-lock-overlay .vestibule-lock-card { + text-align: center; + max-width: 480px; + padding: 32px 48px; + background: rgba(28, 32, 40, 0.95); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6); +} + +#vestibule-lock-overlay h1 { + margin: 0 0 16px; + font-size: 22px; + font-weight: 600; + color: #fff; +} + +#vestibule-lock-overlay p { + margin: 0 0 12px; + font-size: 14px; + line-height: 1.5; + color: #b8bcc4; +} + +#vestibule-lock-overlay button { + margin-top: 16px; + padding: 10px 24px; + background: #3b82f6; + border: none; + border-radius: 8px; + color: #fff; + font: inherit; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 120ms ease; +} + +#vestibule-lock-overlay button:hover { + background: #2563eb; +} + +#vestibule-lock-overlay button:active { + background: #1d4ed8; +} diff --git a/extension/content.js b/extension/content.js new file mode 100755 index 0000000..59dc098 --- /dev/null +++ b/extension/content.js @@ -0,0 +1,127 @@ +// Vestibule content script — production implementation. +// +// Single responsibility: inject and manage the hidden menu overlay +// and the lock overlay. The 3px-from-top gesture is the direct +// descendant of the 2001 VB6 Form_MouseMove handler (Y <= 3). + +const EDGE_PX = 3; +const HIDE_DELAY_MS = 1500; + +let menuEl = null; +let hideTimer = null; + +// ─── Menu construction (data-driven) ────────────────────────────────── + +const MENU_ACTIONS = [ + { action: "back", label: "Back", symbol: "\u25C0", handler: () => history.back() }, + { action: "forward", label: "Forward", symbol: "\u25B6", handler: () => history.forward() }, + { action: "refresh", label: "Refresh", symbol: "\u21BB", handler: () => location.reload() }, + { action: "home", label: "Home", symbol: "\u2302", handler: () => browser.runtime.sendMessage({ type: "navigate-home" }).catch(() => {}) }, + { divider: true }, + { action: "unlock", label: "Unlock", symbol: "\uD83D\uDD12", handler: openUnlockPopup }, + { action: "admin", label: "Admin", symbol: "\u2699", handler: () => browser.runtime.sendMessage({ type: "open-admin" }).catch(() => {}) }, +]; + +function buildMenu() { + const menu = document.createElement("div"); + menu.id = "vestibule-menu"; + menu.setAttribute("role", "toolbar"); + menu.setAttribute("aria-label", "Vestibule controls"); + + MENU_ACTIONS.forEach((item) => { + if (item.divider) { + const span = document.createElement("span"); + span.className = "vestibule-divider"; + menu.appendChild(span); + return; + } + const btn = document.createElement("button"); + btn.dataset.action = item.action; + btn.title = item.label; + btn.setAttribute("aria-label", item.label); + btn.textContent = item.symbol; + btn.addEventListener("click", () => { + item.handler(); + hideMenu(); + }); + menu.appendChild(btn); + }); + + menu.addEventListener("mouseenter", () => clearTimeout(hideTimer)); + menu.addEventListener("mouseleave", scheduleHide); + return menu; +} + +function ensureMenu() { + if (menuEl && document.body.contains(menuEl)) return menuEl; + menuEl = buildMenu(); + document.documentElement.appendChild(menuEl); + return menuEl; +} + +// ─── Show / hide (step-down) ────────────────────────────────────────── + +function showMenu() { + clearTimeout(hideTimer); + ensureMenu().classList.add("vestibule-visible"); +} + +function hideMenu() { + if (menuEl) menuEl.classList.remove("vestibule-visible"); +} + +function scheduleHide() { + clearTimeout(hideTimer); + hideTimer = setTimeout(hideMenu, HIDE_DELAY_MS); +} + +// ─── Unlock popup ───────────────────────────────────────────────────── + +function openUnlockPopup() { + browser.runtime.sendMessage({ type: "open-unlock-popup" }).catch(() => {}); +} + +// ─── Lock overlay ───────────────────────────────────────────────────── + +function showLockOverlay() { + if (document.getElementById("vestibule-lock-overlay")) return; + + const overlay = document.createElement("div"); + overlay.id = "vestibule-lock-overlay"; + overlay.innerHTML = ` +
+

Session Reset

+

This kiosk has been idle. Your browsing data has been cleared.

+

Click Unlock to start a new session.

+ +
+ `; + overlay.addEventListener("click", (e) => { + if (!e.target.closest("button[data-action='unlock']")) return; + openUnlockPopup(); + }); + document.documentElement.appendChild(overlay); +} + +function hideLockOverlay() { + const overlay = document.getElementById("vestibule-lock-overlay"); + if (overlay) overlay.remove(); +} + +// ─── Event wiring ───────────────────────────────────────────────────── + +document.addEventListener("mousemove", (e) => { + if (e.clientY <= EDGE_PX) showMenu(); +}, { passive: true }); + +const RUNTIME_MESSAGE_HANDLERS = { + "show-lock-overlay": showLockOverlay, + "hide-lock-overlay": hideLockOverlay, +}; + +browser.runtime.onMessage.addListener((msg) => { + const handler = RUNTIME_MESSAGE_HANDLERS[msg.type]; + if (handler) handler(msg); +}); + +console.log("[vestibule] content script loaded on", location.href); diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100755 index 0000000..e703de6 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,54 @@ +{ + "manifest_version": 3, + "name": "Vestibule", + "version": "1.2.2", + "description": "Kiosk lockdown browser extension for LibreWolf and Firefox — URL policy, session reset, admin wizard.", + "browser_specific_settings": { + "gecko": { + "id": "vestibule@vestibule.kiosk", + "strict_min_version": "115.0" + } + }, + "permissions": [ + "webRequest", + "webRequestBlocking", + "nativeMessaging", + "storage", + "tabs", + "activeTab", + "scripting", + "idle", + "commands" + ], + "host_permissions": [""], + "background": { + "scripts": ["url-policy.js", "background.js"] + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "css": ["content.css"], + "run_at": "document_idle" + } + ], + "web_accessible_resources": [ + { + "resources": [ + "admin.html", "admin.css", "admin.js", "url-policy.js", + "unlock.html", "unlock.css", "unlock.js", + "blocked.html", "blocked.css", "blocked.js" + ], + "matches": [""] + } + ], + "commands": { + "open-admin-wizard": { + "suggested_key": { + "default": "Ctrl+Shift+V", + "mac": "Command+Shift+V" + }, + "description": "Open the Vestibule admin configuration wizard" + } + } +} diff --git a/extension/unlock.css b/extension/unlock.css new file mode 100755 index 0000000..dbcb1a6 --- /dev/null +++ b/extension/unlock.css @@ -0,0 +1,97 @@ +/* Vestibule unlock popup — production styling. + * Centered card, dark theme, minimal chrome. This is a popup window + * (browser.windows.create type=popup), not a full tab. + */ + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: #0f1116; + color: #e8eaed; + font-family: -apple-system, "Segoe UI", system-ui, sans-serif; + font-size: 14px; + height: 100vh; + overflow: hidden; +} + +body { + display: flex; + align-items: center; + justify-content: center; +} + +.unlock-card { + text-align: center; + padding: 32px 40px; + width: 100%; + max-width: 320px; +} + +.unlock-card h1 { + margin: 0 0 8px; + font-size: 24px; + font-weight: 600; + color: #fff; + letter-spacing: 0.02em; +} + +.unlock-sub { + margin: 0 0 24px; + color: #b8bcc4; + font-size: 13px; +} + +#unlock-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +#unlock-password { + width: 100%; + padding: 10px 14px; + background: #1a1d24; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + color: #e8eaed; + font: inherit; + font-size: 15px; + text-align: center; + letter-spacing: 0.1em; + transition: border-color 120ms ease, box-shadow 120ms ease; +} + +#unlock-password:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +button.primary { + padding: 10px 20px; + background: #3b82f6; + border: none; + border-radius: 8px; + color: #fff; + font: inherit; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 120ms ease; +} + +button.primary:hover { background: #2563eb; } +button.primary:active { background: #1d4ed8; } +button.primary:disabled { background: #1e3a5f; cursor: not-allowed; } + +.field-error { + margin: 12px 0 0; + padding: 8px 12px; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 6px; + color: #ef4444; + font-size: 12px; +} diff --git a/extension/unlock.html b/extension/unlock.html new file mode 100755 index 0000000..b6965a2 --- /dev/null +++ b/extension/unlock.html @@ -0,0 +1,29 @@ + + + + + Vestibule Unlock + + + +
+

Vestibule

+

Enter the kiosk unlock code.

+ +
+ + +
+ + +
+ + + + diff --git a/extension/unlock.js b/extension/unlock.js new file mode 100755 index 0000000..5097053 --- /dev/null +++ b/extension/unlock.js @@ -0,0 +1,64 @@ +// Vestibule unlock popup — production implementation. +// +// Opens as a browser.windows.create popup. Sends the password to the +// background script, which forwards to usher for Argon2id verification. +// On success: the popup closes and the background clears the lock +// overlay + enters admin grace mode. On failure: shows error, clears +// input, refocuses. + +const REASON_MESSAGES = { + invalid: "Invalid unlock code.", + "not-configured": "No unlock password configured. Run the admin wizard (Ctrl+Shift+V).", +}; + +document.getElementById("unlock-form").addEventListener("submit", (e) => { + e.preventDefault(); + const password = document.getElementById("unlock-password").value; + if (!password) return; + + const submitBtn = e.target.querySelector("button[type=submit]"); + submitBtn.disabled = true; + submitBtn.textContent = "Verifying..."; + + browser.runtime + .sendMessage({ type: "unlock-attempt", password }) + .then(() => { + // Result arrives asynchronously via runtime.onMessage. + // The button re-enables when the result lands. + }) + .catch(() => { + resetForm("Communication error. Try again."); + }); +}); + +browser.runtime.onMessage.addListener((msg) => { + if (msg.type !== "unlock-result") return; + + if (msg.granted) { + window.close(); + return; + } + + const message = REASON_MESSAGES[msg.reason] || "Unlock failed."; + resetForm(message); +}); + +function resetForm(errorMessage) { + const input = document.getElementById("unlock-password"); + const submitBtn = document.querySelector("button[type=submit]"); + const errorEl = document.getElementById("unlock-error"); + + input.value = ""; + input.focus(); + submitBtn.disabled = false; + submitBtn.textContent = "Unlock"; + + if (errorMessage) { + errorEl.textContent = errorMessage; + errorEl.hidden = false; + } else { + errorEl.hidden = true; + } +} + +console.log("[vestibule-unlock] popup loaded"); diff --git a/extension/url-policy.js b/extension/url-policy.js new file mode 100644 index 0000000..1b92c41 --- /dev/null +++ b/extension/url-policy.js @@ -0,0 +1,156 @@ +// Vestibule URL policy engine — pure logic, no browser API calls. +// +// This module is the single source of truth for navigation decisions. +// It is loaded as a classic background script before background.js +// (which owns the webRequest wiring and policy state) and imported by +// scripts/test-url-policy.js under Node. The UMD-lite export keeps +// both worlds working from one file. +// +// Modes (one row per mode in MODE_TABLE): +// safelist — default-deny by domain. Every request whose hostname is +// not on the operator's safelist is blocked. Internal +// browser schemes and the configured home origin are +// always permitted. This is the default mode: a fresh +// install can load its home page and nothing else until +// the operator adds domains. +// open — no filtering. +// blocklist — substring block (legacy substring semantics). +// allowlist — substring allow (legacy substring semantics; an empty +// list allows everything — use safelist instead). +// +// Domain matching is hostname-based, never substring-based: the entry +// "example.org" permits example.org and any subdomain +// (portal.example.org), and nothing else. A URL whose query string +// merely contains "example.org" does not match — the failure mode of +// substring matching and the reason safelist mode exists. + +(function (root, factory) { + const api = factory(); + if (typeof module !== "undefined" && module.exports) { + module.exports = api; // Node (unit tests) + } else { + root.VestibuleUrlPolicy = api; // extension background / wizard page + } +})(typeof self !== "undefined" ? self : globalThis, function () { + // Schemes the browser itself needs. Blocking these breaks the admin + // wizard, the unlock popup, and about:blank — the kiosk would brick. + const INTERNAL_SCHEMES = ["about:", "moz-extension:", "chrome:", "resource:"]; + + // Same-document artifacts. Safe as subresources; blocked as + // top-level documents (a data: URL navigation is a known content- + // injection vector and has no legitimate kiosk use). + const DATA_LIKE_SCHEMES = ["data:", "blob:"]; + + const schemeOf = (url) => { + const idx = url.indexOf(":"); + return idx === -1 ? "" : url.slice(0, idx + 1).toLowerCase(); + }; + + const hostnameOf = (url) => { + try { + return new URL(url).hostname.toLowerCase(); + } catch (e) { + return ""; + } + }; + + // Normalize one operator-supplied safelist entry to a bare hostname. + // Accepts domains, wildcard-prefixed domains, and pasted URLs; + // returns null when nothing hostname-shaped can be extracted. + const normalizeDomainEntry = (entry) => { + const raw = String(entry || "").trim().toLowerCase(); + if (!raw) return null; + const stripped = raw.startsWith("*.") ? raw.slice(2) : raw; + const candidate = /^[a-z][a-z0-9+.-]*:/.test(stripped) || stripped.includes("/") + ? stripped // URL-ish — let the URL parser take it apart + : "http://" + stripped + "/"; // bare domain — synthesize a URL + const host = hostnameOf(candidate); + return host || null; + }; + + // Entry "example.org" matches hostname example.org and any + // subdomain of it. The leading-dot boundary is explicit: a sibling + // like evilexample.org must not match. + const hostMatchesEntry = (hostname, entry) => + hostname === entry || hostname.endsWith("." + entry); + + const domainAllowed = (url, safelist) => { + const hostname = hostnameOf(url); + if (!hostname) return false; // file:, malformed — nothing to match + const entries = (safelist || []) + .map(normalizeDomainEntry) + .filter((e) => e !== null); + return entries.some((entry) => hostMatchesEntry(hostname, entry)); + }; + + // The home origin is always navigable regardless of safelist + // contents: session reset, wake, and idle all navigate home, and a + // block there would leave the kiosk showing its own block page + // forever. Exact hostname match — subdomains of home are not + // covered; the operator lists them explicitly. + const homeHostnameOf = (homeUrl) => { + const host = hostnameOf(homeUrl || ""); + if (!host) return null; + const scheme = schemeOf(homeUrl); + return scheme === "http:" || scheme === "https:" ? host : null; + }; + + const substringMatches = (url, patterns) => + (patterns || []).some((pat) => + url.toLowerCase().includes(String(pat).toLowerCase()) + ); + + const SAFELIST_PREDICATE = (url, policy, ctx) => { + const scheme = schemeOf(url); + if (INTERNAL_SCHEMES.includes(scheme)) return false; // always allow + if (DATA_LIKE_SCHEMES.includes(scheme)) return ctx.mainFrame; // subresource only + if (domainAllowed(url, policy.safelist)) return false; + return hostnameOf(url) !== ctx.homeHostname; // home origin passes + }; + + // One row per mode. Unknown modes resolve to the safelist row: + // a corrupted or hand-edited policy string fails closed — the kiosk + // locks to its home page instead of opening the perimeter. + const MODE_TABLE = { + safelist: SAFELIST_PREDICATE, + open: () => false, + blocklist: (url, policy) => substringMatches(url, policy.blocklist), + allowlist: (url, policy) => + (policy.allowlist || []).length > 0 && !substringMatches(url, policy.allowlist), + }; + + const shouldBlockRequest = (url, policy, ctx) => { + const context = { + mainFrame: !!(ctx && ctx.mainFrame), + homeHostname: (ctx && ctx.homeHostname) || null, + }; + const predicate = MODE_TABLE[policy.mode] || MODE_TABLE.safelist; + return predicate(url, policy, context); + }; + + // First-boot adoption: a provisioned kiosk launches with its home URL + // on the command line (kiosk.env → --kiosk URL), which the extension + // cannot learn any other way. When the policy is still the default + // (about:blank home, empty safelist), the browser's startup page is + // adopted as the home URL and its domain becomes the first safelist + // entry. Runs once against the tab list at background startup — only + // pages the browser was launched with qualify, never later + // operator-typed navigations. Returns the new policy, or null when + // nothing should change. + const adoptStartupPolicy = (startupUrls, policy) => { + if (!policy || policy.homeUrl !== "about:blank") return null; + if ((policy.safelist || []).length > 0) return null; + const url = (startupUrls || []).find((u) => homeHostnameOf(u) !== null); + if (!url) return null; + return { ...policy, homeUrl: url, safelist: [homeHostnameOf(url)] }; + }; + + return { + INTERNAL_SCHEMES: INTERNAL_SCHEMES.slice(), + normalizeDomainEntry: normalizeDomainEntry, + hostnameOf: hostnameOf, + homeHostnameOf: homeHostnameOf, + shouldBlockRequest: shouldBlockRequest, + adoptStartupPolicy: adoptStartupPolicy, + }; +}); diff --git a/helper/Cargo.lock b/helper/Cargo.lock new file mode 100755 index 0000000..0ce8dea --- /dev/null +++ b/helper/Cargo.lock @@ -0,0 +1,1319 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "static_assertions" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "usher" +version = "1.2.2" +dependencies = [ + "argon2", + "dirs", + "serde", + "serde_json", + "windows", + "zbus", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/helper/Cargo.toml b/helper/Cargo.toml new file mode 100755 index 0000000..5b6d52c --- /dev/null +++ b/helper/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "usher" +version = "1.2.2" +edition = "2021" +description = "Native helper for Vestibule — kiosk unlock + power awareness" +license = "MIT" +authors = ["Jeremy Anderson "] +publish = false + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +argon2 = "0.5" +dirs = "5" + +[target.'cfg(unix)'.dependencies] +zbus = "4" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = [ + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_System_Power", + "Win32_System_LibraryLoader", +] } + +[profile.release] +opt-level = "z" +lto = true +strip = true +panic = "abort" +codegen-units = 1 diff --git a/helper/src/crypto.rs b/helper/src/crypto.rs new file mode 100755 index 0000000..f48b259 --- /dev/null +++ b/helper/src/crypto.rs @@ -0,0 +1,46 @@ +//! Argon2id password hashing for kiosk unlock verification. +//! +//! PHC (Password Hashing Competition) string format encodes algorithm, +//! version, parameters, salt, and hash in one self-describing string. +//! Store the PHC string; verify against it later. No separate salt +//! management needed. + +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Algorithm, Argon2, PasswordHash, PasswordHasher, PasswordVerifier, Params, Version, +}; + +/// Argon2id parameters — memory-hard, tuned for kiosk-class hardware. +/// 64 MiB memory cost, 3 iterations, 4 parallel lanes. +/// Tunable in Phase 2 via config file; hardcoded for Phase 1. +const M_COST: u32 = 65_536; +const T_COST: u32 = 3; +const P_COST: u32 = 4; + +fn argon2_instance() -> Argon2<'static> { + let params = Params::new(M_COST, T_COST, P_COST, None) + .expect("valid Argon2id parameters"); + Argon2::new(Algorithm::Argon2id, Version::V0x13, params) +} + +/// Hash a password and return the PHC string. +pub fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = argon2_instance(); + argon2 + .hash_password(password.as_bytes(), &salt) + .map(|h| h.to_string()) + .map_err(|e| e.to_string()) +} + +/// Verify a password against a PHC string. +/// Returns false on any failure (invalid hash, wrong password, malformed PHC). +pub fn verify_password(password: &str, phc: &str) -> bool { + let parsed = match PasswordHash::new(phc) { + Ok(p) => p, + Err(_) => return false, + }; + argon2_instance() + .verify_password(password.as_bytes(), &parsed) + .is_ok() +} diff --git a/helper/src/main.rs b/helper/src/main.rs new file mode 100755 index 0000000..a85e459 --- /dev/null +++ b/helper/src/main.rs @@ -0,0 +1,214 @@ +// usher — native helper for Vestibule +// +// Production implementation. No stubs. +// +// Threaded architecture (one responsibility per thread): +// main — stdin read loop, message dispatch +// power — OS power event subscription +// writer — owns stdout lock, drains the shared channel +// +// Native Messaging protocol: 4-byte little-endian length prefix + JSON +// payload over stdin/stdout. stdout is the message channel; all +// diagnostics go to stderr. + +use std::io::{self, Read, Write}; +use std::sync::mpsc; +use std::thread; + +use serde::{Deserialize, Serialize}; + +mod crypto; +mod power; +mod storage; + +// ─── Protocol types ─────────────────────────────────────────────────── + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +enum Incoming { + Hello { client: String, version: String }, + Ping { echo: Option }, + Unlock { password: String }, + SetUnlock { password: String }, + /// Test affordance: triggers an outgoing Wake event without an actual + /// suspend. Exercises the real Wake emission path for CI. Harmless in + /// production — it only triggers a session reset, which is the same + /// behavior as an actual wake event. + SimulateWake, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(tag = "type", rename_all = "kebab-case")] +enum Outgoing { + Hello { server: String, version: String }, + Pong { echo: String }, + UnlockResult { granted: bool, reason: Option }, + UnlockSet { ok: bool, error: Option }, + Wake { reason: String }, +} + +// ─── IO primitives ──────────────────────────────────────────────────── + +const MAX_MESSAGE_LEN: usize = 1_000_000; +const SHUTDOWN_GRACE_MS: u64 = 100; + +fn read_message(r: &mut R) -> io::Result> { + let mut len_buf = [0u8; 4]; + match r.read_exact(&mut len_buf) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e), + } + + let len = u32::from_le_bytes(len_buf) as usize; + if len == 0 || len > MAX_MESSAGE_LEN { + eprintln!("[usher] rejecting message length {}", len); + return Ok(None); + } + + let mut buf = vec![0u8; len]; + r.read_exact(&mut buf)?; + + Ok(serde_json::from_slice(&buf).map(Some).unwrap_or_else(|e| { + eprintln!("[usher] parse error: {}", e); + None + })) +} + +fn write_message(w: &mut W, msg: &Outgoing) -> io::Result<()> { + let bytes = serde_json::to_vec(msg).expect("serialize outgoing"); + let len = bytes.len() as u32; + w.write_all(&len.to_le_bytes())?; + w.write_all(&bytes)?; + w.flush() +} + +// ─── Message dispatch (table-driven, step-down) ─────────────────────── + +fn handle_message(msg: Incoming, tx: &mpsc::Sender) -> Option { + eprintln!("[usher] recv: {:?}", msg); + match msg { + Incoming::Hello { client, version } => { + eprintln!("[usher] hello from {} v{}", client, version); + Some(Outgoing::Hello { + server: "usher".into(), + version: env!("CARGO_PKG_VERSION").into(), + }) + } + Incoming::Ping { echo } => Some(Outgoing::Pong { + echo: echo.unwrap_or_default(), + }), + Incoming::Unlock { password } => Some(handle_unlock(&password)), + Incoming::SetUnlock { password } => Some(handle_set_unlock(&password)), + Incoming::SimulateWake => { + eprintln!("[usher] simulate-wake received, emitting Wake"); + let _ = tx.send(Outgoing::Wake { reason: "simulated".into() }); + None + } + } +} + +/// Real unlock verification: load PHC from disk, verify with Argon2id. +fn handle_unlock(password: &str) -> Outgoing { + match storage::load_hash() { + Ok(Some(phc)) => { + let granted = crypto::verify_password(password, &phc); + let reason = if granted { None } else { Some("invalid".into()) }; + Outgoing::UnlockResult { granted, reason } + } + Ok(None) => Outgoing::UnlockResult { + granted: false, + reason: Some("not-configured".into()), + }, + Err(e) => Outgoing::UnlockResult { + granted: false, + reason: Some(format!("error: {}", e)), + }, + } +} + +/// Real password setting: hash with Argon2id, store to disk (0600). +fn handle_set_unlock(password: &str) -> Outgoing { + match crypto::hash_password(password) { + Ok(phc) => match storage::store_hash(&phc) { + Ok(()) => { + eprintln!("[usher] unlock hash stored"); + Outgoing::UnlockSet { ok: true, error: None } + } + Err(e) => Outgoing::UnlockSet { + ok: false, + error: Some(format!("storage: {}", e)), + }, + }, + Err(e) => Outgoing::UnlockSet { + ok: false, + error: Some(format!("hash: {}", e)), + }, + } +} + +// ─── Main loop ──────────────────────────────────────────────────────── + +fn main() { + eprintln!("[usher] starting v{}", env!("CARGO_PKG_VERSION")); + + let (tx, rx) = mpsc::channel::(); + + spawn_writer(rx); + spawn_power_monitor(tx.clone()); + + run_stdin_loop(tx); + + // Grace period for the writer to flush queued messages. + // The power thread is blocked on an OS-level receive call (D-Bus on + // Linux, GetMessage on Windows) and cannot be interrupted without an + // async runtime. It has no cleanup: no open files, no unflushed state, + // no connections beyond what the kernel reclaims on process exit. + // This is the correct shutdown strategy for this architecture. + thread::sleep(std::time::Duration::from_millis(SHUTDOWN_GRACE_MS)); + eprintln!("[usher] shutdown complete"); + std::process::exit(0); +} + +fn spawn_writer(rx: mpsc::Receiver) { + thread::spawn(move || { + let stdout = io::stdout(); + let mut out = stdout.lock(); + for msg in rx { + if write_message(&mut out, &msg).is_err() { + eprintln!("[usher] writer error, exiting"); + break; + } + } + }); +} + +fn spawn_power_monitor(tx: mpsc::Sender) { + thread::spawn(move || power::run_power_monitor(tx)); +} + +fn run_stdin_loop(tx: mpsc::Sender) { + let stdin = io::stdin(); + let mut stdin_lock = stdin.lock(); + + loop { + let msg = match read_message(&mut stdin_lock) { + Ok(Some(m)) => m, + Ok(None) => { + eprintln!("[usher] stdin closed, exiting"); + return; + } + Err(e) => { + eprintln!("[usher] read error: {}, exiting", e); + return; + } + }; + + if let Some(reply) = handle_message(msg, &tx) { + if tx.send(reply).is_err() { + eprintln!("[usher] reply send failed, exiting"); + return; + } + } + } +} diff --git a/helper/src/power.rs b/helper/src/power.rs new file mode 100755 index 0000000..510d188 --- /dev/null +++ b/helper/src/power.rs @@ -0,0 +1,267 @@ +//! Cross-platform power event detection for usher. +//! +//! Linux: subscribes to `org.freedesktop.login1.Manager.PrepareForSleep` +//! via D-Bus. Emits `Outgoing::Wake` on resume (active=false). +//! +//! Windows: creates a message-only window, registers for suspend/resume +//! notifications via `RegisterSuspendResumeNotification`, and handles +//! `WM_POWERBROADCAST` in the window procedure. Emits `Outgoing::Wake` +//! on `PBT_APMRESUMEAUTOMATIC`. +//! +//! Design: the monitor is a long-running thread with no shared mutable +//! state beyond a static `Mutex>` used to pass events +//! from the Windows window procedure back to the channel. The thread +//! is kill-safe — process exit requires no cleanup on its side. + +use std::sync::mpsc::Sender; + +use crate::Outgoing; + +// ─── Linux implementation ───────────────────────────────────────────── + +#[cfg(unix)] +mod linux_impl { + use super::*; + use std::time::Duration; + use zbus::proxy; + + #[proxy( + default_service = "org.freedesktop.login1", + default_path = "/org/freedesktop/login1", + interface = "org.freedesktop.login1.Manager" + )] + trait LoginManager { + #[zbus(signal)] + fn prepare_for_sleep(&self, active: bool) -> zbus::Result<()>; + } + + const RECONNECT_DELAY_S: u64 = 5; + + pub fn run_power_monitor(tx: Sender) { + eprintln!("[usher] power: connecting to D-Bus system bus"); + + loop { + let conn = match connect_with_retry() { + Some(c) => c, + None => return, + }; + + let proxy = match LoginManagerProxyBlocking::new(&conn) { + Ok(p) => p, + Err(e) => { + eprintln!("[usher] power: login1 proxy failed: {}", e); + eprintln!("[usher] power: monitoring disabled (kiosk still works)"); + return; + } + }; + + eprintln!("[usher] power: subscribed to PrepareForSleep (login1.Manager)"); + + drain_signals(&proxy, &tx); + + eprintln!("[usher] power: signal stream ended, reconnecting"); + std::thread::sleep(Duration::from_secs(RECONNECT_DELAY_S)); + } + } + + fn connect_with_retry() -> Option { + loop { + match zbus::blocking::Connection::system() { + Ok(c) => return Some(c), + Err(e) => { + eprintln!( + "[usher] power: D-Bus connect failed: {}, retrying in {}s", + e, RECONNECT_DELAY_S + ); + std::thread::sleep(Duration::from_secs(RECONNECT_DELAY_S)); + } + } + } + } + + fn drain_signals(proxy: &LoginManagerProxyBlocking, tx: &Sender) { + let iterator = match proxy.receive_prepare_for_sleep() { + Ok(it) => it, + Err(e) => { + eprintln!("[usher] power: receive failed: {}", e); + return; + } + }; + + for signal in iterator { + match signal.args() { + Ok(args) => handle_prepare_for_sleep(args.active, tx), + Err(e) => eprintln!("[usher] power: deserialize error: {}", e), + } + } + } + + fn handle_prepare_for_sleep(active: bool, tx: &Sender) { + if active { + eprintln!("[usher] power: PrepareForSleep(true) — entering suspend"); + return; + } + eprintln!("[usher] power: PrepareForSleep(false) — woke from suspend"); + if let Err(e) = tx.send(Outgoing::Wake { reason: "suspend".into() }) { + eprintln!("[usher] power: channel send failed: {}", e); + } + } +} + +// ─── Windows implementation ─────────────────────────────────────────── + +#[cfg(windows)] +mod windows_impl { + use super::*; + use std::sync::Mutex; + use windows::core::w; + use windows::Win32::Foundation::{DefWindowProcW, HWND, LPARAM, LRESULT, WPARAM}; + use windows::Win32::System::LibraryLoader::GetModuleHandleW; + use windows::Win32::System::Power::{ + RegisterSuspendResumeNotification, UnregisterSuspendResumeNotification, + DEVICE_NOTIFY_WINDOW_HANDLE, + }; + use windows::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DispatchMessageW, GetMessageW, RegisterClassExW, TranslateMessage, + MSG, WINDOW_EX_STYLE, WINDOW_STYLE, WNDCLASSEXW, WM_DESTROY, WM_POWERBROADCAST, + WM_QUIT, + }; + + // Power broadcast event codes (from WinUser.h). + const PBT_APMRESUMEAUTOMATIC: u32 = 0x0012; + const PBT_APMRESUMESUSPEND: u32 = 0x0022; + const PBT_APMSUSPEND: u32 = 0x0006; + + // Channel for passing Wake events from the window procedure to the + // writer thread. Set once at startup; read on each power broadcast. + static TX: Mutex>> = Mutex::new(None); + + pub fn run_power_monitor(tx: Sender) { + *TX.lock().unwrap() = Some(tx); + eprintln!("[usher] power: creating message-only window for WM_POWERBROADCAST"); + + unsafe { + let hinst = GetModuleHandleW(None).unwrap_or_default(); + + let class_name = w!("VestibuleUsherPowerWnd"); + let wc = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + lpfnWndProc: Some(wnd_proc), + hInstance: hinst.into(), + lpszClassName: class_name, + ..Default::default() + }; + + if RegisterClassExW(&wc) == 0 { + eprintln!("[usher] power: RegisterClassExW failed"); + return; + } + + // HWND_MESSAGE creates a message-only window — invisible, + // no taskbar entry, receives only directly-post messages and + // messages from registered notifications. + let hwnd = CreateWindowExW( + WINDOW_EX_STYLE::default(), + class_name, + w!("Vestibule"), + WINDOW_STYLE::default(), + 0, 0, 0, 0, + HWND_MESSAGE, + None, + hinst, + None, + ); + + let hwnd = match hwnd { + Ok(h) => h, + Err(e) => { + eprintln!("[usher] power: CreateWindowExW failed: {}", e); + return; + } + }; + + // Register for suspend/resume notifications. Windows 8+. + // This ensures the message-only window receives WM_POWERBROADCAST + // even though it's not a top-level visible window. + let notify_handle = match RegisterSuspendResumeNotification( + hwnd, + DEVICE_NOTIFY_WINDOW_HANDLE, + ) { + Ok(h) => { + eprintln!("[usher] power: registered for suspend/resume notifications"); + h + } + Err(e) => { + eprintln!( + "[usher] power: RegisterSuspendResumeNotification failed: {}", + e + ); + eprintln!("[usher] power: monitoring disabled (kiosk still works)"); + return; + } + }; + + eprintln!("[usher] power: message loop running"); + + let mut msg = MSG::default(); + while GetMessageW(&mut msg, None, 0, 0).into() { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + let _ = UnregisterSuspendResumeNotification(notify_handle); + eprintln!("[usher] power: message loop exited"); + } + } + + extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wp: WPARAM, lp: LPARAM) -> LRESULT { + // Table-driven message dispatch — one arm per handled message. + match msg { + WM_POWERBROADCAST => handle_power_broadcast(wp.0 as u32), + WM_DESTROY => { + // Signal the message loop to exit (not expected in normal operation). + unsafe { + windows::Win32::UI::WindowsAndMessaging::PostQuitMessage(0); + } + LRESULT(0) + } + _ => unsafe { DefWindowProcW(hwnd, msg, wp, lp) }, + } + } + + fn handle_power_broadcast(event: u32) -> LRESULT { + match event { + PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => { + eprintln!("[usher] power: WM_POWERBROADCAST resume (event {})", event); + emit_wake("suspend"); + } + PBT_APMSUSPEND => { + eprintln!("[usher] power: WM_POWERBROADCAST suspend"); + } + _ => { + eprintln!("[usher] power: WM_POWERBROADCAST event {}", event); + } + } + LRESULT(1) // TRUE — acknowledge receipt + } + + fn emit_wake(reason: &str) { + if let Some(tx) = TX.lock().unwrap().as_ref() { + if let Err(e) = tx.send(Outgoing::Wake { reason: reason.into() }) { + eprintln!("[usher] power: channel send failed: {}", e); + } + } + } +} + +// ─── Platform dispatch ──────────────────────────────────────────────── + +#[cfg(unix)] +pub fn run_power_monitor(tx: Sender) { + linux_impl::run_power_monitor(tx) +} + +#[cfg(windows)] +pub fn run_power_monitor(tx: Sender) { + windows_impl::run_power_monitor(tx) +} diff --git a/helper/src/storage.rs b/helper/src/storage.rs new file mode 100755 index 0000000..405958a --- /dev/null +++ b/helper/src/storage.rs @@ -0,0 +1,81 @@ +//! File-based hash storage with restrictive permissions. +//! +//! Stores the Argon2id PHC string at: +//! Linux: ~/.config/vestibule/unlock.hash (mode 0600) +//! Windows: %APPDATA%\Vestibule\unlock.hash (default user ACL) +//! +//! File-based storage is the pragmatic choice for kiosk deployments. +//! OS keyring daemons (gnome-keyring, kwallet) are unavailable on +//! minimal kiosk compositors like cage. File permissions enforce +//! owner-only access; the kiosk user account is dedicated and +//! non-privileged. + +use std::fs; +use std::io; +use std::path::PathBuf; + +const HASH_FILE_NAME: &str = "unlock.hash"; +const CONFIG_SUBDIR: &str = "vestibule"; + +fn hash_file_path() -> Option { + dirs::config_dir().map(|d| d.join(CONFIG_SUBDIR).join(HASH_FILE_NAME)) +} + +/// Store the PHC string to disk. Creates the config directory if needed. +/// On Unix, sets file permissions to 0600 (owner read/write only). +pub fn store_hash(phc: &str) -> io::Result<()> { + let path = hash_file_path() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no config directory available"))?; + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + #[cfg(unix)] + { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&path)?; + file.write_all(phc.as_bytes())?; + } + + #[cfg(windows)] + { + fs::write(&path, phc)?; + } + + Ok(()) +} + +/// Load the PHC string from disk. Returns None if no hash is stored. +pub fn load_hash() -> io::Result> { + let path = match hash_file_path() { + Some(p) => p, + None => return Ok(None), + }; + + match fs::read_to_string(&path) { + Ok(s) => Ok(Some(s.trim().to_string())), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(e), + } +} + +/// Remove the hash file. Used for testing and full reset. +pub fn clear_hash() -> io::Result<()> { + let path = match hash_file_path() { + Some(p) => p, + None => return Ok(()), + }; + + match fs::remove_file(&path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + } +} diff --git a/history/README.md b/history/README.md new file mode 100644 index 0000000..ee5a622 --- /dev/null +++ b/history/README.md @@ -0,0 +1,86 @@ +# History — the 2001 VB6 original + +This directory is the source code of the kiosk browser Jeremy Anderson +wrote in Visual Basic 6 in December 2001, at age eighteen, while on +co-op from school for his first employer — a Boston-area MSP and +programming company that serviced medical offices. It solved one +problem: patients waiting in a lobby would misuse the computer, so the +computer needed to become a single-purpose, escape-proof browser. + +It ran in that lobby for years. Twenty-five years later it became +[Vestibule](../README.md). Nothing in Vestibule's build, runtime, or +packaging depends on this directory — it is a non-shipping historic +artifact, kept for provenance. + +## What is here + +Three snapshots of the same project, in the directory arrangement they +arrived in: + +| Directory | What it is | Date | +|---|---|---| +| `vb6-2001/original/` | The earliest iteration. The unlock code is read from `C:\windows\system\smt.txt`; a first-run setup screen lets the operator set it. | Dec 11–17, 2001 | +| `vb6-2001/` (top level) | The lobby build. The unlock code is hardcoded in the form source; home page `www.msn.com`. The `f`-suffixed files are a later development line with the browser-disable step commented out for testing. | Dec 12–18, 2001 | +| `vb6-2001/Final/` | The shipped build: the IEXPLORE.EXE step is active again, wrapped in an error-53 retry loop for machines where the file was already gone. Includes `frmBrowserfFloppy.frm`, a self-demo variant that navigates to `a:\description.html` with a blank unlock code, and `frmMain.frm` ("Reloader"), a leftover test stub. | Dec 18, 2001 – Jan 3, 2002 | + +`description.html` is the 2001-era readme, typos intact. + +## How it worked + +- A maximized, chromeless VB6 form hosting the Internet Explorer COM + control (`SHDOCVW.DLL`), `ControlBox = False`, `WindowState = 2`. +- `SystemParametersInfo(97, True)` — the undocumented Win9x + screensaver flag — disabled Ctrl+Alt+Del, Alt+Tab, and the Windows + key. +- `SetWindowPos` with `HWND_TOPMOST` kept the window on top. +- The menu hid by default and appeared when the mouse touched the top + edge of the screen (`picAddress_MouseMove`, `Y <= 10` twips) — the + gesture Vestibule still ships. +- On startup it renamed `IEXPLORE.EXE` to `IEXPLORE.bak` and deleted + the original: an escaped user found no browser to escape to. On + unlock it copied the file back. +- The unlock delay was a `GoTo` counting loop burning the CPU for half + a second. The lock screen used Comic Sans. + +It refused to run on NT, 2000, or XP — the screensaver trick does +nothing on NT-family kernels, and the About box says so. + +## The "System Security 1.5" strings + +The lock screen and About box carry the name and description of a +sibling application — a standalone system-lockdown tool the author +wrote alongside this one. The browser reused that app's lock-screen +UI wholesale, branding included. The `SmtSysMan` variable names and +the `smt.txt` unlock-code file belong to that lineage as well. The +strings are left in place: they are what the shipped binary displayed. + +## Scrub log (2026-08-24) + +The code is preserved byte-for-byte except for the following, applied +so the artifact carries no employer, school, or client identifiers: + +| Change | Where | +|---|---| +| Employer-branded project filenames renamed to the neutral `LobbyBrowser*`; project name and output executable inside each project file renamed to match; SourceSafe section headers follow the filenames | all `.vbp`, `.vbw`, `MSSCCPRJ.SCC` | +| Company version string (the school's initials) → empty | every `.vbp` | +| Hardcoded unlock code (the employer's initials) → `"REDACTED"` | `frmBrowser.frm`, `frmBrowserf.frm`, `Final/frmBrowserf.frm` | +| Employer name in the 2001 readme → "my co-op employer, a Boston-area MSP and programming company" | `description.html` | +| The floppy self-demo form's filename (which named a school) → `frmBrowserfFloppy.frm` | `Final/` | + +Three files were excluded and are not in this tree: + +- The compiled `.exe` — it embeds the original project name, the + school company string, and the unlock code in its version resource; + a binary cannot be scrubbed without a rebuild, and the artifact is + the source. +- `IEXPLORE.bak` — a copy of Microsoft's Internet Explorer executable + captured by the rename step. It is Microsoft's binary, not authored + code, and does not ship here. +- `test.txt` — an empty scratch file. + +## Viewing it + +The `.frm`/`.bas`/`.vbp` files are plain text and read fine anywhere. +Opening the project requires the Visual Basic 6 IDE, and running the +result requires Windows 95, 98, 98 SE, or ME — by design, it does +nothing on NT-family kernels, which includes every Windows since. diff --git a/history/vb6-2001/FRMBRO~2.log b/history/vb6-2001/FRMBRO~2.log new file mode 100644 index 0000000..b533299 --- /dev/null +++ b/history/vb6-2001/FRMBRO~2.log @@ -0,0 +1 @@ +Line 185: Class SHDocVwCtl.WebBrowser of control brwWebBrowser was not a loaded control class. diff --git a/history/vb6-2001/Final/LobbyBrowserf.vbp b/history/vb6-2001/Final/LobbyBrowserf.vbp new file mode 100644 index 0000000..79f120e --- /dev/null +++ b/history/vb6-2001/Final/LobbyBrowserf.vbp @@ -0,0 +1,38 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\WINDOWS\SYSTEM\stdole2.tlb#OLE Automation +Object={6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.3#0; COMCTL32.OCX +Object={EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0; SHDOCVW.DLL +Form=frmBrowserf.frm +Module=modMain; modMainf.bas +Startup="frmWebB" +HelpFile="" +ExeName32="LobbyBrowser.exe" +Path32=".." +Command32="" +Name="LobbyBrowser" +HelpContextID="0" +CompatibleMode="0" +MajorVer=1 +MinorVer=0 +RevisionVer=0 +AutoIncrementVer=0 +ServerSupportFiles=0 +VersionCompanyName="" +CompilationType=-1 +OptimizationType=0 +FavorPentiumPro(tm)=0 +CodeViewDebugInfo=0 +NoAliasing=0 +BoundsCheck=0 +OverflowCheck=0 +FlPointCheck=0 +FDIVCheck=0 +UnroundedFP=0 +StartMode=0 +Unattended=0 +Retained=0 +ThreadPerObject=0 +MaxNumberOfThreads=1 + +[MS Transaction Server] +AutoRefresh=1 diff --git a/history/vb6-2001/Final/LobbyBrowserf.vbw b/history/vb6-2001/Final/LobbyBrowserf.vbw new file mode 100644 index 0000000..c66c7a6 --- /dev/null +++ b/history/vb6-2001/Final/LobbyBrowserf.vbw @@ -0,0 +1,2 @@ +frmWebB = 88, 87, 499, 427, C, 24, 28, 479, 412, C +modMain = 66, 66, 424, 418, C diff --git a/history/vb6-2001/Final/MSSCCPRJ.SCC b/history/vb6-2001/Final/MSSCCPRJ.SCC new file mode 100644 index 0000000..9dd24f3 --- /dev/null +++ b/history/vb6-2001/Final/MSSCCPRJ.SCC @@ -0,0 +1,6 @@ +[SCC] +SCC=This is a source code control file + +[LobbyBrowserf.vbp] +SCC_Project_Name=this project is not under source code control +SCC_Aux_Path= diff --git a/history/vb6-2001/Final/frmBrowserf.frm b/history/vb6-2001/Final/frmBrowserf.frm new file mode 100644 index 0000000..c8b274e --- /dev/null +++ b/history/vb6-2001/Final/frmBrowserf.frm @@ -0,0 +1,586 @@ +VERSION 5.00 +Object = "{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0"; "SHDOCVW.DLL" +Begin VB.Form frmWebB + BackColor = &H00000000& + ClientHeight = 8595 + ClientLeft = 255 + ClientTop = 150 + ClientWidth = 11415 + ControlBox = 0 'False + LinkTopic = "Form1" + ScaleHeight = 8595 + ScaleWidth = 11415 + WindowState = 2 'Maximized + Begin VB.Frame fraAbout + BackColor = &H00000000& + Caption = "About System Security 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 3495 + Left = 3960 + TabIndex = 6 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.Label Label6 + BackColor = &H00000000& + Caption = "Web Browser Version 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 255 + Left = 480 + TabIndex = 16 + Top = 480 + Width = 2775 + End + Begin VB.Label Label1 + Alignment = 2 'Center + BackColor = &H00000000& + Caption = "Author: Jeremy Anderson" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 360 + TabIndex = 12 + Top = 2760 + Width = 2895 + End + Begin VB.Label Label2 + BackColor = &H00000000& + Caption = "For Windows 95 - 98 - 98 SE && ME" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 600 + TabIndex = 11 + Top = 960 + Width = 2535 + End + Begin VB.Label Label3 + Alignment = 2 'Center + BackColor = &H00000000& + Caption = $"frmBrowserf.frx":0000 + ForeColor = &H00FFFFFF& + Height = 855 + Left = 360 + TabIndex = 10 + Top = 1320 + Width = 3015 + End + Begin VB.Label Label4 + BackColor = &H00000000& + Caption = "NOTE: Will NOT work on windows 2000, NT, or XP." + ForeColor = &H00FFFFFF& + Height = 375 + Left = 360 + TabIndex = 9 + Top = 2280 + Width = 3015 + End + Begin VB.Label lblClose + BackColor = &H00000000& + Caption = "Close" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 375 + Left = 2880 + TabIndex = 8 + Top = 3000 + Width = 735 + End + Begin VB.Label Label5 + BackColor = &H00000000& + Caption = "Revision: 1" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 240 + TabIndex = 7 + Top = 3120 + Width = 1095 + End + End + Begin VB.Frame FraUnlock + BackColor = &H00000000& + Caption = "Unlock Code:" + BeginProperty Font + Name = "Comic Sans MS" + Size = 9.75 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 3960 + TabIndex = 3 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.TextBox SmtSysManTech2 + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + IMEMode = 3 'DISABLE + Left = 120 + PasswordChar = "*" + TabIndex = 5 + Top = 240 + Width = 2055 + End + Begin VB.CommandButton CmdOK + BackColor = &H00000000& + Caption = "OK" + BeginProperty Font + Name = "Comic Sans MS" + Size = 8.25 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + Left = 2280 + MaskColor = &H00000000& + TabIndex = 4 + Top = 240 + Width = 1335 + End + End + Begin VB.Timer timTimer + Enabled = 0 'False + Interval = 5 + Left = 5760 + Top = 1440 + End + Begin SHDocVwCtl.WebBrowser brwWebBrowser + Height = 4350 + Left = 120 + TabIndex = 0 + Top = 600 + Width = 5400 + ExtentX = 9525 + ExtentY = 7673 + ViewMode = 1 + Offline = 0 + Silent = 0 + RegisterAsBrowser= 0 + RegisterAsDropTarget= 0 + AutoArrange = -1 'True + NoClientEdge = -1 'True + AlignLeft = 0 'False + NoWebView = 0 'False + HideFileNames = 0 'False + SingleClick = 0 'False + SingleSelection = 0 'False + NoFolders = 0 'False + Transparent = 0 'False + ViewID = "{0057D0E0-3573-11CF-AE69-08002B2E1262}" + Location = "http:///" + End + Begin VB.PictureBox picAddress + Align = 1 'Align Top + BackColor = &H00000000& + BorderStyle = 0 'None + Height = 435 + Left = 0 + ScaleHeight = 435 + ScaleWidth = 11415 + TabIndex = 1 + TabStop = 0 'False + Top = 0 + Width = 11415 + Begin VB.ComboBox cboAddress + Height = 315 + Left = 120 + TabIndex = 2 + Top = 120 + Width = 4515 + End + End + Begin VB.Label lblTitle1 + BackColor = &H00000000& + Caption = "System Security 1.5" + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 3960 + TabIndex = 14 + Top = 600 + Visible = 0 'False + Width = 3975 + End + Begin VB.Label lblDisp + Alignment = 2 'Center + BackColor = &H00000000& + Caption = "Please enter your Unlock code." + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 0 + TabIndex = 15 + Top = 6840 + Visible = 0 'False + Width = 11895 + End + Begin VB.Label lblTitle2 + BackColor = &H00000000& + Caption = "Lock Out Screen " + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 4200 + TabIndex = 13 + Top = 1320 + Visible = 0 'False + Width = 3975 + End + Begin VB.Menu mnuBack + Caption = "&Back" + End + Begin VB.Menu mnuForward + Caption = "&Forward" + End + Begin VB.Menu mnuRefresh + Caption = "&Refresh" + End + Begin VB.Menu mnuStop + Caption = "S&top" + End + Begin VB.Menu mnuHome + Caption = "&Home" + End + Begin VB.Menu mnuSearch + Caption = "&Search" + End + Begin VB.Menu mnuPrintPage + Caption = "&Print WebPage" + End + Begin VB.Menu mnuGeneral + Caption = "&General" + Begin VB.Menu mnuAbout + Caption = "&About" + End + Begin VB.Menu mnuExit + Caption = "&Exit" + End + End +End +Attribute VB_Name = "frmWebB" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = True +Attribute VB_Exposed = False +'fix about to exit display problem + +Option Explicit +Public StartingAddress As String +Dim mbDontNavigateNow As Boolean +Private Sub Form_Load() +BeforeError: +commons True +On Error GoTo myer +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.EXE", "C:\Program Files\Internet Explorer\IEXPLORE.bak" +Kill "C:\Program Files\Internet Explorer\IEXPLORE.EXE" +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuPrintPage.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuSearch.Visible = False +On Error Resume Next + Me.Show + Form_Resize + cboAddress.Move 50 + cboAddress.Text = "www.msn.com" + cboAddress.AddItem cboAddress.Text + timTimer.Enabled = True + brwWebBrowser.Navigate "www.msn.com" +Exit Sub +myer: +'MsgBox "Error is " & Err.Description +If Err.Number = 53 Then +'MsgBox "equals 53" +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.bak", "C:\Program Files\Internet Explorer\IEXPLORE.exe" +On Error GoTo error2 +GoTo enderror2 +error2: +MsgBox "error 2 " & Err.Description +enderror2: +Kill "C:\Program Files\Internet Explorer\IEXPLORE.bak" +On Error GoTo error3 +GoTo enderror3 +error3: +MsgBox "error 3 " & Err.Description +enderror3: +GoTo BeforeError +End If +End Sub +Private Sub brwWebBrowser_DownloadComplete() + On Error Resume Next + End Sub +Private Sub brwWebBrowser_NavigateComplete(ByVal URL As String) + Dim i As Integer + Dim bFound As Boolean + For i = 0 To cboAddress.ListCount - 1 + If cboAddress.List(i) = brwWebBrowser.LocationURL Then + bFound = True + Exit For + End If + Next i + mbDontNavigateNow = True + If bFound Then + cboAddress.RemoveItem i + End If + cboAddress.AddItem brwWebBrowser.LocationURL, 0 + cboAddress.ListIndex = 0 + mbDontNavigateNow = False +End Sub +Private Sub cboAddress_Click() + If mbDontNavigateNow Then Exit Sub + timTimer.Enabled = True + brwWebBrowser.Navigate cboAddress.Text +End Sub +Private Sub cboAddress_KeyPress(KeyAscii As Integer) + On Error Resume Next + If KeyAscii = vbKeyReturn Then + cboAddress_Click + End If +End Sub +Private Sub Form_Resize() + If frmWebB.WindowState = 1 Then GoTo new1 + cboAddress.Width = Me.ScaleWidth - 200 + brwWebBrowser.Left = 200 + brwWebBrowser.Width = Me.ScaleWidth - 400 + brwWebBrowser.Height = Me.ScaleHeight - (picAddress.Top + picAddress.Height) - 200 +new1: +End Sub +Private Sub mnuBack_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoBack +Else +brwWebBrowser.GoBack +End If +End Sub +Private Sub mnuExit_Click() +FraUnlock.Visible = True +lblTitle1.Visible = True +lblTitle2.Visible = True +lblDisp.Visible = True +fraAbout.Visible = False +brwWebBrowser.Visible = False +cboAddress.Visible = False +End Sub +Private Sub mnuForward_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoForward +Else +brwWebBrowser.GoForward +End If +End Sub +Private Sub mnuHome_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoHome +Else +brwWebBrowser.GoHome +End If +End Sub +Private Sub mnuPrintPage_Click() +MsgBox " To print right click on the webpage its self, go down the list and click print." +End Sub +Private Sub mnuRefresh_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Refresh +Else +brwWebBrowser.Refresh +End If +End Sub +Private Sub mnuSearch_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoSearch +Else +brwWebBrowser.GoSearch +End If +End Sub +Private Sub mnuStop_Click() +timTimer.Enabled = False +brwWebBrowser.Stop +End Sub +Private Sub picAddress_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +If Not Y <= 10 Then +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuPrintPage.Visible = False +mnuSearch.Visible = False +Else +mnuBack.Visible = True +mnuPrintPage.Visible = True +mnuForward.Visible = True +mnuRefresh.Visible = True +mnuStop.Visible = True +mnuHome.Visible = True +mnuGeneral.Visible = True +mnuExit.Visible = True +mnuSearch.Visible = True +End If +End Sub +Private Sub timTimer_Timer() + If brwWebBrowser.Busy = False Then + timTimer.Enabled = False + Else + End If +End Sub +Private Sub cmdOK_Click() +If SmtSysManTech2.Text = "REDACTED" Then + CmdOK.Enabled = False + SmtSysManTech2.Enabled = False + FraUnlock.Enabled = False + lblDisp.Enabled = True + lblDisp.Caption = "Unlocked!" + frmWebB.Refresh + Dim time As Double +beginn: + time = time + 1 + If Val(time) = 20000 Then GoTo endd + GoTo beginn +endd: +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.bak", "C:\Program Files\Internet Explorer\IEXPLORE.exe" +Kill "C:\Program Files\Internet Explorer\IEXPLORE.bak" +commons False +End +Else +FraUnlock.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End If +End Sub +Private Sub fraAbout_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbWhite +End Sub +Private Sub lblClose_Click() +fraAbout.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End Sub +Private Sub lblClose_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbRed +End Sub +Private Sub mnuAbout_Click() +fraAbout.Visible = True +cboAddress.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = False +cboAddress.Visible = False +lblClose.ForeColor = vbWhite +End Sub +Private Sub SmtSysManTech2_KeyPress(KeyAscii As Integer) +If KeyAscii = 13 Then +cmdOK_Click +End If +End Sub + diff --git a/history/vb6-2001/Final/frmBrowserf.frx b/history/vb6-2001/Final/frmBrowserf.frx new file mode 100644 index 0000000..c435d75 Binary files /dev/null and b/history/vb6-2001/Final/frmBrowserf.frx differ diff --git a/history/vb6-2001/Final/frmBrowserfFloppy.frm b/history/vb6-2001/Final/frmBrowserfFloppy.frm new file mode 100644 index 0000000..0609306 --- /dev/null +++ b/history/vb6-2001/Final/frmBrowserfFloppy.frm @@ -0,0 +1,577 @@ +VERSION 5.00 +Object = "{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0"; "SHDOCVW.DLL" +Begin VB.Form frmWebB + BackColor = &H00000000& + ClientHeight = 8595 + ClientLeft = 255 + ClientTop = 150 + ClientWidth = 11415 + ControlBox = 0 'False + LinkTopic = "Form1" + ScaleHeight = 8595 + ScaleWidth = 11415 + WindowState = 2 'Maximized + Begin VB.Frame fraAbout + BackColor = &H00000000& + Caption = "About System Security 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 3495 + Left = 3960 + TabIndex = 6 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.Label Label6 + BackColor = &H00000000& + Caption = "Web Browser Version 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 255 + Left = 480 + TabIndex = 16 + Top = 480 + Width = 2775 + End + Begin VB.Label Label1 + Alignment = 2 'Center + BackColor = &H00000000& + Caption = "Author: Jeremy Anderson" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 360 + TabIndex = 12 + Top = 2760 + Width = 2895 + End + Begin VB.Label Label2 + BackColor = &H00000000& + Caption = "For Windows 95 - 98 - 98 SE && ME" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 600 + TabIndex = 11 + Top = 960 + Width = 2535 + End + Begin VB.Label Label3 + Alignment = 2 'Center + BackColor = &H00000000& + Caption = $"frmBrowserfFloppy.frx":0000 + ForeColor = &H00FFFFFF& + Height = 855 + Left = 360 + TabIndex = 10 + Top = 1320 + Width = 3015 + End + Begin VB.Label Label4 + BackColor = &H00000000& + Caption = "NOTE: Will NOT work on windows 2000, NT, or XP." + ForeColor = &H00FFFFFF& + Height = 375 + Left = 360 + TabIndex = 9 + Top = 2280 + Width = 3015 + End + Begin VB.Label lblClose + BackColor = &H00000000& + Caption = "Close" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 375 + Left = 2880 + TabIndex = 8 + Top = 3000 + Width = 735 + End + Begin VB.Label Label5 + BackColor = &H00000000& + Caption = "Revision: 1" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 240 + TabIndex = 7 + Top = 3120 + Width = 1095 + End + End + Begin VB.Frame FraUnlock + BackColor = &H00000000& + Caption = "Unlock Code:" + BeginProperty Font + Name = "Comic Sans MS" + Size = 9.75 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 3960 + TabIndex = 3 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.TextBox SmtSysManTech2 + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + IMEMode = 3 'DISABLE + Left = 120 + PasswordChar = "*" + TabIndex = 5 + Top = 240 + Width = 2055 + End + Begin VB.CommandButton CmdOK + BackColor = &H00000000& + Caption = "OK" + BeginProperty Font + Name = "Comic Sans MS" + Size = 8.25 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + Left = 2280 + MaskColor = &H00000000& + TabIndex = 4 + Top = 240 + Width = 1335 + End + End + Begin VB.Timer timTimer + Enabled = 0 'False + Interval = 5 + Left = 5760 + Top = 1440 + End + Begin SHDocVwCtl.WebBrowser brwWebBrowser + Height = 4350 + Left = 120 + TabIndex = 0 + Top = 600 + Width = 5400 + ExtentX = 9525 + ExtentY = 7673 + ViewMode = 1 + Offline = 0 + Silent = 0 + RegisterAsBrowser= 0 + RegisterAsDropTarget= 0 + AutoArrange = -1 'True + NoClientEdge = -1 'True + AlignLeft = 0 'False + ViewID = "{0057D0E0-3573-11CF-AE69-08002B2E1262}" + Location = "" + End + Begin VB.PictureBox picAddress + Align = 1 'Align Top + BackColor = &H00000000& + BorderStyle = 0 'None + Height = 435 + Left = 0 + ScaleHeight = 435 + ScaleWidth = 11415 + TabIndex = 1 + TabStop = 0 'False + Top = 0 + Width = 11415 + Begin VB.ComboBox cboAddress + Height = 315 + Left = 120 + TabIndex = 2 + Top = 120 + Width = 4515 + End + End + Begin VB.Label lblTitle1 + BackColor = &H00000000& + Caption = "System Security 1.5" + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 3960 + TabIndex = 14 + Top = 600 + Visible = 0 'False + Width = 3975 + End + Begin VB.Label lblDisp + Alignment = 2 'Center + BackColor = &H00000000& + Caption = "Please enter your Unlock code." + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 0 + TabIndex = 15 + Top = 6840 + Visible = 0 'False + Width = 11895 + End + Begin VB.Label lblTitle2 + BackColor = &H00000000& + Caption = "Lock Out Screen " + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 4200 + TabIndex = 13 + Top = 1320 + Visible = 0 'False + Width = 3975 + End + Begin VB.Menu mnuBack + Caption = "&Back" + End + Begin VB.Menu mnuForward + Caption = "&Forward" + End + Begin VB.Menu mnuRefresh + Caption = "&Refresh" + End + Begin VB.Menu mnuStop + Caption = "S&top" + End + Begin VB.Menu mnuHome + Caption = "&Home" + End + Begin VB.Menu mnuSearch + Caption = "&Search" + End + Begin VB.Menu mnuPrintPage + Caption = "&Print WebPage" + End + Begin VB.Menu mnuGeneral + Caption = "&General" + Begin VB.Menu mnuAbout + Caption = "&About" + End + Begin VB.Menu mnuExit + Caption = "&Exit" + End + End +End +Attribute VB_Name = "frmWebB" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = True +Attribute VB_Exposed = False +Option Explicit +Public StartingAddress As String +Dim mbDontNavigateNow As Boolean +Private Sub Form_Load() +BeforeError: +commons True +On Error GoTo myer +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.EXE", "C:\Program Files\Internet Explorer\IEXPLORE.bak" +Kill "C:\Program Files\Internet Explorer\IEXPLORE.EXE" +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuPrintPage.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuSearch.Visible = False +On Error Resume Next + Me.Show + Form_Resize + cboAddress.Move 50 + cboAddress.Text = "www.msn.com" + cboAddress.AddItem cboAddress.Text + timTimer.Enabled = True + brwWebBrowser.Navigate "a:\description.html" +Exit Sub +myer: +'MsgBox "Error is " & Err.Description +If Err.Number = 53 Then +'MsgBox "equals 53" +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.bak", "C:\Program Files\Internet Explorer\IEXPLORE.exe" +On Error GoTo error2 +GoTo enderror2 +error2: +MsgBox "error 2 " & Err.Description +enderror2: +Kill "C:\Program Files\Internet Explorer\IEXPLORE.bak" +On Error GoTo error3 +GoTo enderror3 +error3: +MsgBox "error 3 " & Err.Description +enderror3: +GoTo BeforeError +End If +End Sub +Private Sub brwWebBrowser_DownloadComplete() + On Error Resume Next + End Sub +Private Sub brwWebBrowser_NavigateComplete(ByVal URL As String) + Dim i As Integer + Dim bFound As Boolean + For i = 0 To cboAddress.ListCount - 1 + If cboAddress.List(i) = brwWebBrowser.LocationURL Then + bFound = True + Exit For + End If + Next i + mbDontNavigateNow = True + If bFound Then + cboAddress.RemoveItem i + End If + cboAddress.AddItem brwWebBrowser.LocationURL, 0 + cboAddress.ListIndex = 0 + mbDontNavigateNow = False +End Sub +Private Sub cboAddress_Click() + If mbDontNavigateNow Then Exit Sub + timTimer.Enabled = True + brwWebBrowser.Navigate cboAddress.Text +End Sub +Private Sub cboAddress_KeyPress(KeyAscii As Integer) + On Error Resume Next + If KeyAscii = vbKeyReturn Then + cboAddress_Click + End If +End Sub +Private Sub Form_Resize() + If frmWebB.WindowState = 1 Then GoTo new1 + cboAddress.Width = Me.ScaleWidth - 200 + brwWebBrowser.Left = 200 + brwWebBrowser.Width = Me.ScaleWidth - 400 + brwWebBrowser.Height = Me.ScaleHeight - (picAddress.Top + picAddress.Height) - 200 +new1: +End Sub +Private Sub mnuBack_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoBack +Else +brwWebBrowser.GoBack +End If +End Sub +Private Sub mnuExit_Click() +FraUnlock.Visible = True +lblTitle1.Visible = True +lblTitle2.Visible = True +lblDisp.Visible = True +brwWebBrowser.Visible = False +cboAddress.Visible = False +End Sub +Private Sub mnuForward_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoForward +Else +brwWebBrowser.GoForward +End If +End Sub +Private Sub mnuHome_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoHome +Else +brwWebBrowser.GoHome +End If +End Sub +Private Sub mnuPrintPage_Click() +MsgBox " To print right click on the webpage its self, go down the list and click print." +End Sub +Private Sub mnuRefresh_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Refresh +Else +brwWebBrowser.Refresh +End If +End Sub +Private Sub mnuSearch_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoSearch +Else +brwWebBrowser.GoSearch +End If +End Sub +Private Sub mnuStop_Click() +timTimer.Enabled = False +brwWebBrowser.Stop +End Sub +Private Sub picAddress_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +If Not Y <= 10 Then +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuPrintPage.Visible = False +mnuSearch.Visible = False +Else +mnuBack.Visible = True +mnuPrintPage.Visible = True +mnuForward.Visible = True +mnuRefresh.Visible = True +mnuStop.Visible = True +mnuHome.Visible = True +mnuGeneral.Visible = True +mnuExit.Visible = True +mnuSearch.Visible = True +End If +End Sub +Private Sub timTimer_Timer() + If brwWebBrowser.Busy = False Then + timTimer.Enabled = False + Else + End If +End Sub +Private Sub cmdOK_Click() +If SmtSysManTech2.Text = "" Then 'password + CmdOK.Enabled = False + SmtSysManTech2.Enabled = False + FraUnlock.Enabled = False + lblDisp.Enabled = True + lblDisp.Caption = "Unlocked!" + frmWebB.Refresh + Dim time As Double +beginn: + time = time + 1 + If Val(time) = 20000 Then GoTo endd + GoTo beginn +endd: +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.bak", "C:\Program Files\Internet Explorer\IEXPLORE.exe" +Kill "C:\Program Files\Internet Explorer\IEXPLORE.bak" +commons False +End +Else +FraUnlock.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End If +End Sub +Private Sub fraAbout_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbWhite +End Sub +Private Sub lblClose_Click() +fraAbout.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End Sub +Private Sub lblClose_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbRed +End Sub +Private Sub mnuAbout_Click() +fraAbout.Visible = True +cboAddress.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = False +cboAddress.Visible = False +lblClose.ForeColor = vbWhite +End Sub +Private Sub SmtSysManTech2_KeyPress(KeyAscii As Integer) +If KeyAscii = 13 Then +cmdOK_Click +End If +End Sub + diff --git a/history/vb6-2001/Final/frmBrowserfFloppy.frx b/history/vb6-2001/Final/frmBrowserfFloppy.frx new file mode 100644 index 0000000..c435d75 Binary files /dev/null and b/history/vb6-2001/Final/frmBrowserfFloppy.frx differ diff --git a/history/vb6-2001/Final/frmMain.frm b/history/vb6-2001/Final/frmMain.frm new file mode 100644 index 0000000..de39026 --- /dev/null +++ b/history/vb6-2001/Final/frmMain.frm @@ -0,0 +1,39 @@ +VERSION 5.00 +Begin VB.Form frmMain + Caption = "Reloader" + ClientHeight = 480 + ClientLeft = 60 + ClientTop = 345 + ClientWidth = 2055 + LinkTopic = "Form1" + ScaleHeight = 480 + ScaleWidth = 2055 + StartUpPosition = 3 'Windows Default + Begin VB.CommandButton cmdTest + Caption = "Test" + Height = 255 + Left = 600 + TabIndex = 0 + Top = 120 + Width = 1335 + End + Begin VB.Timer Timer1 + Interval = 1 + Left = 0 + Top = 0 + End +End +Attribute VB_Name = "frmMain" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = True +Attribute VB_Exposed = False +Private Sub cmdTest_Click() +Open "C:\test.txt" For Output As #1 +Print #1, "this is a test" +Close #1 +End Sub + +Private Sub Form_Load() + +End Sub diff --git a/history/vb6-2001/Final/modMainf.bas b/history/vb6-2001/Final/modMainf.bas new file mode 100644 index 0000000..dc5e6cb --- /dev/null +++ b/history/vb6-2001/Final/modMainf.bas @@ -0,0 +1,23 @@ +Attribute VB_Name = "modMain" +Public X As Long +Option Explicit +Private Declare Function SystemParametersInfo Lib _ +"user32" Alias "SystemParametersInfoA" (ByVal uAction _ +As Long, ByVal uParam As Long, ByVal lpvParam As Any, _ +ByVal fuWinIni As Long) As Long +Const FLAGS = 3 +Const HWND_TOPMOST = -1 +Const HWND_NOTOPMOST = -2 +Public SetTop As Boolean +Private Declare Function SetWindowPos Lib "user32" (ByVal h%, ByVal hb%, ByVal X%, ByVal Y%, ByVal cx%, ByVal cy%, ByVal f%) As Integer +Sub commons(Disablem As Boolean) + X = SystemParametersInfo(97, Disablem, CStr(1), 0) +End Sub +Sub AlwaysOnTop(FormName As Form, bOnTop As Boolean) +Dim wind As Integer +If bOnTop = False Then + wind% = SetWindowPos(FormName.Wnd, HWND_TOPMOST, 0, 0, 0, 0, FLAGS) +Else + wind% = SetWindowPos(FormName.hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, FLAGS) +End If +End Sub diff --git a/history/vb6-2001/LobbyBrowser.vbp b/history/vb6-2001/LobbyBrowser.vbp new file mode 100644 index 0000000..69f345b --- /dev/null +++ b/history/vb6-2001/LobbyBrowser.vbp @@ -0,0 +1,37 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\WINDOWS\SYSTEM\StdOle2.Tlb#OLE Automation +Object={6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.3#0; COMCTL32.OCX +Object={EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0; SHDOCVW.DLL +Form=frmBrowser.frm +Module=modMain; modMain.bas +Startup="frmWebB" +HelpFile="" +ExeName32="LobbyBrowser.exe" +Command32="" +Name="LobbyBrowser" +HelpContextID="0" +CompatibleMode="0" +MajorVer=1 +MinorVer=0 +RevisionVer=0 +AutoIncrementVer=0 +ServerSupportFiles=0 +VersionCompanyName="" +CompilationType=-1 +OptimizationType=0 +FavorPentiumPro(tm)=0 +CodeViewDebugInfo=0 +NoAliasing=0 +BoundsCheck=0 +OverflowCheck=0 +FlPointCheck=0 +FDIVCheck=0 +UnroundedFP=0 +StartMode=0 +Unattended=0 +Retained=0 +ThreadPerObject=0 +MaxNumberOfThreads=1 + +[MS Transaction Server] +AutoRefresh=1 diff --git a/history/vb6-2001/LobbyBrowser.vbw b/history/vb6-2001/LobbyBrowser.vbw new file mode 100644 index 0000000..91402ab --- /dev/null +++ b/history/vb6-2001/LobbyBrowser.vbw @@ -0,0 +1,2 @@ +frmWebB = 19, 113, 430, 453, , 22, 29, 477, 413, C +modMain = 66, 66, 424, 418, C diff --git a/history/vb6-2001/LobbyBrowserf.vbp b/history/vb6-2001/LobbyBrowserf.vbp new file mode 100644 index 0000000..3bfff42 --- /dev/null +++ b/history/vb6-2001/LobbyBrowserf.vbp @@ -0,0 +1,37 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#D:\WIN98\System32\stdole2.tlb#OLE Automation +Object={6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.3#0; COMCTL32.OCX +Object={EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0; shdocvw.dll +Form=frmBrowserf.frm +Module=modMain; modMainf.bas +Startup="frmWebB" +HelpFile="" +ExeName32="LobbyBrowser.exe" +Command32="" +Name="LobbyBrowser" +HelpContextID="0" +CompatibleMode="0" +MajorVer=1 +MinorVer=0 +RevisionVer=0 +AutoIncrementVer=0 +ServerSupportFiles=0 +VersionCompanyName="" +CompilationType=-1 +OptimizationType=0 +FavorPentiumPro(tm)=0 +CodeViewDebugInfo=0 +NoAliasing=0 +BoundsCheck=0 +OverflowCheck=0 +FlPointCheck=0 +FDIVCheck=0 +UnroundedFP=0 +StartMode=0 +Unattended=0 +Retained=0 +ThreadPerObject=0 +MaxNumberOfThreads=1 + +[MS Transaction Server] +AutoRefresh=1 diff --git a/history/vb6-2001/LobbyBrowserf.vbw b/history/vb6-2001/LobbyBrowserf.vbw new file mode 100644 index 0000000..91402ab --- /dev/null +++ b/history/vb6-2001/LobbyBrowserf.vbw @@ -0,0 +1,2 @@ +frmWebB = 19, 113, 430, 453, , 22, 29, 477, 413, C +modMain = 66, 66, 424, 418, C diff --git a/history/vb6-2001/MSSCCPRJ.SCC b/history/vb6-2001/MSSCCPRJ.SCC new file mode 100644 index 0000000..83310f7 --- /dev/null +++ b/history/vb6-2001/MSSCCPRJ.SCC @@ -0,0 +1,6 @@ +[SCC] +SCC=This is a source code control file + +[LobbyBrowser.vbp] +SCC_Project_Name=this project is not under source code control +SCC_Aux_Path= diff --git a/history/vb6-2001/description.html b/history/vb6-2001/description.html new file mode 100644 index 0000000..575bc6d --- /dev/null +++ b/history/vb6-2001/description.html @@ -0,0 +1,11 @@ + + +

This is a Full screen Web Browser, Written and designed by Jeremy Anderson

+

It was designed for my co-op employer, a Boston-area MSP and programming company

+

It purpose is to keep the Patients waiting in the lobby using the computer to only use online

+

They didn't want them to use anything else so I put in API calls to keep them from +using the Ctrl-Alt-Del, or any other shortcut keys. And Disabled the Internet Explorer

+

When you move the mouse three pixels away from the top of the screen a menu apears. +When You move it down again it disapears.

+ + diff --git a/history/vb6-2001/frmBrowser.frm b/history/vb6-2001/frmBrowser.frm new file mode 100644 index 0000000..465f8bd --- /dev/null +++ b/history/vb6-2001/frmBrowser.frm @@ -0,0 +1,573 @@ +VERSION 5.00 +Object = "{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0"; "SHDOCVW.DLL" +Begin VB.Form frmWebB + BackColor = &H00000000& + ClientHeight = 8595 + ClientLeft = 255 + ClientTop = 150 + ClientWidth = 11415 + ControlBox = 0 'False + LinkTopic = "Form1" + ScaleHeight = 8595 + ScaleWidth = 11415 + WindowState = 2 'Maximized + Begin VB.Frame fraAbout + BackColor = &H00000000& + Caption = "About System Security 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 3495 + Left = 4080 + TabIndex = 6 + Top = 2520 + Visible = 0 'False + Width = 3495 + Begin VB.Label Label6 + BackColor = &H00000000& + Caption = "Web Browser Version 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 255 + Left = 360 + TabIndex = 16 + Top = 480 + Width = 2775 + End + Begin VB.Label Label1 + BackColor = &H00000000& + Caption = "Author: Jeremy Anderson" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 840 + TabIndex = 12 + Top = 2760 + Width = 1935 + End + Begin VB.Label Label2 + BackColor = &H00000000& + Caption = "For Windows 95 - 98 - 98 SE && ME" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 480 + TabIndex = 11 + Top = 960 + Width = 2535 + End + Begin VB.Label Label3 + BackColor = &H00000000& + Caption = $"frmBrowser.frx":0000 + ForeColor = &H00FFFFFF& + Height = 855 + Left = 240 + TabIndex = 10 + Top = 1320 + Width = 3015 + End + Begin VB.Label Label4 + BackColor = &H00000000& + Caption = "NOTE: Will NOT work on windows 2000, NT, or XP." + ForeColor = &H00FFFFFF& + Height = 375 + Left = 240 + TabIndex = 9 + Top = 2280 + Width = 3015 + End + Begin VB.Label lblClose + BackColor = &H00000000& + Caption = "Close" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 375 + Left = 2640 + TabIndex = 8 + Top = 3000 + Width = 735 + End + Begin VB.Label Label5 + BackColor = &H00000000& + Caption = "Revision: 1" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 240 + TabIndex = 7 + Top = 3120 + Width = 855 + End + End + Begin VB.Frame FraUnlock + BackColor = &H00000000& + Caption = "Unlock Code:" + BeginProperty Font + Name = "Comic Sans MS" + Size = 9.75 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 3960 + TabIndex = 3 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.TextBox SmtSysManTech2 + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + IMEMode = 3 'DISABLE + Left = 120 + PasswordChar = "*" + TabIndex = 5 + Top = 240 + Width = 2055 + End + Begin VB.CommandButton CmdOK + BackColor = &H00000000& + Caption = "OK" + BeginProperty Font + Name = "Comic Sans MS" + Size = 8.25 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + Left = 2280 + MaskColor = &H00000000& + TabIndex = 4 + Top = 240 + Width = 1335 + End + End + Begin VB.Timer timTimer + Enabled = 0 'False + Interval = 5 + Left = 5760 + Top = 1440 + End + Begin SHDocVwCtl.WebBrowser brwWebBrowser + Height = 4350 + Left = 120 + TabIndex = 0 + Top = 600 + Width = 5400 + ExtentX = 9525 + ExtentY = 7673 + ViewMode = 1 + Offline = 0 + Silent = 0 + RegisterAsBrowser= 0 + RegisterAsDropTarget= 0 + AutoArrange = -1 'True + NoClientEdge = -1 'True + AlignLeft = 0 'False + NoWebView = 0 'False + HideFileNames = 0 'False + SingleClick = 0 'False + SingleSelection = 0 'False + NoFolders = 0 'False + Transparent = 0 'False + ViewID = "{0057D0E0-3573-11CF-AE69-08002B2E1262}" + Location = "http:///" + End + Begin VB.PictureBox picAddress + Align = 1 'Align Top + BackColor = &H00000000& + BorderStyle = 0 'None + Height = 435 + Left = 0 + ScaleHeight = 435 + ScaleWidth = 11415 + TabIndex = 1 + TabStop = 0 'False + Top = 0 + Width = 11415 + Begin VB.ComboBox cboAddress + Height = 315 + Left = 120 + TabIndex = 2 + Top = 120 + Width = 4515 + End + End + Begin VB.Label lblTitle1 + BackColor = &H00000000& + Caption = "System Security 1.5" + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 3960 + TabIndex = 14 + Top = 600 + Visible = 0 'False + Width = 3975 + End + Begin VB.Label lblDisp + Alignment = 2 'Center + BackColor = &H00000000& + Caption = "Please enter your Unlock code." + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 0 + TabIndex = 15 + Top = 6840 + Visible = 0 'False + Width = 11895 + End + Begin VB.Label lblTitle2 + BackColor = &H00000000& + Caption = "Lock Out Screen " + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 4200 + TabIndex = 13 + Top = 1320 + Visible = 0 'False + Width = 3975 + End + Begin VB.Menu mnuBack + Caption = "&Back" + End + Begin VB.Menu mnuForward + Caption = "&Forward" + End + Begin VB.Menu mnuRefresh + Caption = "&Refresh" + End + Begin VB.Menu mnuStop + Caption = "S&top" + End + Begin VB.Menu mnuHome + Caption = "&Home" + End + Begin VB.Menu mnuSearch + Caption = "&Search" + End + Begin VB.Menu mnuPrintPage + Caption = "&Print WebPage" + End + Begin VB.Menu mnuGeneral + Caption = "&General" + Begin VB.Menu mnuAbout + Caption = "&About" + End + Begin VB.Menu mnuExit + Caption = "&Exit" + End + End +End +Attribute VB_Name = "frmWebB" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = True +Attribute VB_Exposed = False +Option Explicit +Public StartingAddress As String +Dim mbDontNavigateNow As Boolean +Private Sub Form_Load() +On Error GoTo myer +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.EXE", "C:\Program Files\Internet Explorer\IEXPLORE.bak" +Kill "C:\Program Files\Internet Explorer\IEXPLORE.EXE" +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuPrintPage.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuSearch.Visible = False +SmtSysMan = "REDACTED" +commons True +On Error Resume Next + Me.Show + Form_Resize + cboAddress.Move 50 + cboAddress.Text = "www.msn.com" + cboAddress.AddItem cboAddress.Text + timTimer.Enabled = True + brwWebBrowser.Navigate "www.msn.com" +Exit Sub +myer: +MsgBox "error is " & Err.Description +End Sub +Private Sub brwWebBrowser_DownloadComplete() + On Error Resume Next + End Sub +Private Sub brwWebBrowser_NavigateComplete(ByVal URL As String) + Dim i As Integer + Dim bFound As Boolean + For i = 0 To cboAddress.ListCount - 1 + If cboAddress.List(i) = brwWebBrowser.LocationURL Then + bFound = True + Exit For + End If + Next i + mbDontNavigateNow = True + If bFound Then + cboAddress.RemoveItem i + End If + cboAddress.AddItem brwWebBrowser.LocationURL, 0 + cboAddress.ListIndex = 0 + mbDontNavigateNow = False +End Sub +Private Sub cboAddress_Click() + If mbDontNavigateNow Then Exit Sub + timTimer.Enabled = True + brwWebBrowser.Navigate cboAddress.Text +End Sub +Private Sub cboAddress_KeyPress(KeyAscii As Integer) + On Error Resume Next + If KeyAscii = vbKeyReturn Then + cboAddress_Click + End If +End Sub +Private Sub Form_Resize() + If frmWebB.WindowState = 1 Then GoTo new1 + cboAddress.Width = Me.ScaleWidth - 200 + brwWebBrowser.Left = 200 + brwWebBrowser.Width = Me.ScaleWidth - 400 + brwWebBrowser.Height = Me.ScaleHeight - (picAddress.Top + picAddress.Height) - 200 +new1: +End Sub +Private Sub mnuBack_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoBack +Else +brwWebBrowser.GoBack +End If +End Sub +Private Sub mnuExit_Click() +FraUnlock.Visible = True +lblTitle1.Visible = True +lblTitle2.Visible = True +lblDisp.Visible = True +brwWebBrowser.Visible = False +cboAddress.Visible = False +End Sub +Private Sub mnuForward_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoForward +Else +brwWebBrowser.GoForward +End If +End Sub +Private Sub mnuHome_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoHome +Else +brwWebBrowser.GoHome +End If +End Sub + +Private Sub mnuPrintPage_Click() +MsgBox " To print right click on the webpage its self, go down the list and click print." +End Sub +Private Sub mnuRefresh_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Refresh +Else +brwWebBrowser.Refresh +End If +End Sub +Private Sub mnuSearch_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoSearch +Else +brwWebBrowser.GoSearch +End If +End Sub +Private Sub mnuStop_Click() +timTimer.Enabled = False +brwWebBrowser.Stop +End Sub +Private Sub picAddress_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +If Not Y <= 10 Then +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuPrintPage.Visible = False +mnuSearch.Visible = False +Else +mnuBack.Visible = True +mnuPrintPage.Visible = True +mnuForward.Visible = True +mnuRefresh.Visible = True +mnuStop.Visible = True +mnuHome.Visible = True +mnuGeneral.Visible = True +mnuExit.Visible = True +mnuSearch.Visible = True +End If +End Sub +Private Sub timTimer_Timer() + If brwWebBrowser.Busy = False Then + timTimer.Enabled = False + Else + End If +End Sub +Private Sub cmdOK_Click() +Dim X As Double +If SmtSysManTech2.Text = SmtSysMan Then + CmdOK.Enabled = False + SmtSysManTech2.Enabled = False + FraUnlock.Enabled = False + lblDisp.Enabled = True + lblDisp.Caption = "Unlocked!" + frmWebB.Refresh + Dim time As Double +beginn: + time = time + 1 + If Val(time) = 20000 Then GoTo endd + GoTo beginn +endd: +commons False +FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.bak", "C:\Program Files\Internet Explorer\IEXPLORE.exe" +Kill "C:\Program Files\Internet Explorer\IEXPLORE.bak" +End +Else +FraUnlock.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End If +End Sub +Private Sub fraAbout_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbWhite +End Sub +Private Sub lblClose_Click() +fraAbout.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End Sub +Private Sub lblClose_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbRed +End Sub +Private Sub mnuAbout_Click() +fraAbout.Visible = True +cboAddress.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = False +cboAddress.Visible = False +lblClose.ForeColor = vbWhite +End Sub +Private Sub mnuSetup_Click() +FraUnlock.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +End Sub +Private Sub SmtSysManTech2_KeyPress(KeyAscii As Integer) +If KeyAscii = 13 Then +cmdOK_Click +End If +End Sub + diff --git a/history/vb6-2001/frmBrowser.frx b/history/vb6-2001/frmBrowser.frx new file mode 100644 index 0000000..c435d75 Binary files /dev/null and b/history/vb6-2001/frmBrowser.frx differ diff --git a/history/vb6-2001/frmBrowserf.frm b/history/vb6-2001/frmBrowserf.frm new file mode 100644 index 0000000..58b494a --- /dev/null +++ b/history/vb6-2001/frmBrowserf.frm @@ -0,0 +1,565 @@ +VERSION 5.00 +Object = "{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0"; "shdocvw.dll" +Begin VB.Form frmWebB + BackColor = &H00000000& + ClientHeight = 8595 + ClientLeft = 255 + ClientTop = 150 + ClientWidth = 11415 + ControlBox = 0 'False + LinkTopic = "Form1" + ScaleHeight = 8595 + ScaleWidth = 11415 + WindowState = 2 'Maximized + Begin VB.Frame fraAbout + BackColor = &H00000000& + Caption = "About System Security 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 3495 + Left = 4080 + TabIndex = 6 + Top = 2520 + Visible = 0 'False + Width = 3495 + Begin VB.Label Label6 + BackColor = &H00000000& + Caption = "Web Browser Version 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 255 + Left = 360 + TabIndex = 16 + Top = 480 + Width = 2775 + End + Begin VB.Label Label1 + BackColor = &H00000000& + Caption = "Author: Jeremy Anderson" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 840 + TabIndex = 12 + Top = 2760 + Width = 1935 + End + Begin VB.Label Label2 + BackColor = &H00000000& + Caption = "For Windows 95 - 98 - 98 SE && ME" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 480 + TabIndex = 11 + Top = 960 + Width = 2535 + End + Begin VB.Label Label3 + BackColor = &H00000000& + Caption = $"frmBrowserf.frx":0000 + ForeColor = &H00FFFFFF& + Height = 855 + Left = 240 + TabIndex = 10 + Top = 1320 + Width = 3015 + End + Begin VB.Label Label4 + BackColor = &H00000000& + Caption = "NOTE: Will NOT work on windows 2000, NT, or XP." + ForeColor = &H00FFFFFF& + Height = 375 + Left = 240 + TabIndex = 9 + Top = 2280 + Width = 3015 + End + Begin VB.Label lblClose + BackColor = &H00000000& + Caption = "Close" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 375 + Left = 2640 + TabIndex = 8 + Top = 3000 + Width = 735 + End + Begin VB.Label Label5 + BackColor = &H00000000& + Caption = "Revision: 1" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 240 + TabIndex = 7 + Top = 3120 + Width = 855 + End + End + Begin VB.Frame FraUnlock + BackColor = &H00000000& + Caption = "Unlock Code:" + BeginProperty Font + Name = "Comic Sans MS" + Size = 9.75 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 3960 + TabIndex = 3 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.TextBox SmtSysManTech2 + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + IMEMode = 3 'DISABLE + Left = 120 + PasswordChar = "*" + TabIndex = 5 + Top = 240 + Width = 2055 + End + Begin VB.CommandButton CmdOK + BackColor = &H00000000& + Caption = "OK" + BeginProperty Font + Name = "Comic Sans MS" + Size = 8.25 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + Left = 2280 + MaskColor = &H00000000& + TabIndex = 4 + Top = 240 + Width = 1335 + End + End + Begin VB.Timer timTimer + Enabled = 0 'False + Interval = 5 + Left = 5760 + Top = 1440 + End + Begin SHDocVwCtl.WebBrowser brwWebBrowser + Height = 4350 + Left = 120 + TabIndex = 0 + Top = 600 + Width = 5400 + ExtentX = 9525 + ExtentY = 7673 + ViewMode = 1 + Offline = 0 + Silent = 0 + RegisterAsBrowser= 0 + RegisterAsDropTarget= 0 + AutoArrange = -1 'True + NoClientEdge = -1 'True + AlignLeft = 0 'False + NoWebView = 0 'False + HideFileNames = 0 'False + SingleClick = 0 'False + SingleSelection = 0 'False + NoFolders = 0 'False + Transparent = 0 'False + ViewID = "{0057D0E0-3573-11CF-AE69-08002B2E1262}" + Location = "http:///" + End + Begin VB.PictureBox picAddress + Align = 1 'Align Top + BackColor = &H00000000& + BorderStyle = 0 'None + Height = 435 + Left = 0 + ScaleHeight = 435 + ScaleWidth = 11415 + TabIndex = 1 + TabStop = 0 'False + Top = 0 + Width = 11415 + Begin VB.ComboBox cboAddress + Height = 315 + Left = 120 + TabIndex = 2 + Top = 120 + Width = 4515 + End + End + Begin VB.Label lblTitle1 + BackColor = &H00000000& + Caption = "System Security 1.5" + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 3960 + TabIndex = 14 + Top = 600 + Visible = 0 'False + Width = 3975 + End + Begin VB.Label lblDisp + Alignment = 2 'Center + BackColor = &H00000000& + Caption = "Please enter your Unlock code." + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 0 + TabIndex = 15 + Top = 6840 + Visible = 0 'False + Width = 11895 + End + Begin VB.Label lblTitle2 + BackColor = &H00000000& + Caption = "Lock Out Screen " + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 4200 + TabIndex = 13 + Top = 1320 + Visible = 0 'False + Width = 3975 + End + Begin VB.Menu mnuBack + Caption = "&Back" + End + Begin VB.Menu mnuForward + Caption = "&Forward" + End + Begin VB.Menu mnuRefresh + Caption = "&Refresh" + End + Begin VB.Menu mnuStop + Caption = "S&top" + End + Begin VB.Menu mnuHome + Caption = "&Home" + End + Begin VB.Menu mnuSearch + Caption = "&Search" + End + Begin VB.Menu mnuPrintPage + Caption = "&Print WebPage" + End + Begin VB.Menu mnuGeneral + Caption = "&General" + Begin VB.Menu mnuAbout + Caption = "&About" + End + Begin VB.Menu mnuExit + Caption = "&Exit" + End + End +End +Attribute VB_Name = "frmWebB" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = True +Attribute VB_Exposed = False +Option Explicit +Public StartingAddress As String +Dim mbDontNavigateNow As Boolean +Private Sub Form_Load() +commons True +On Error GoTo myer +'FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.EXE", "C:\Program Files\Internet Explorer\IEXPLORE.bak" +'Kill "C:\Program Files\Internet Explorer\IEXPLORE.EXE" +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuPrintPage.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuSearch.Visible = False +On Error Resume Next + Me.Show + Form_Resize + cboAddress.Move 50 + cboAddress.Text = "www.msn.com" + cboAddress.AddItem cboAddress.Text + timTimer.Enabled = True + brwWebBrowser.Navigate "www.msn.com" +Exit Sub +myer: +MsgBox "Error is " & Err.Description +End Sub +Private Sub brwWebBrowser_DownloadComplete() + On Error Resume Next + End Sub +Private Sub brwWebBrowser_NavigateComplete(ByVal URL As String) + Dim i As Integer + Dim bFound As Boolean + For i = 0 To cboAddress.ListCount - 1 + If cboAddress.List(i) = brwWebBrowser.LocationURL Then + bFound = True + Exit For + End If + Next i + mbDontNavigateNow = True + If bFound Then + cboAddress.RemoveItem i + End If + cboAddress.AddItem brwWebBrowser.LocationURL, 0 + cboAddress.ListIndex = 0 + mbDontNavigateNow = False +End Sub +Private Sub cboAddress_Click() + If mbDontNavigateNow Then Exit Sub + timTimer.Enabled = True + brwWebBrowser.Navigate cboAddress.Text +End Sub +Private Sub cboAddress_KeyPress(KeyAscii As Integer) + On Error Resume Next + If KeyAscii = vbKeyReturn Then + cboAddress_Click + End If +End Sub +Private Sub Form_Resize() + If frmWebB.WindowState = 1 Then GoTo new1 + cboAddress.Width = Me.ScaleWidth - 200 + brwWebBrowser.Left = 200 + brwWebBrowser.Width = Me.ScaleWidth - 400 + brwWebBrowser.Height = Me.ScaleHeight - (picAddress.Top + picAddress.Height) - 200 +new1: +End Sub +Private Sub mnuBack_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoBack +Else +brwWebBrowser.GoBack +End If +End Sub +Private Sub mnuExit_Click() +FraUnlock.Visible = True +lblTitle1.Visible = True +lblTitle2.Visible = True +lblDisp.Visible = True +brwWebBrowser.Visible = False +cboAddress.Visible = False +End Sub +Private Sub mnuForward_Click() +On Error Resume Next +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoForward +Else +brwWebBrowser.GoForward +End If +End Sub +Private Sub mnuHome_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoHome +Else +brwWebBrowser.GoHome +End If +End Sub + +Private Sub mnuPrintPage_Click() +MsgBox " To print right click on the webpage its self, go down the list and click print." +End Sub +Private Sub mnuRefresh_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Refresh +Else +brwWebBrowser.Refresh +End If +End Sub +Private Sub mnuSearch_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +cboAddress.Visible = True +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.GoSearch +Else +brwWebBrowser.GoSearch +End If +End Sub +Private Sub mnuStop_Click() +timTimer.Enabled = False +brwWebBrowser.Stop +End Sub +Private Sub picAddress_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +If Not Y <= 10 Then +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuPrintPage.Visible = False +mnuSearch.Visible = False +Else +mnuBack.Visible = True +mnuPrintPage.Visible = True +mnuForward.Visible = True +mnuRefresh.Visible = True +mnuStop.Visible = True +mnuHome.Visible = True +mnuGeneral.Visible = True +mnuExit.Visible = True +mnuSearch.Visible = True +End If +End Sub +Private Sub timTimer_Timer() + If brwWebBrowser.Busy = False Then + timTimer.Enabled = False + Else + End If +End Sub +Private Sub cmdOK_Click() +If SmtSysManTech2.Text = "REDACTED" Then + CmdOK.Enabled = False + SmtSysManTech2.Enabled = False + FraUnlock.Enabled = False + lblDisp.Enabled = True + lblDisp.Caption = "Unlocked!" + frmWebB.Refresh + Dim time As Double +beginn: + time = time + 1 + If Val(time) = 20000 Then GoTo endd + GoTo beginn +endd: +'FileCopy "C:\Program Files\Internet Explorer\IEXPLORE.bak", "C:\Program Files\Internet Explorer\IEXPLORE.exe" +'Kill "C:\Program Files\Internet Explorer\IEXPLORE.bak" +commons False +End +Else +FraUnlock.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End If +End Sub +Private Sub fraAbout_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbWhite +End Sub +Private Sub lblClose_Click() +fraAbout.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = True +cboAddress.Visible = True +End Sub +Private Sub lblClose_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbRed +End Sub +Private Sub mnuAbout_Click() +fraAbout.Visible = True +cboAddress.Visible = True +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +brwWebBrowser.Visible = False +cboAddress.Visible = False +lblClose.ForeColor = vbWhite +End Sub +Private Sub SmtSysManTech2_KeyPress(KeyAscii As Integer) +If KeyAscii = 13 Then +cmdOK_Click +End If +End Sub + diff --git a/history/vb6-2001/frmBrowserf.frx b/history/vb6-2001/frmBrowserf.frx new file mode 100644 index 0000000..c435d75 Binary files /dev/null and b/history/vb6-2001/frmBrowserf.frx differ diff --git a/history/vb6-2001/modMain.bas b/history/vb6-2001/modMain.bas new file mode 100644 index 0000000..7c921c1 --- /dev/null +++ b/history/vb6-2001/modMain.bas @@ -0,0 +1,24 @@ +Attribute VB_Name = "modMain" +Public X As Long +Option Explicit +Public SmtSysMan As String +Private Declare Function SystemParametersInfo Lib _ +"user32" Alias "SystemParametersInfoA" (ByVal uAction _ +As Long, ByVal uParam As Long, ByVal lpvParam As Any, _ +ByVal fuWinIni As Long) As Long +Const FLAGS = 3 +Const HWND_TOPMOST = -1 +Const HWND_NOTOPMOST = -2 +Public SetTop As Boolean +Private Declare Function SetWindowPos Lib "user32" (ByVal h%, ByVal hb%, ByVal X%, ByVal Y%, ByVal cx%, ByVal cy%, ByVal f%) As Integer +Sub commons(Disablem As Boolean) + X = SystemParametersInfo(97, Disablem, CStr(1), 0) +End Sub +Sub AlwaysOnTop(FormName As Form, bOnTop As Boolean) +Dim wind As Integer +If bOnTop = False Then + wind% = SetWindowPos(FormName.hWnd, HWND_TOPMOST, 0, 0, 0, 0, FLAGS) +Else + wind% = SetWindowPos(FormName.hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, FLAGS) +End If +End Sub diff --git a/history/vb6-2001/modMainf.bas b/history/vb6-2001/modMainf.bas new file mode 100644 index 0000000..dc5e6cb --- /dev/null +++ b/history/vb6-2001/modMainf.bas @@ -0,0 +1,23 @@ +Attribute VB_Name = "modMain" +Public X As Long +Option Explicit +Private Declare Function SystemParametersInfo Lib _ +"user32" Alias "SystemParametersInfoA" (ByVal uAction _ +As Long, ByVal uParam As Long, ByVal lpvParam As Any, _ +ByVal fuWinIni As Long) As Long +Const FLAGS = 3 +Const HWND_TOPMOST = -1 +Const HWND_NOTOPMOST = -2 +Public SetTop As Boolean +Private Declare Function SetWindowPos Lib "user32" (ByVal h%, ByVal hb%, ByVal X%, ByVal Y%, ByVal cx%, ByVal cy%, ByVal f%) As Integer +Sub commons(Disablem As Boolean) + X = SystemParametersInfo(97, Disablem, CStr(1), 0) +End Sub +Sub AlwaysOnTop(FormName As Form, bOnTop As Boolean) +Dim wind As Integer +If bOnTop = False Then + wind% = SetWindowPos(FormName.Wnd, HWND_TOPMOST, 0, 0, 0, 0, FLAGS) +Else + wind% = SetWindowPos(FormName.hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, FLAGS) +End If +End Sub diff --git a/history/vb6-2001/original/LobbyBrowser.vbp b/history/vb6-2001/original/LobbyBrowser.vbp new file mode 100644 index 0000000..c80244d --- /dev/null +++ b/history/vb6-2001/original/LobbyBrowser.vbp @@ -0,0 +1,37 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#..\..\..\..\..\WIN98\System32\stdole2.tlb#OLE Automation +Object={6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.3#0; COMCTL32.OCX +Object={EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0; shdocvw.dll +Form=frmBrowser.frm +Module=modMain; modMain.bas +Startup="frmWebB" +HelpFile="" +ExeName32="LobbyBrowser.exe" +Command32="" +Name="LobbyBrowser" +HelpContextID="0" +CompatibleMode="0" +MajorVer=1 +MinorVer=0 +RevisionVer=0 +AutoIncrementVer=0 +ServerSupportFiles=0 +VersionCompanyName="" +CompilationType=-1 +OptimizationType=0 +FavorPentiumPro(tm)=0 +CodeViewDebugInfo=0 +NoAliasing=0 +BoundsCheck=0 +OverflowCheck=0 +FlPointCheck=0 +FDIVCheck=0 +UnroundedFP=0 +StartMode=0 +Unattended=0 +Retained=0 +ThreadPerObject=0 +MaxNumberOfThreads=1 + +[MS Transaction Server] +AutoRefresh=1 diff --git a/history/vb6-2001/original/LobbyBrowser.vbw b/history/vb6-2001/original/LobbyBrowser.vbw new file mode 100644 index 0000000..92ba9b0 --- /dev/null +++ b/history/vb6-2001/original/LobbyBrowser.vbw @@ -0,0 +1,2 @@ +frmWebB = 19, 113, 430, 453, Z, 22, 29, 477, 413, C +modMain = 66, 66, 424, 418, diff --git a/history/vb6-2001/original/MSSCCPRJ.SCC b/history/vb6-2001/original/MSSCCPRJ.SCC new file mode 100644 index 0000000..83310f7 --- /dev/null +++ b/history/vb6-2001/original/MSSCCPRJ.SCC @@ -0,0 +1,6 @@ +[SCC] +SCC=This is a source code control file + +[LobbyBrowser.vbp] +SCC_Project_Name=this project is not under source code control +SCC_Aux_Path= diff --git a/history/vb6-2001/original/frmBrowser.frm b/history/vb6-2001/original/frmBrowser.frm new file mode 100644 index 0000000..30c74c4 --- /dev/null +++ b/history/vb6-2001/original/frmBrowser.frm @@ -0,0 +1,756 @@ +VERSION 5.00 +Object = "{6B7E6392-850A-101B-AFC0-4210102A8DA7}#1.3#0"; "COMCTL32.OCX" +Object = "{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}#1.1#0"; "shdocvw.dll" +Begin VB.Form frmWebB + BackColor = &H00000000& + ClientHeight = 8595 + ClientLeft = 255 + ClientTop = 150 + ClientWidth = 11415 + ControlBox = 0 'False + LinkTopic = "Form1" + ScaleHeight = 8595 + ScaleWidth = 11415 + WindowState = 2 'Maximized + Begin VB.Frame FraUnlock + BackColor = &H00000000& + Caption = "Unlock Code:" + BeginProperty Font + Name = "Comic Sans MS" + Size = 9.75 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 3960 + TabIndex = 12 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.TextBox SmtSysManTech2 + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + IMEMode = 3 'DISABLE + Left = 120 + PasswordChar = "*" + TabIndex = 14 + Top = 240 + Width = 2055 + End + Begin VB.CommandButton CmdOK + BackColor = &H00000000& + Caption = "OK" + BeginProperty Font + Name = "Comic Sans MS" + Size = 8.25 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + Height = 375 + Left = 2280 + MaskColor = &H00000000& + TabIndex = 13 + Top = 240 + Width = 1335 + End + End + Begin VB.Frame fraSetup + BackColor = &H00000000& + Caption = "Setup" + BeginProperty Font + Name = "Comic Sans MS" + Size = 9.75 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 2535 + Left = 3960 + TabIndex = 3 + Top = 2520 + Visible = 0 'False + Width = 3735 + Begin VB.TextBox txtPassword2 + Enabled = 0 'False + Height = 285 + IMEMode = 3 'DISABLE + Left = 1575 + PasswordChar = "*" + TabIndex = 8 + Top = 1320 + Width = 2055 + End + Begin VB.TextBox txtOrig + Height = 270 + IMEMode = 3 'DISABLE + Left = 1575 + PasswordChar = "*" + TabIndex = 7 + Top = 480 + Width = 2085 + End + Begin VB.CommandButton cmdsetupOK + Caption = "OK" + Default = -1 'True + Enabled = 0 'False + Height = 390 + Left = 480 + TabIndex = 6 + Top = 1920 + Width = 1140 + End + Begin VB.CommandButton cmdCancel + Cancel = -1 'True + Caption = "Cancel" + Height = 390 + Left = 2160 + TabIndex = 5 + Top = 1920 + Width = 1140 + End + Begin VB.TextBox txtPassword1 + Enabled = 0 'False + Height = 330 + IMEMode = 3 'DISABLE + Left = 1560 + PasswordChar = "*" + TabIndex = 4 + Top = 840 + Width = 2085 + End + Begin VB.Label lblCon + BackColor = &H00000000& + Caption = "&Confirm Password" + Enabled = 0 'False + ForeColor = &H00FFFFFF& + Height = 255 + Left = 135 + TabIndex = 11 + Top = 1320 + Width = 1335 + End + Begin VB.Label lblOrig + BackColor = &H00000000& + Caption = "&Original Password" + ForeColor = &H00FFFFFF& + Height = 270 + Index = 0 + Left = 120 + TabIndex = 10 + Top = 510 + Width = 1320 + End + Begin VB.Label lblPass + BackColor = &H00000000& + Caption = "&New Password:" + Enabled = 0 'False + ForeColor = &H00FFFFFF& + Height = 270 + Index = 1 + Left = 120 + TabIndex = 9 + Top = 900 + Width = 1320 + End + End + Begin VB.PictureBox picAddress + Align = 1 'Align Top + BackColor = &H00000000& + BorderStyle = 0 'None + Height = 435 + Left = 0 + ScaleHeight = 435 + ScaleWidth = 11415 + TabIndex = 1 + TabStop = 0 'False + Top = 0 + Width = 11415 + Begin VB.ComboBox cboAddress + Height = 315 + Left = 120 + TabIndex = 2 + Top = 120 + Width = 4515 + End + End + Begin VB.Timer timTimer + Enabled = 0 'False + Interval = 5 + Left = 5760 + Top = 1440 + End + Begin SHDocVwCtl.WebBrowser brwWebBrowser + Height = 4350 + Left = 120 + TabIndex = 0 + Top = 600 + Width = 5400 + ExtentX = 9525 + ExtentY = 7673 + ViewMode = 1 + Offline = 0 + Silent = 0 + RegisterAsBrowser= 0 + RegisterAsDropTarget= 0 + AutoArrange = -1 'True + NoClientEdge = -1 'True + AlignLeft = 0 'False + NoWebView = 0 'False + HideFileNames = 0 'False + SingleClick = 0 'False + SingleSelection = 0 'False + NoFolders = 0 'False + Transparent = 0 'False + ViewID = "{0057D0E0-3573-11CF-AE69-08002B2E1262}" + Location = "http:///" + End + Begin VB.Frame fraAbout + BackColor = &H00000000& + Caption = "About System Security 1.5" + BeginProperty Font + Name = "MS Sans Serif" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 2655 + Left = 4080 + TabIndex = 15 + Top = 2520 + Visible = 0 'False + Width = 3495 + Begin VB.Label Label1 + BackColor = &H00000000& + Caption = "Author: Jeremy Anderson" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 840 + TabIndex = 21 + Top = 2040 + Width = 1935 + End + Begin VB.Label Label2 + BackColor = &H00000000& + Caption = "For Windows 95 - 98 - 98 SE && ME" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 480 + TabIndex = 20 + Top = 480 + Width = 2535 + End + Begin VB.Label Label3 + BackColor = &H00000000& + Caption = "Description: This program is basic security for any home computer system running the supported windows platforms above. " + ForeColor = &H00FFFFFF& + Height = 615 + Left = 240 + TabIndex = 19 + Top = 840 + Width = 3015 + End + Begin VB.Label Label4 + BackColor = &H00000000& + Caption = "NOTE: Will NOT work on windows 2000, NT, or XP." + ForeColor = &H00FFFFFF& + Height = 375 + Left = 240 + TabIndex = 18 + Top = 1560 + Width = 3015 + End + Begin VB.Label lblClose + BackColor = &H00000000& + Caption = "Close" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 2880 + TabIndex = 17 + Top = 2280 + Width = 495 + End + Begin VB.Label Label5 + BackColor = &H00000000& + Caption = "Revision: 2" + ForeColor = &H00FFFFFF& + Height = 255 + Left = 240 + TabIndex = 16 + Top = 2280 + Width = 855 + End + End + Begin VB.Label lblDisp + Alignment = 2 'Center + BackColor = &H00000000& + Caption = "Please enter your Unlock code." + BeginProperty Font + Name = "Comic Sans MS" + Size = 12 + Charset = 0 + Weight = 700 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 735 + Left = 0 + TabIndex = 24 + Top = 6840 + Visible = 0 'False + Width = 11895 + End + Begin VB.Label lblTitle1 + BackColor = &H00000000& + Caption = "System Security 1.5" + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 3960 + TabIndex = 23 + Top = 0 + Visible = 0 'False + Width = 3975 + End + Begin VB.Label lblTitle2 + BackColor = &H00000000& + Caption = "Lock Out Screen " + BeginProperty Font + Name = "Times New Roman" + Size = 24 + Charset = 0 + Weight = 400 + Underline = 0 'False + Italic = 0 'False + Strikethrough = 0 'False + EndProperty + ForeColor = &H00FFFFFF& + Height = 615 + Left = 4200 + TabIndex = 22 + Top = 600 + Visible = 0 'False + Width = 3975 + End + Begin ComctlLib.ImageList imlIcons + Left = 5640 + Top = 2040 + _ExtentX = 1005 + _ExtentY = 1005 + BackColor = -2147483643 + ImageWidth = 24 + ImageHeight = 24 + MaskColor = 12632256 + _Version = 327682 + BeginProperty Images {0713E8C2-850A-101B-AFC0-4210102A8DA7} + NumListImages = 6 + BeginProperty ListImage1 {0713E8C3-850A-101B-AFC0-4210102A8DA7} + Picture = "frmBrowser.frx":0000 + Key = "" + EndProperty + BeginProperty ListImage2 {0713E8C3-850A-101B-AFC0-4210102A8DA7} + Picture = "frmBrowser.frx":0692 + Key = "" + EndProperty + BeginProperty ListImage3 {0713E8C3-850A-101B-AFC0-4210102A8DA7} + Picture = "frmBrowser.frx":0D24 + Key = "" + EndProperty + BeginProperty ListImage4 {0713E8C3-850A-101B-AFC0-4210102A8DA7} + Picture = "frmBrowser.frx":13B6 + Key = "" + EndProperty + BeginProperty ListImage5 {0713E8C3-850A-101B-AFC0-4210102A8DA7} + Picture = "frmBrowser.frx":1A48 + Key = "" + EndProperty + BeginProperty ListImage6 {0713E8C3-850A-101B-AFC0-4210102A8DA7} + Picture = "frmBrowser.frx":20DA + Key = "" + EndProperty + EndProperty + End + Begin VB.Menu mnuBack + Caption = "&Back" + End + Begin VB.Menu mnuForward + Caption = "&Forward" + End + Begin VB.Menu mnuRefresh + Caption = "&Refresh" + End + Begin VB.Menu mnuStop + Caption = "&Stop" + End + Begin VB.Menu mnuHome + Caption = "&Home" + End + Begin VB.Menu mnuGeneral + Caption = "&General" + Begin VB.Menu mnuSetup + Caption = "&Setup" + End + Begin VB.Menu mnuAbout + Caption = "&About" + End + End + Begin VB.Menu mnuSearch + Caption = "&Search" + End + Begin VB.Menu mnuExit + Caption = "&Exit" + End +End +Attribute VB_Name = "frmWebB" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = True +Attribute VB_Exposed = False +'Add code so the whole form doesnt look like it moves when the +'menu apears at the top of the screen +'us if statements,.top,.height,and move all objects up the +'amount of space that the menu took up +Option Explicit +Public StartingAddress, origpass, inFile As String +Dim mbDontNavigateNow As Boolean +Private Sub Form_Load() +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuSearch.Visible = False +On Error GoTo ErrorCorrection +Open "C:\windows\system\smt.txt" For Input As #2 +Input #2, SmtSysMan +Close #2 +GoTo EndError: +ErrorCorrection: +FraUnlock.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +cmdCancel.Enabled = False +fraSetup.Visible = True +txtOrig.Enabled = False +lblOrig(0).Enabled = False +txtPassword1.Enabled = True +lblPass(1).Enabled = True +txtPassword2.Enabled = True +lblCon.Enabled = True +EndError: + + + +'mnuGeneral.Visible = False +'CmdOK.Enabled = False +' SmtSysManTech2.Enabled = True +' SmtSysManTech2.Text = "" +' FraUnlock.Enabled = True +' lblDisp.Enabled = True +' lblDisp.Caption = "Please Enter Your System Security Unlock Code" +' CmdOK.Enabled = True +commons True + On Error Resume Next + Me.Show + Form_Resize + cboAddress.Move 50 + If Len(StartingAddress) > 0 Then + cboAddress.Text = StartingAddress + cboAddress.AddItem cboAddress.Text + 'try to navigate to the starting address + timTimer.Enabled = True + brwWebBrowser.Navigate StartingAddress + End If +End Sub +Private Sub brwWebBrowser_DownloadComplete() + On Error Resume Next + End Sub +Private Sub brwWebBrowser_NavigateComplete(ByVal URL As String) + Dim i As Integer + Dim bFound As Boolean + For i = 0 To cboAddress.ListCount - 1 + If cboAddress.List(i) = brwWebBrowser.LocationURL Then + bFound = True + Exit For + End If + Next i + mbDontNavigateNow = True + If bFound Then + cboAddress.RemoveItem i + End If + cboAddress.AddItem brwWebBrowser.LocationURL, 0 + cboAddress.ListIndex = 0 + mbDontNavigateNow = False +End Sub +Private Sub cboAddress_Click() + If mbDontNavigateNow Then Exit Sub + timTimer.Enabled = True + brwWebBrowser.Navigate cboAddress.Text +End Sub +Private Sub cboAddress_KeyPress(KeyAscii As Integer) + On Error Resume Next + If KeyAscii = vbKeyReturn Then + cboAddress_Click + End If +End Sub +Private Sub Form_Resize() + If frmWebB.WindowState = 1 Then GoTo new1 + cboAddress.Width = Me.ScaleWidth - 200 + brwWebBrowser.Left = 200 + brwWebBrowser.Width = Me.ScaleWidth - 400 + brwWebBrowser.Height = Me.ScaleHeight - (picAddress.Top + picAddress.Height) - 200 +new1: +End Sub +Private Sub mnuBack_Click() + On Error Resume Next + brwWebBrowser.GoBack +End Sub +Private Sub mnuExit_Click() +End +End Sub +Private Sub mnuForward_Click() + On Error Resume Next + brwWebBrowser.GoForward +End Sub +Private Sub mnuHome_Click() + brwWebBrowser.GoHome +End Sub +Private Sub mnuRefresh_Click() +If brwWebBrowser.Visible = False Then +brwWebBrowser.Visible = True +Else +brwWebBrowser.Refresh +End If +End Sub +Private Sub mnuSearch_Click() +brwWebBrowser.GoSearch +End Sub +Private Sub mnuStop_Click() +timTimer.Enabled = False +brwWebBrowser.Stop +End Sub +Private Sub picAddress_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +If Not Y <= 10 Then +mnuBack.Visible = False +mnuForward.Visible = False +mnuRefresh.Visible = False +mnuStop.Visible = False +mnuHome.Visible = False +mnuGeneral.Visible = False +mnuExit.Visible = False +mnuSearch.Visible = False +Else +mnuBack.Visible = True +mnuForward.Visible = True +mnuRefresh.Visible = True +mnuStop.Visible = True +mnuHome.Visible = True +mnuGeneral.Visible = True +mnuExit.Visible = True +mnuSearch.Visible = True +End If +End Sub +Private Sub timTimer_Timer() + If brwWebBrowser.Busy = False Then + timTimer.Enabled = False + Else + End If +End Sub + + + + + + + + + + + + + + + + + + + + + +Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +If Y <= 3 Then +mnuGeneral.Visible = True +Else +mnuGeneral.Visible = False +End If +End Sub +Private Sub cmdOK_Click() +Dim X As Double +If SmtSysManTech2.Text = SmtSysMan Then + CmdOK.Enabled = False + SmtSysManTech2.Enabled = False + FraUnlock.Enabled = False + lblDisp.Enabled = True + lblDisp.Caption = "Unlocked!" + SMTech.Refresh + Dim time As Double +beginn: + time = time + 1 + If Val(time) = 20000 Then GoTo endd + GoTo beginn +endd: +commons False +End +Else +Do Until X = 75000 +DoEvents +lblDisp.Caption = "Invalid Unlock Code, Please GO AWAY!." +X = X + 1 +Loop +SmtSysManTech2.Text = "" +SmtSysManTech2.SetFocus +lblDisp.Caption = "Please Enter Your System Security Unlock Code" +End If +End Sub +Private Sub fraAbout_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbWhite +End Sub +Private Sub lblClose_Click() +fraAbout.Visible = False +fraSetup.Visible = False +lblTitle1.Visible = True +lblTitle2.Visible = True +lblDisp.Visible = True +FraUnlock.Visible = True +End Sub +Private Sub lblClose_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) +lblClose.ForeColor = vbRed +End Sub +Private Sub mnuAbout_Click() +fraAbout.Visible = True +fraSetup.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +FraUnlock.Visible = False +lblClose.ForeColor = vbWhite +End Sub +Private Sub mnuSetup_Click() +FraUnlock.Visible = False +lblTitle1.Visible = False +lblTitle2.Visible = False +lblDisp.Visible = False +fraSetup.Visible = True +End Sub +Private Sub cmdCancel_Click() +fraSetup.Visible = False +FraUnlock.Visible = True +lblDisp.Visible = True +lblTitle1.Visible = True +lblTitle2.Visible = True + +txtPassword1.Text = "" +txtPassword2.Text = "" +txtOrig.Text = "" +lblPass(1).Enabled = False +txtPassword1.Enabled = False +lblCon.Enabled = False +txtPassword2.Enabled = False +txtOrig.Enabled = True +lblOrig(0).Enabled = True +End Sub +Private Sub cmdsetupOK_Click() +cmdCancel.Enabled = True +If txtPassword1.Text = txtPassword2.Text Then +' +If txtOrig.Text = SmtSysMan Then +Open "C:\windows\system\smt.txt" For Output As #1 +Print #1, txtPassword1.Text +Close #1 +End If +' + SmtSysMan = txtPassword1.Text +' + txtPassword1.Text = "" + txtPassword2.Text = "" + txtOrig.Text = "" +lblPass(1).Enabled = False +txtPassword1.Enabled = False +lblCon.Enabled = False +txtPassword2.Enabled = False +txtOrig.Enabled = True +lblOrig(0).Enabled = True +' +Else: + If txtPassword1.Text = "" Or txtPassword2.Text = "" Then + MsgBox ("Please Confirm Your New Password") + GoTo one: + End If + MsgBox "Please Confim you type everything correctly.": +one: End If: +FraUnlock.Visible = True +lblTitle1.Visible = True +lblTitle2.Visible = True +lblDisp.Visible = True +fraSetup.Visible = False +End Sub +Private Sub SmtSysManTech2_KeyPress(KeyAscii As Integer) +If KeyAscii = 13 Then +cmdOK_Click +End If +End Sub +Private Sub txtOrig_Change() +cmdCancel.Enabled = True +If txtOrig.Text = SmtSysMan Then +lblPass(1).Enabled = True +txtPassword1.Enabled = True +lblCon.Enabled = True +txtPassword2.Enabled = True +txtOrig.Enabled = False +lblOrig(0).Enabled = False +cmdsetupOK.Enabled = True +End If +End Sub +Private Sub txtPassword1_Change() +cmdsetupOK.Enabled = True +End Sub +Private Sub txtPassword2_KeyPress(KeyAscii As Integer) +If KeyAscii = 13 Then +cmdsetupOK_Click +End If +End Sub + diff --git a/history/vb6-2001/original/frmBrowser.frx b/history/vb6-2001/original/frmBrowser.frx new file mode 100644 index 0000000..8672147 Binary files /dev/null and b/history/vb6-2001/original/frmBrowser.frx differ diff --git a/history/vb6-2001/original/modMain.bas b/history/vb6-2001/original/modMain.bas new file mode 100644 index 0000000..7c921c1 --- /dev/null +++ b/history/vb6-2001/original/modMain.bas @@ -0,0 +1,24 @@ +Attribute VB_Name = "modMain" +Public X As Long +Option Explicit +Public SmtSysMan As String +Private Declare Function SystemParametersInfo Lib _ +"user32" Alias "SystemParametersInfoA" (ByVal uAction _ +As Long, ByVal uParam As Long, ByVal lpvParam As Any, _ +ByVal fuWinIni As Long) As Long +Const FLAGS = 3 +Const HWND_TOPMOST = -1 +Const HWND_NOTOPMOST = -2 +Public SetTop As Boolean +Private Declare Function SetWindowPos Lib "user32" (ByVal h%, ByVal hb%, ByVal X%, ByVal Y%, ByVal cx%, ByVal cy%, ByVal f%) As Integer +Sub commons(Disablem As Boolean) + X = SystemParametersInfo(97, Disablem, CStr(1), 0) +End Sub +Sub AlwaysOnTop(FormName As Form, bOnTop As Boolean) +Dim wind As Integer +If bOnTop = False Then + wind% = SetWindowPos(FormName.hWnd, HWND_TOPMOST, 0, 0, 0, 0, FLAGS) +Else + wind% = SetWindowPos(FormName.hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, FLAGS) +End If +End Sub diff --git a/packaging/build-flatpak.sh b/packaging/build-flatpak.sh new file mode 100755 index 0000000..fbbf528 --- /dev/null +++ b/packaging/build-flatpak.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# build-flatpak.sh — one-shot Linux packaging: build the Flatpak bundle. +# +# Output: packaging/Vestibule-.flatpak +# +# Prerequisites: +# - flatpak + flatpak-builder (distro package) +# - the Freedesktop 24.08 SDK + Rust extension (installed automatically +# from Flathub on first run, ~1 GB) +# +# Usage: +# ./packaging/build-flatpak.sh +# +# POSIX sh. + +set -eu + +VERSION="1.2.2" +APP_ID="net.dcos.Vestibule" +PACKAGING_DIR=$(cd "$(dirname "$0")" && pwd) +REPO_ROOT=$(cd "${PACKAGING_DIR}/.." && pwd) +REPO_DIR="${PACKAGING_DIR}/repo" +BUNDLE="${PACKAGING_DIR}/Vestibule-${VERSION}.flatpak" + +for tool in flatpak flatpak-builder; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "error: ${tool} not found — install it from your distribution" >&2 + exit 1 + fi +done + +echo "==> ensure flathub remote + Freedesktop 24.08 SDK (first run: ~1 GB)" +flatpak remote-add --if-not-exists --user flathub \ + https://flathub.org/repo/flathub.flatpakrepo +flatpak install --user --noninteractive -y flathub \ + org.freedesktop.Sdk//24.08 \ + org.freedesktop.Sdk.Extension.rust-stable//24.08 \ + 2>/dev/null || flatpak install --user --noninteractive flathub \ + org.freedesktop.Sdk//24.08 \ + org.freedesktop.Sdk.Extension.rust-stable//24.08 + +echo "==> flatpak-builder" +cd "${REPO_ROOT}" +rm -rf "${REPO_DIR}" +flatpak-builder --force-clean --repo="${PACKAGING_DIR}/repo" \ + "${PACKAGING_DIR}/builddir" \ + "${PACKAGING_DIR}/${APP_ID}.json" + +echo "==> bundle" +flatpak build-bundle "${PACKAGING_DIR}/repo" "${BUNDLE}" "${APP_ID}" "${VERSION}" + +echo "" +echo " [ok] ${BUNDLE} ($(du -h "${BUNDLE}" | cut -f1))" +echo "" +echo "Install locally: flatpak install --user ${BUNDLE}" +echo "Then run: flatpak run ${APP_ID}" diff --git a/packaging/build-installer.ps1 b/packaging/build-installer.ps1 new file mode 100644 index 0000000..6aefcb0 --- /dev/null +++ b/packaging/build-installer.ps1 @@ -0,0 +1,63 @@ +# build-installer.ps1 — one-shot Windows packaging: build usher, then +# compile the Inno Setup installer. +# +# Output: packaging\Output\Vestibule-Setup-.exe +# +# Prerequisites: +# - Rust (rustup) for usher +# - Inno Setup 6 https://jrsoftware.org/isinfo.php +# (or: choco install innosetup -y) +# +# Usage: +# powershell -ExecutionPolicy Bypass -File packaging\build-installer.ps1 + +$ErrorActionPreference = "Stop" + +$packagingDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $packagingDir + +# ─── 1. Build usher ─────────────────────────────────────────────────── + +Write-Host "==> cargo build --release (usher)" -ForegroundColor Cyan +& cargo build --release --manifest-path (Join-Path $repoRoot "helper\Cargo.toml") +if ($LASTEXITCODE -ne 0) { + Write-Error "cargo build failed" + exit 1 +} +$usher = Join-Path $repoRoot "helper\target\release\usher.exe" +Write-Host " [ok] $usher" + +# ─── 2. Locate ISCC.exe ─────────────────────────────────────────────── + +$iscc = Get-Command "ISCC.exe" -ErrorAction SilentlyContinue +if (-not $iscc) { + foreach ($candidate in @( + "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", + "${env:ProgramFiles}\Inno Setup 6\ISCC.exe")) { + if (Test-Path $candidate) { $iscc = $candidate; break } + } +} else { + $iscc = $iscc.Source +} +if (-not $iscc) { + Write-Error "ISCC.exe (Inno Setup 6) not found. Install from https://jrsoftware.org/isinfo.php or: choco install innosetup -y" + exit 1 +} + +# ─── 3. Compile installer ───────────────────────────────────────────── + +Write-Host "==> ISCC vestibule.iss" -ForegroundColor Cyan +& $iscc (Join-Path $packagingDir "vestibule.iss") +if ($LASTEXITCODE -ne 0) { + Write-Error "Inno Setup compile failed" + exit 1 +} + +$out = Join-Path $packagingDir "Output\Vestibule-Setup-1.2.2.exe" +Write-Host "" +Write-Host " [ok] $out ($([math]::Round((Get-Item $out).Length / 1MB, 2)) MB)" -ForegroundColor Green +Write-Host "" +Write-Host "Next: run it on the kiosk machine and tick 'Run the kiosk" +Write-Host "provisioning wizard', or deploy unattended:" +Write-Host " Vestibule-Setup-1.2.2.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART" +Write-Host " powershell -ExecutionPolicy Bypass -File ``"C:\Program Files\Vestibule\scripts\provision-kiosk.ps1``" -KioskUser Kiosk -HomeUrl https://example.org -Quiet -Restart" diff --git a/packaging/icons/vestibule-128.png b/packaging/icons/vestibule-128.png new file mode 100644 index 0000000..66d692d Binary files /dev/null and b/packaging/icons/vestibule-128.png differ diff --git a/packaging/icons/vestibule-16.png b/packaging/icons/vestibule-16.png new file mode 100644 index 0000000..f559ef1 Binary files /dev/null and b/packaging/icons/vestibule-16.png differ diff --git a/packaging/icons/vestibule-256.png b/packaging/icons/vestibule-256.png new file mode 100644 index 0000000..7047222 Binary files /dev/null and b/packaging/icons/vestibule-256.png differ diff --git a/packaging/icons/vestibule-32.png b/packaging/icons/vestibule-32.png new file mode 100644 index 0000000..4cba656 Binary files /dev/null and b/packaging/icons/vestibule-32.png differ diff --git a/packaging/icons/vestibule-48.png b/packaging/icons/vestibule-48.png new file mode 100644 index 0000000..ae8ff78 Binary files /dev/null and b/packaging/icons/vestibule-48.png differ diff --git a/packaging/icons/vestibule-64.png b/packaging/icons/vestibule-64.png new file mode 100644 index 0000000..cf609b9 Binary files /dev/null and b/packaging/icons/vestibule-64.png differ diff --git a/packaging/icons/vestibule.ico b/packaging/icons/vestibule.ico new file mode 100644 index 0000000..f38e8e5 Binary files /dev/null and b/packaging/icons/vestibule.ico differ diff --git a/packaging/icons/vestibule.svg b/packaging/icons/vestibule.svg new file mode 100644 index 0000000..f07852e --- /dev/null +++ b/packaging/icons/vestibule.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/packaging/net.dcos.Vestibule.desktop b/packaging/net.dcos.Vestibule.desktop new file mode 100644 index 0000000..08cb741 --- /dev/null +++ b/packaging/net.dcos.Vestibule.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=Vestibule +GenericName=Kiosk Provisioning Toolkit +Comment=Turn this machine into a locked-down public kiosk browser +Exec=vestibule +Icon=net.dcos.Vestibule +Terminal=true +Categories=System;Utility; +Keywords=kiosk;browser;librewolf;lockdown;public;terminal; +NoDisplay=false diff --git a/packaging/net.dcos.Vestibule.json b/packaging/net.dcos.Vestibule.json new file mode 100644 index 0000000..b30318a --- /dev/null +++ b/packaging/net.dcos.Vestibule.json @@ -0,0 +1,61 @@ +{ + "app-id": "net.dcos.Vestibule", + "runtime": "org.freedesktop.Platform", + "runtime-version": "24.08", + "sdk": "org.freedesktop.Sdk", + "sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"], + "command": "vestibule", + "finish-args": [ + "--talk-name=org.freedesktop.Flatpak" + ], + "modules": [ + { + "name": "usher", + "buildsystem": "simple", + "build-options": { + "append-path": "/usr/lib/sdk/rust-stable/bin", + "env": { + "CARGO_HOME": "/run/build/usher/cargo-home" + } + }, + "build-commands": [ + "cargo build --release", + "install -Dm755 target/release/usher -t /app/bin/" + ], + "sources": [ + { + "type": "dir", + "path": "../../helper", + "skip": ["target"] + } + ] + }, + { + "name": "vestibule-payload", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 packaging/vestibule-flatpak-cli /app/bin/vestibule", + "mkdir -p /app/share/vestibule", + "cp -r extension config scripts /app/share/vestibule/", + "mkdir -p /app/share/vestibule/docs", + "cp DEPLOYMENT.md README.md QUICKSTART.md LICENSE /app/share/vestibule/docs/", + "install -Dm644 packaging/net.dcos.Vestibule.desktop /app/share/applications/net.dcos.Vestibule.desktop", + "install -Dm644 packaging/net.dcos.Vestibule.metainfo.xml /app/share/metainfo/net.dcos.Vestibule.metainfo.xml", + "install -Dm644 packaging/icons/vestibule.svg /app/share/icons/hicolor/scalable/apps/net.dcos.Vestibule.svg", + "install -Dm644 packaging/icons/vestibule-16.png /app/share/icons/hicolor/16x16/apps/net.dcos.Vestibule.png", + "install -Dm644 packaging/icons/vestibule-32.png /app/share/icons/hicolor/32x32/apps/net.dcos.Vestibule.png", + "install -Dm644 packaging/icons/vestibule-48.png /app/share/icons/hicolor/48x48/apps/net.dcos.Vestibule.png", + "install -Dm644 packaging/icons/vestibule-64.png /app/share/icons/hicolor/64x64/apps/net.dcos.Vestibule.png", + "install -Dm644 packaging/icons/vestibule-128.png /app/share/icons/hicolor/128x128/apps/net.dcos.Vestibule.png", + "install -Dm644 packaging/icons/vestibule-256.png /app/share/icons/hicolor/256x256/apps/net.dcos.Vestibule.png" + ], + "sources": [ + { + "type": "dir", + "path": "../..", + "skip": [".git", "helper/target", "packaging/Output", "packaging/repo", "packaging/builddir"] + } + ] + } + ] +} diff --git a/packaging/net.dcos.Vestibule.metainfo.xml b/packaging/net.dcos.Vestibule.metainfo.xml new file mode 100644 index 0000000..d8ff88a --- /dev/null +++ b/packaging/net.dcos.Vestibule.metainfo.xml @@ -0,0 +1,76 @@ + + + net.dcos.Vestibule + Vestibule + Kiosk lockdown browser toolkit — provision a public terminal in one command + + Jeremy Anderson + + info@dcos.net + CC0-1.0 + MIT + https://dcos.net + https://dcos.net + +

+ Vestibule turns a Linux machine into a public-facing kiosk browser + built on LibreWolf ESR plus a small Rust native helper (usher). The + device is the kiosk, not an app pretending to be one: the cage + Wayland compositor runs LibreWolf as the only client on a VT, under + systemd supervision, with every session sanitized on idle timeout, + wake-from-sleep, and unlock. +

+

+ This package is the deployment kit. It ships the usher binary, the + Vestibule WebExtension, enterprise policies, and the provisioning + scripts. Running it prints the exact host-side command that turns + the machine (or any machine with this Flatpak installed) into a + kiosk — no manual OS configuration required. +

+
+ net.dcos.Vestibule.desktop + + + +

Safe-by-default navigation.

+
    +
  • New safelist URL policy mode — the default: every domain is blocked until the operator lists it
  • +
  • Domain-based matching replaces substring matching for the safelist; subdomains covered, smuggle attempts blocked
  • +
  • Home-origin guarantee: the kiosk home page is always navigable, whatever the list says
  • +
  • Blocked top-level navigations land on an in-extension block page instead of a raw connection error
  • +
  • Admin wizard validates the home URL against the safelist live and at save time, with one-click domain add
  • +
  • 62-assertion unit suite for the URL policy engine (scripts/test-url-policy.js), wired into CI
  • +
  • Relicensed MIT (was Apache 2.0)
  • +
+
+
+ + +

Production readiness pass.

+
    +
  • Constant-time admin password verification in the wizard
  • +
  • Table-driven smoke test; step-down browser/flavor resolution in the provisioning scripts
  • +
  • Launcher dispatch integration-tested across all browser/flavor permutations
  • +
  • Deprovision resets each policy directory to its pre-Vestibule baseline
  • +
+
+
+ + +

Phase 2 — deployability.

+
    +
  • cage systemd kiosk session provisioning (native + Flatpak LibreWolf)
  • +
  • Windows AssignedAccess / Shell Launcher provisioning wizard
  • +
  • Inno Setup installer + this Flatpak packaging
  • +
  • Full deprovision on both platforms
  • +
+
+
+ + +

Phase 1 — Argon2id unlock, dedicated unlock popup, Windows power events, per-origin cookie preservation, systemd supervision, CI matrix.

+
+
+
+ +
diff --git a/packaging/vestibule-flatpak-cli b/packaging/vestibule-flatpak-cli new file mode 100755 index 0000000..0f7fce1 --- /dev/null +++ b/packaging/vestibule-flatpak-cli @@ -0,0 +1,95 @@ +#!/bin/sh +# vestibule — CLI entry point inside the Flatpak sandbox. +# +# The Flatpak is a delivery vehicle + self-describing deployment kit: it +# carries usher, the WebExtension, enterprise policies, and the +# provisioning scripts. The actual kiosk provisioning runs on the HOST +# (it must configure systemd, /opt, /etc) — this CLI locates the payload +# from the host's perspective and prints the exact command to run. +# +# Subcommands: +# vestibule print status + quick start +# vestibule version print the Vestibule version +# vestibule paths print host-visible payload paths +# vestibule provision print the host-side provisioning command +# +# Host access uses flatpak-spawn (permission: org.freedesktop.Flatpak, +# granted by the manifest). Without it, candidate paths are printed. + +VERSION="1.2.2" +APP_ID="net.dcos.Vestibule" +SHARE_REL="files/share/vestibule" + +say() { printf '%s\n' "$1"; } + +host_sh() { + # Run a shell snippet on the host. Returns 1 if not permitted. + flatpak-spawn --host sh -c "$1" 2>/dev/null +} + +payload_path() { + # Resolve the payload directory as visible from the host. + if host_sh "true"; then + host_sh " + for p in \ + /var/lib/flatpak/app/${APP_ID}/current/active/${SHARE_REL} \ + \"\${HOME}/.local/share/flatpak/app/${APP_ID}/current/active/${SHARE_REL}\"; do + if [ -d \"\$p\" ]; then printf '%s' \"\$p\"; exit 0; fi + done + exit 1" + return + fi + return 1 +} + +cmd_status() { + say "Vestibule ${VERSION} — kiosk lockdown browser toolkit" + say "" + say "This Flatpak carries the deployment kit (usher, extension, policies," + say "provisioning scripts). To turn the machine into a kiosk, run:" + say "" + cmd_provision + say "" + say "Full runbook: DEPLOYMENT.md (also shipped inside this package)." +} + +cmd_version() { + say "${VERSION}" +} + +cmd_paths() { + p=$(payload_path) + if [ -n "${p}" ]; then + say "payload : ${p}" + say "usher : $(dirname "${p}")/bin/usher" + else + say "host access unavailable (flatpak-spawn not permitted). Candidates:" + say " /var/lib/flatpak/app/${APP_ID}/current/active/${SHARE_REL}" + say " ~/.local/share/flatpak/app/${APP_ID}/current/active/${SHARE_REL}" + fi +} + +cmd_provision() { + p=$(payload_path) + if [ -z "${p}" ]; then + say "# flatpak-spawn not permitted — run on the host:" + p="/var/lib/flatpak/app/${APP_ID}/current/active/${SHARE_REL}" + fi + say "sudo sh '${p}/scripts/provision-kiosk.sh' \\" + say " --usher-bin '$(dirname "${p}")/bin/usher' \\" + say " --home-url https://your-kiosk-start-page.example.org --yes" +} + +case "${1:-status}" in + status) cmd_status ;; + version) cmd_version ;; + paths) cmd_paths ;; + provision) cmd_provision ;; + help|-h|--help) + say "usage: vestibule [status|version|paths|provision]" + ;; + *) + say "unknown command: $1 (try: vestibule help)" >&2 + exit 2 + ;; +esac diff --git a/packaging/vestibule.iss b/packaging/vestibule.iss new file mode 100644 index 0000000..1244b30 --- /dev/null +++ b/packaging/vestibule.iss @@ -0,0 +1,118 @@ +; vestibule.iss — Inno Setup script for the Vestibule Windows installer. +; +; Builds Vestibule-Setup-.exe: stages usher.exe, the extension, +; configs, and provisioning scripts to C:\Program Files\Vestibule, adds +; Start Menu shortcuts for provisioning, and offers to launch the +; AssignedAccess provisioning wizard at the end. +; +; The installer STAGES files only — it never touches OS lockdown state. +; All OS-level configuration (kiosk account, AssignedAccess, autologon, +; policies) is done by scripts\provision-kiosk.ps1, which is fully +; parameterized for unattended deployment. See DEPLOYMENT.md. +; +; Build (local): +; cd helper && cargo build --release && cd .. +; packaging\build-installer.ps1 +; Build (CI): .github/workflows/ci.yml, "package-windows" job. +; +; Inno Setup 6.x required. + +#define MyAppName "Vestibule" +#define MyAppVersion "1.2.2" +#define MyAppPublisher "Jeremy Anderson" +#define MyAppURL "https://dcos.net" +#define MyAppExeVersion "1.2.2" + +; Compile-time guard: fail with a clear message instead of shipping an +; installer without the helper binary. +#if !FileExists("..\helper\target\release\usher.exe") + #error usher.exe not found under helper\target\release. Build it first: cd helper; cargo build --release +#endif + +[Setup] +AppId={{1DE48322-DE9E-43B3-B86B-41ABCD8EB585} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +LicenseFile=..\LICENSE +; Vestibule is a system-level kiosk product: install per-machine, elevated. +PrivilegesRequired=admin +ArchitecturesInstallIn64BitMode=x64compatible +OutputDir=Output +OutputBaseFilename=Vestibule-Setup-{#MyAppVersion} +SetupIconFile=icons\vestibule.ico +UninstallDisplayIcon={app}\bin\usher.exe +UninstallDisplayName={#MyAppName} {#MyAppVersion} +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +; The installer is unsigned until a code-signing budget exists (see +; README "Honest limitations"). SmartScreen will warn; operators verify +; the hash from the release notes. + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "provision"; Description: "Run the kiosk provisioning wizard after setup (configure AssignedAccess, kiosk account, autologon)"; Flags: unchecked +Name: "stagenativehost"; Description: "Also register usher as Native Messaging host for THIS user (developer mode; kiosk users get it automatically at logon)" + +[Files] +Source: "..\helper\target\release\usher.exe"; DestDir: "{app}\bin"; Flags: ignoreversion +Source: "..\extension\*"; DestDir: "{app}\extension"; Flags: recursesubdirs createallsubdirs ignoreversion +Source: "..\config\*"; DestDir: "{app}\config"; Flags: ignoreversion +Source: "..\scripts\*.ps1"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "..\scripts\*.py"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "..\scripts\vestibule-kiosk.service.in"; DestDir: "{app}\scripts"; Flags: ignoreversion +Source: "..\docs\*"; DestDir: "{app}\docs"; Flags: recursesubdirs ignoreversion +Source: "..\DEPLOYMENT.md"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\README.md"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\QUICKSTART.md"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\LICENSE"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\Provision kiosk (run as admin)"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\provision-kiosk.ps1""" +Name: "{group}\Deprovision kiosk (run as admin)"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\deprovision-kiosk.ps1""" +Name: "{group}\Deployment guide"; Filename: "{app}\DEPLOYMENT.md" +Name: "{group}\README"; Filename: "{app}\README.md" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" + +[Run] +; Developer-mode native host registration for the installing user +; (mirrors scripts/install-native-host.ps1 but from the staged binary). +Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""if (-not (Test-Path \"$env:LOCALAPPDATA\Vestibule\usher.exe\")) { New-Item -ItemType Directory -Force -Path \"$env:LOCALAPPDATA\Vestibule\" | Out-Null; Copy-Item \"{app}\bin\usher.exe\" \"$env:LOCALAPPDATA\Vestibule\usher.exe\" }"""; Tasks: stagenativehost; Flags: runhidden; Description: "Register usher for this user" +; Provisioning wizard (elevated — the installer process is elevated). +Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\scripts\provision-kiosk.ps1"""; Tasks: provision; Flags: postinstall nowait skipifsilent; Description: "Run the kiosk provisioning wizard" + +[UninstallDelete] +; The XPI is generated by provision-kiosk.ps1 after install. +Type: files; Name: "{app}\extension\vestibule.xpi" +Type: filesandordirs; Name: "{app}\Output" + +[Code] +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + ResultCode: Integer; +begin + if CurUninstallStep = usUninstall then + begin + if MsgBox('Also remove kiosk lockdown configuration?' + #13#10 + #13#10 + + 'This runs deprovision-kiosk.ps1: removes AssignedAccess/' + + 'Shell Launcher config, autologon, the kiosk shortcut, and ' + + 'Vestibule policies. Choose Yes on kiosk machines, No to ' + + 'keep OS configuration (e.g. shared dev boxes).', + mbConfirmation, MB_YESNO) = IDYES then + begin + Exec(ExpandConstant('{cmd}'), + '/C powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + + ExpandConstant('{app}') + '\scripts\deprovision-kiosk.ps1" -Quiet -RemoveKioskAccount', + '', SW_SHOW, ewWaitUntilTerminated, ResultCode); + end; + end; +end; diff --git a/scripts/deprovision-kiosk.ps1 b/scripts/deprovision-kiosk.ps1 new file mode 100644 index 0000000..b026ac5 --- /dev/null +++ b/scripts/deprovision-kiosk.ps1 @@ -0,0 +1,245 @@ +# deprovision-kiosk.ps1 — remove the Vestibule kiosk session from this +# machine (Windows). The inverse of provision-kiosk.ps1, applied in +# reverse order: +# +# 1. AssignedAccess / Shell Launcher kiosk configuration +# 2. Automatic logon registry values (only if they point at the kiosk +# account — an unrelated autologon is never touched) +# 3. The Start Menu kiosk shortcut +# 4. Vestibule's merged policies.json — each directory is reset to its +# pre-Vestibule baseline (the backed-up operator file is reinstalled +# where one exists, Vestibule's generated file is deleted where one +# does not) +# 5. C:\ProgramData\Vestibule +# 6. Optionally the kiosk account and the Program Files install +# +# Exit codes: +# 0 success +# 10 success, reboot required to fully clear kiosk mode +# 2 unsupported platform / not elevated +# 5 removal failure +# +# Usage: +# powershell -ExecutionPolicy Bypass -File deprovision-kiosk.ps1 ` +# -RemoveKioskAccount -RemoveInstall + +param( + [string]$KioskUser = "VestibuleKiosk", + [string]$InstallRoot = "C:\Program Files\Vestibule", + [switch]$RemoveKioskAccount, + [switch]$RemoveInstall, + [switch]$Quiet, + [switch]$Check, + [switch]$Restart +) + +$ErrorActionPreference = "Stop" +$AUMID = "Vestibule.Kiosk" + +function Fail([int]$code, [string]$msg) { + Write-Host "" + Write-Host "ERROR: $msg" -ForegroundColor Red + Write-Host " exiting with code $code" + exit $code +} +function Info($msg) { Write-Host $msg } +function Ok($msg) { Write-Host " [ok] $msg" -ForegroundColor Green } +function Step($msg) { Write-Host ""; Write-Host "==> $msg" -ForegroundColor Cyan } + +if ($env:OS -ne "Windows_NT") { + Fail 2 "Windows-only; on Linux use scripts/deprovision-kiosk.sh" +} +$id = [Security.Principal.WindowsIdentity]::GetCurrent() +if (-not ([Security.Principal.WindowsPrincipal]$id).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator)) { + Fail 2 "must run elevated" +} + +$rebootNeeded = $false + +# ─── Discover current state ─────────────────────────────────────────── + +$lnkPath = Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs\Vestibule Kiosk.lnk" +$wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" +$autologonUser = (Get-ItemProperty $wl -ErrorAction SilentlyContinue).DefaultUserName +$autologonOn = (Get-ItemProperty $wl -ErrorAction SilentlyContinue).AutoAdminLogon -eq "1" +$weslPresent = [bool](Get-CimClass -Namespace "root\standardcimv2\embedded" ` + -ClassName "WESL_UserSetting" -ErrorAction SilentlyContinue) +$accountPresent = [bool](Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue) + +Step "Current state" +Info "kiosk account : $(if ($accountPresent) {'present'} else {'absent'})" +Info "autologon : $(if ($autologonOn) {"on (user: $autologonUser)"} else {'off'})" +Info "Shell Launcher WMI : $(if ($weslPresent) {'present'} else {'absent'})" +Info "kiosk shortcut : $(if (Test-Path $lnkPath) {'present'} else {'absent'})" + +if ($Check) { + Info "(check only — no changes made)" + exit 0 +} +if (-not $Quiet) { + $answer = Read-Host "Proceed with removal? [y/N]" + if ($answer -notmatch '^[Yy]') { Info "aborted"; exit 0 } +} + +# ─── 1. Lockdown removal ────────────────────────────────────────────── + +Step "Remove kiosk lockdown" + +if ($weslPresent) { + $wesl = [wmiclass]"\\.\root\standardcimv2\embedded:WESL_UserSetting" + $removed = $false + foreach ($call in @( + { $wesl.RemoveCustomShell($KioskUser) }, + { $wesl.RemoveCustomShell() } + )) { + try { + $r = & $call + if ($r.ReturnValue -eq 0) { $removed = $true; break } + } catch { } + } + if ($removed) { Ok "Shell Launcher custom shell removed for '$KioskUser'" } + else { Info "Shell Launcher: no custom shell to remove (or already clean)" } +} + +# AssignedAccess removal: call any Remove* method the bridge exposes on +# this build. Bridge method availability varies by build; on some builds +# the only clean removal is deleting the kiosk account (which orphans and +# neutralizes the config) — handled below, and reported honestly. +try { + $class = Get-CimClass -Namespace "root\cimv2\mdm\dmmap" -ClassName "MDM_AssignedAccess" ` + -ErrorAction Stop + $removeMethods = @($class.CimClassMethods | Where-Object { $_.Name -like "Remove*" }) + foreach ($m in $removeMethods) { + try { + $instances = @(Get-CimInstance -Namespace "root\cimv2\mdm\dmmap" ` + -ClassName "MDM_AssignedAccess" -ErrorAction Stop) + foreach ($inst in $instances) { + $res = Invoke-CimMethod -InputObject $inst -MethodName $m.Name -ErrorAction Stop + if ($res.ReturnValue -eq 0) { + Ok "AssignedAccess cleared via $($m.Name)" + } + } + $rebootNeeded = $true + } catch { + Info "bridge method $($m.Name) present but call failed: $($_.Exception.Message)" + } + } +} catch { + Info "MDM bridge not reachable — AssignedAccess config (if any) is cleared by removing the kiosk account below" + if ($accountPresent) { $rebootNeeded = $true } +} + +# ─── 2. Automatic logon ─────────────────────────────────────────────── + +Step "Remove automatic logon" +if ($autologonOn -and $autologonUser -eq $KioskUser) { + Set-ItemProperty $wl -Name "AutoAdminLogon" -Value "0" -Type String + Remove-ItemProperty $wl -Name "DefaultUserName" -ErrorAction SilentlyContinue + Remove-ItemProperty $wl -Name "DefaultDomainName" -ErrorAction SilentlyContinue + Remove-ItemProperty $wl -Name "DefaultPassword" -ErrorAction SilentlyContinue + Ok "autologon disabled and credentials cleared" +} elseif ($autologonOn) { + Info "autologon is configured for '$autologonUser' (not ours) — left untouched" +} else { + Info "autologon already off" +} + +# ─── 3. Shortcut ────────────────────────────────────────────────────── + +Step "Remove kiosk shortcut" +if (Test-Path $lnkPath) { + Remove-Item $lnkPath -Force + Ok "removed $lnkPath" +} else { + Info "already absent" +} + +# ─── 4. Policies ────────────────────────────────────────────────────── + +Step "Reset browser policies to baseline" +# LibreWolf and Firefox share the distribution/policies.json mechanism; +# cover every install location for both browsers. Step-down per +# directory: backup present -> reinstall it; ours -> delete; else leave. +foreach ($browserExe in @( + "${env:ProgramFiles}\LibreWolf\librewolf.exe", + "${env:ProgramFiles(x86)}\LibreWolf\librewolf.exe", + "${env:LOCALAPPDATA}\Programs\LibreWolf\librewolf.exe", + "${env:ProgramFiles}\Mozilla Firefox\firefox.exe", + "${env:ProgramFiles}\Mozilla Firefox ESR\firefox.exe", + "${env:ProgramFiles(x86)}\Mozilla Firefox\firefox.exe", + "${env:ProgramFiles(x86)}\Mozilla Firefox ESR\firefox.exe", + "${env:LOCALAPPDATA}\Mozilla Firefox\firefox.exe")) { + if (-not (Test-Path $browserExe)) { continue } + $distDir = Join-Path (Split-Path -Parent $browserExe) "distribution" + $policiesPath = Join-Path $distDir "policies.json" + $bak = "$policiesPath.vestibule-bak" + if (Test-Path $bak) { + Move-Item $bak $policiesPath -Force + Ok "baseline policies.json reinstalled in $distDir (from vestibule-bak)" + } elseif (Test-Path $policiesPath) { + $content = Get-Content $policiesPath -Raw + if ($content -match "vestibule@vestibule\.kiosk") { + Remove-Item $policiesPath -Force + Ok "Vestibule-generated policies.json deleted in $distDir (no baseline on record)" + } else { + Info "policies.json in $distDir exists but is not ours — left untouched" + } + } + # librewolf.overrides.cfg is LibreWolf-only; harmless no-op for Firefox. + $cfg = Join-Path (Split-Path -Parent $browserExe) "librewolf.overrides.cfg" + if (Test-Path $cfg) { Remove-Item $cfg -Force; Ok "removed librewolf.overrides.cfg" } +} + +# ─── 5. ProgramData ─────────────────────────────────────────────────── + +Step "Remove deployment config" +$pdDir = Join-Path $env:ProgramData "Vestibule" +if (Test-Path $pdDir) { + Remove-Item $pdDir -Recurse -Force + Ok "removed $pdDir" +} else { + Info "already absent" +} + +# ─── 6. Optional: account + install tree ────────────────────────────── + +if ($RemoveKioskAccount) { + Step "Remove kiosk account" + if ($accountPresent) { + # Ensure the account is not mid-logon (fast user switching) first. + try { + Remove-LocalUser -Name $KioskUser -ErrorAction Stop + Ok "removed local account '$KioskUser'" + $rebootNeeded = $true + } catch { + Fail 5 "could not remove account: $($_.Exception.Message)" + } + } else { + Info "account '$KioskUser' already absent" + } +} + +if ($RemoveInstall) { + Step "Remove install tree" + if (Test-Path $InstallRoot) { + Remove-Item $InstallRoot -Recurse -Force + Ok "removed $InstallRoot" + } else { + Info "already absent" + } +} + +# ─── Summary ────────────────────────────────────────────────────────── + +Step "Deprovisioning complete" +Info "verify in Settings > Accounts > Other users that no kiosk entry remains" +if ($rebootNeeded) { + Info "reboot recommended to fully clear kiosk mode." + if ($Restart) { + shutdown.exe /r /t 10 /c "Vestibule kiosk removal" + exit 0 + } + exit 10 +} +exit 0 diff --git a/scripts/deprovision-kiosk.sh b/scripts/deprovision-kiosk.sh new file mode 100755 index 0000000..3a10211 --- /dev/null +++ b/scripts/deprovision-kiosk.sh @@ -0,0 +1,237 @@ +#!/bin/sh +# deprovision-kiosk.sh — remove the Vestibule kiosk session from this +# machine (Linux). The inverse of provision-kiosk.sh, applied in reverse +# order: +# +# 1. The vestibule-kiosk systemd unit (stop, disable, delete) +# 2. /usr/local/bin/vestibule-kiosk-launch and /etc/vestibule +# 3. Vestibule's merged policies.json everywhere it was deployed — +# LibreWolf native/Flatpak, Firefox native (/etc/firefox), and the +# kiosk-home locations for Firefox Flatpak/snap. Each directory is +# reset to its pre-Vestibule baseline: the backed-up operator file +# is reinstalled where one exists, Vestibule's generated file is +# deleted where one does not. +# 4. The usher Native Messaging manifests from the kiosk user's home +# (all five locations: LibreWolf + Firefox, native + Flatpak + snap) +# 5. The sandbox-visible XPI copies (Flatpak/snap flavors) +# 6. Optionally: the kiosk account, /opt/vestibule, /usr/local/bin/usher +# +# The regular getty/console login is never modified, so removing the +# unit is all it takes to return the machine to a stock boot. +# +# Exit codes: +# 0 success +# 2 not root / no systemd +# 5 removal failure +# +# Usage: +# sudo ./deprovision-kiosk.sh # keep account + /opt +# sudo ./deprovision-kiosk.sh --remove-user --remove-opt --remove-usher +# +# POSIX sh — no bashisms. + +set -eu + +KIOSK_USER="vestibule-kiosk" +LW_FLATPAK_APP="io.gitlab.librewolf-community" +FF_FLATPAK_APP="org.mozilla.firefox" +OPT_ROOT="/opt/vestibule" +UNIT_DST="/etc/systemd/system/vestibule-kiosk.service" +LAUNCH_DST="/usr/local/bin/vestibule-kiosk-launch" +ENV_DIR="/etc/vestibule" +USHER_DST="/usr/local/bin/usher" +REMOVE_USER=0 +REMOVE_OPT=0 +REMOVE_USHER=0 +ASSUME_YES=0 +CHECK=0 + +usage() { + cat <<'EOF' +deprovision-kiosk.sh — remove the Vestibule kiosk session from this machine + +Options: + --kiosk-user NAME Kiosk account name (default: vestibule-kiosk) + --remove-user Also delete the kiosk account and its home directory + --remove-opt Also delete /opt/vestibule (staged files, XPI, docs) + --remove-usher Also delete /usr/local/bin/usher + --yes Skip the confirmation prompt + --check Show what would be removed; change nothing + -h, --help This text +EOF +} + +die() { + code="$1"; msg="$2" + echo "" + echo "ERROR: ${msg}" >&2 + echo " exiting with code ${code}" >&2 + exit "${code}" +} +info() { echo "$1"; } +ok() { echo " [ok] $1"; } +step() { echo ""; echo "==> $1"; } + +while [ $# -gt 0 ]; do + case "$1" in + --kiosk-user) KIOSK_USER="$2"; shift 2 ;; + --remove-user) REMOVE_USER=1; shift ;; + --remove-opt) REMOVE_OPT=1; shift ;; + --remove-usher) REMOVE_USHER=1; shift ;; + --yes|-y) ASSUME_YES=1; shift ;; + --check) CHECK=1; shift ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; die 5 "unknown option: $1" ;; + esac +done + +if [ ! -d /run/systemd/system ]; then + die 2 "systemd is not the running init system" +fi + +# ─── Discover current state ─────────────────────────────────────────── + +UNIT_ACTIVE=0 +if systemctl is-active vestibule-kiosk.service >/dev/null 2>&1; then + UNIT_ACTIVE=1 +fi +UNIT_ENABLED=0 +if systemctl is-enabled vestibule-kiosk.service >/dev/null 2>&1; then + UNIT_ENABLED=1 +fi +KIOSK_HOME="" +if id -u "${KIOSK_USER}" >/dev/null 2>&1; then + KIOSK_HOME=$(getent passwd "${KIOSK_USER}" | cut -d: -f6) +fi + +step "Current state" +info "unit : $(if [ -f "${UNIT_DST}" ]; then echo present; else echo absent; fi) (active: ${UNIT_ACTIVE}, enabled: ${UNIT_ENABLED})" +info "launcher : $(if [ -x "${LAUNCH_DST}" ]; then echo present; else echo absent; fi)" +info "kiosk.env : $(if [ -f "${ENV_DIR}/kiosk.env" ]; then echo present; else echo absent; fi)" +info "kiosk account : $(if [ -n "${KIOSK_HOME}" ]; then echo "present (home: ${KIOSK_HOME})"; else echo absent; fi)" +info "opt root : $(if [ -d "${OPT_ROOT}" ]; then echo present; else echo absent; fi)" + +if [ "${CHECK}" -eq 1 ]; then + info "(check only — no changes made)" + exit 0 +fi + +[ "$(id -u)" -eq 0 ] || die 2 "must run as root (sudo)" + +if [ "${ASSUME_YES}" -eq 0 ]; then + echo "" + printf "Proceed with removal? [y/N] " + read -r answer + case "${answer}" in + y|Y|yes|YES) ;; + *) info "aborted"; exit 0 ;; + esac +fi + +# ─── 1. systemd unit ────────────────────────────────────────────────── + +step "Remove kiosk session" +if [ -f "${UNIT_DST}" ]; then + systemctl stop vestibule-kiosk.service 2>/dev/null || true + systemctl disable vestibule-kiosk.service 2>/dev/null || true + rm -f "${UNIT_DST}" + systemctl daemon-reload + ok "unit stopped, disabled, and removed" +else + info "unit already absent" +fi + +# ─── 2. Launcher + env ──────────────────────────────────────────────── + +step "Remove launcher + session config" +if [ -x "${LAUNCH_DST}" ]; then + rm -f "${LAUNCH_DST}" + ok "removed ${LAUNCH_DST}" +fi +if [ -d "${ENV_DIR}" ]; then + rm -rf "${ENV_DIR}" + ok "removed ${ENV_DIR}" +fi + +# ─── 3. Policies ────────────────────────────────────────────────────── + +step "Reset browser policies to baseline" +reset_policy_dir() { + # Step-down, one decision per line: + # absent directory -> nothing to do + # baseline backup present -> reinstall it over Vestibule's merge + # Vestibule-generated file -> delete it (no pre-Vestibule baseline) + _dir="$1" + if [ ! -d "${_dir}" ]; then + return + fi + _dst="${_dir}/policies.json" + _bak="${_dst}.vestibule-bak" + if [ -f "${_bak}" ]; then + mv "${_bak}" "${_dst}" + ok "baseline policies.json reinstalled in ${_dir}" + elif [ -f "${_dst}" ] && grep -q "vestibule@vestibule\.kiosk" "${_dst}" 2>/dev/null; then + rm -f "${_dst}" + ok "Vestibule-generated policies.json deleted in ${_dir} (no baseline on record)" + fi +} + +reset_policy_dir /usr/lib/librewolf/distribution +reset_policy_dir /usr/share/librewolf/distribution +reset_policy_dir /opt/librewolf/distribution +reset_policy_dir "/var/lib/flatpak/app/${LW_FLATPAK_APP}/current/active/files/librewolf/distribution" +reset_policy_dir /etc/firefox/policies +if [ -n "${KIOSK_HOME}" ]; then + reset_policy_dir "${KIOSK_HOME}/.var/app/${FF_FLATPAK_APP}/.mozilla/policies" + reset_policy_dir "${KIOSK_HOME}/snap/firefox/common/.mozilla/policies" +fi + +# ─── 4. Native Messaging manifests ──────────────────────────────────── + +step "Remove Native Messaging manifests" +if [ -n "${KIOSK_HOME}" ]; then + rm -f "${KIOSK_HOME}/.librewolf/native-messaging-hosts/com.vestibule.usher.json" + rm -f "${KIOSK_HOME}/.mozilla/native-messaging-hosts/com.vestibule.usher.json" + rm -f "${KIOSK_HOME}/.var/app/${LW_FLATPAK_APP}/.librewolf/native-messaging-hosts/com.vestibule.usher.json" + rm -f "${KIOSK_HOME}/.var/app/${FF_FLATPAK_APP}/.mozilla/native-messaging-hosts/com.vestibule.usher.json" + rm -f "${KIOSK_HOME}/snap/firefox/common/.mozilla/native-messaging-hosts/com.vestibule.usher.json" + # XPI copies staged for sandbox-visible installs (flatpak/snap). + rm -f "${KIOSK_HOME}/.var/app/${LW_FLATPAK_APP}/vestibule.xpi" + rm -f "${KIOSK_HOME}/.var/app/${FF_FLATPAK_APP}/vestibule.xpi" + rm -f "${KIOSK_HOME}/snap/firefox/common/vestibule.xpi" + ok "manifests + sandbox XPI copies removed from ${KIOSK_HOME}" +else + info "kiosk account already absent — nothing to clean" +fi + +# ─── 5. Optional removals ───────────────────────────────────────────── + +if [ "${REMOVE_USHER}" -eq 1 ]; then + step "Remove usher binary" + if [ -f "${USHER_DST}" ]; then + rm -f "${USHER_DST}" + ok "removed ${USHER_DST}" + fi +fi + +if [ "${REMOVE_OPT}" -eq 1 ]; then + step "Remove ${OPT_ROOT}" + if [ -d "${OPT_ROOT}" ]; then + rm -rf "${OPT_ROOT}" + ok "removed ${OPT_ROOT}" + fi +fi + +if [ "${REMOVE_USER}" -eq 1 ]; then + step "Remove kiosk account" + if [ -n "${KIOSK_HOME}" ]; then + userdel -r "${KIOSK_USER}" || die 5 "userdel failed" + ok "removed account '${KIOSK_USER}' and its home" + else + info "account already absent" + fi +fi + +step "Deprovisioning complete" +info "next boot returns to the normal login prompt" +exit 0 diff --git a/scripts/install-native-host.ps1 b/scripts/install-native-host.ps1 new file mode 100755 index 0000000..386e119 --- /dev/null +++ b/scripts/install-native-host.ps1 @@ -0,0 +1,50 @@ +# Registers usher as a Firefox/LibreWolf native messaging host on Windows +# for the current user. No admin elevation required — installs to +# $env:LOCALAPPDATA\Vestibule and registers under HKCU. +# +# After running this script, restart LibreWolf and load the extension from +# about:debugging to verify usher connects (check the Browser Console: +# Ctrl+Shift+J). +$ErrorActionPreference = "Stop" + +$hostName = "com.vestibule.usher" +$extId = "vestibule@vestibule.kiosk" +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$projectRoot = Split-Path -Parent $scriptDir +$helperBin = Join-Path $projectRoot "helper\target\release\usher.exe" + +if (-not (Test-Path $helperBin)) { + Write-Error "usher.exe not found at $helperBin" + Write-Host " Build it first: cd helper; cargo build --release" + exit 1 +} + +# Install location: %LOCALAPPDATA%\Vestibule (no admin needed) +$installDir = Join-Path $env:LOCALAPPDATA "Vestibule" +New-Item -ItemType Directory -Force -Path $installDir | Out-Null +$destExe = Join-Path $installDir "usher.exe" +Copy-Item $helperBin $destExe -Force +Write-Host "installed binary: $destExe" + +# Generate manifest JSON with the correct path +$manifest = @{ + name = $hostName + description = "Vestibule native helper" + path = $destExe + type = "stdio" + allowed_extensions = @($extId) +} +$manifestPath = Join-Path $installDir "$hostName.json" +$manifest | ConvertTo-Json -Depth 5 | Set-Content $manifestPath -Encoding UTF8 +Write-Host "manifest: $manifestPath" + +# Register in registry under HKCU (no admin needed) +# LibreWolf reads from HKCU\Software\Mozilla\NativeMessagingHosts\ +$regKey = "HKCU:\Software\Mozilla\NativeMessagingHosts\$hostName" +if (-not (Test-Path $regKey)) { New-Item -Path $regKey -Force | Out-Null } +Set-ItemProperty -Path $regKey -Name "(default)" -Value $manifestPath +Write-Host "registry: $regKey -> $manifestPath" + +Write-Host "" +Write-Host "Done. Restart LibreWolf, load the extension from about:debugging," +Write-Host "and check the Browser Console (Ctrl+Shift+J) for '[vestibule] usher hello'." diff --git a/scripts/install-native-host.sh b/scripts/install-native-host.sh new file mode 100755 index 0000000..f2c3dd8 --- /dev/null +++ b/scripts/install-native-host.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Registers usher as a Firefox/LibreWolf native messaging host for the +# current user on Linux. Installs to ~/.local/bin and registers the +# manifest in ~/.librewolf/native-messaging-hosts and +# ~/.mozilla/native-messaging-hosts. No sudo required. +# +# POSIX sh — no bashisms. Runs on any minimal Linux base. +# After running, restart LibreWolf and load the extension from +# about:debugging to verify usher connects (check the Browser Console: +# Ctrl+Shift+J). + +set -eu + +HOST_NAME="com.vestibule.usher" +EXT_ID="vestibule@vestibule.kiosk" + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +PROJECT_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd) + +HELPER_BIN="${PROJECT_ROOT}/helper/target/release/usher" + +if [ ! -x "${HELPER_BIN}" ]; then + echo "error: usher binary not found at ${HELPER_BIN}" >&2 + echo " build it first: (cd ${PROJECT_ROOT}/helper && cargo build --release)" >&2 + exit 1 +fi + +# Install binary to ~/.local/bin (no sudo required) +TARGET_BIN="${HOME}/.local/bin/usher" +BIN_DIR=$(dirname "${TARGET_BIN}") +mkdir -p "${BIN_DIR}" +install -m 0755 "${HELPER_BIN}" "${TARGET_BIN}" +echo "installed binary: ${TARGET_BIN}" + +# Substitute the absolute install path into the manifest template. +# Paths travel as argv, never interpolated into source (SEI CERT +# IDS03-ENUM; a path containing a quote must not alter the program). +MANIFEST_TEMPLATE="${PROJECT_ROOT}/config/com.vestibule.usher.linux.json" +GENERATED_MANIFEST=$(mktemp) +trap 'rm -f "${GENERATED_MANIFEST}"' EXIT INT TERM + +python3 - "${MANIFEST_TEMPLATE}" "${GENERATED_MANIFEST}" "${TARGET_BIN}" <<'PYEOF' +import json +import sys + +template, generated, target_bin = sys.argv[1:4] + +with open(template) as f: + manifest = json.load(f) +manifest["path"] = target_bin +with open(generated, "w") as f: + json.dump(manifest, f, indent=2) +PYEOF + +# Install manifest into both LibreWolf and Mozilla native-messaging-hosts +# directories. LibreWolf reads from ~/.librewolf, vanilla Firefox from +# ~/.mozilla. Covering both is harmless and future-proof. +for DIR in \ + "${HOME}/.librewolf/native-messaging-hosts" \ + "${HOME}/.mozilla/native-messaging-hosts"; do + mkdir -p "${DIR}" + install -m 0644 "${GENERATED_MANIFEST}" "${DIR}/${HOST_NAME}.json" + echo "installed manifest: ${DIR}/${HOST_NAME}.json" +done + +echo +echo "Done. Restart LibreWolf, load the extension from about:debugging," +echo "and check the Browser Console (Ctrl+Shift+J) for '[vestibule] usher hello'." diff --git a/scripts/install-usher-service.sh b/scripts/install-usher-service.sh new file mode 100755 index 0000000..64f3065 --- /dev/null +++ b/scripts/install-usher-service.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# Installs the usher systemd user service for crash recovery. +# This is optional — in normal operation, LibreWolf manages usher's +# lifecycle via Native Messaging. This service ensures usher is +# available for pre-LibreWolf power monitoring and restarts on crash. +# +# POSIX sh. No sudo required (user-level systemd unit). + +set -eu + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +PROJECT_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd) +SERVICE_SRC="${PROJECT_ROOT}/scripts/vestibule-usher.service" +SERVICE_DST="${HOME}/.config/systemd/user/vestibule-usher.service" + +if [ ! -x "${HOME}/.local/bin/usher" ]; then + echo "error: usher not installed at ~/.local/bin/usher" >&2 + echo " run install-native-host.sh first" >&2 + exit 1 +fi + +mkdir -p "$(dirname "${SERVICE_DST}")" + +# Substitute %h with the actual home directory (systemd user units +# support %h natively, but some older versions don't — do it explicitly) +sed "s|%h|${HOME}|g" "${SERVICE_SRC}" > "${SERVICE_DST}" +echo "installed: ${SERVICE_DST}" + +systemctl --user daemon-reload +systemctl --user enable vestibule-usher.service +echo "enabled: vestibule-usher.service" + +echo +echo "To start now: systemctl --user start vestibule-usher.service" +echo "To check: systemctl --user status vestibule-usher.service" +echo "To view logs: journalctl --user -u vestibule-usher.service -f" diff --git a/scripts/install-usher-task.ps1 b/scripts/install-usher-task.ps1 new file mode 100755 index 0000000..3115dea --- /dev/null +++ b/scripts/install-usher-task.ps1 @@ -0,0 +1,44 @@ +# Installs a Windows Scheduled Task that starts usher at logon and +# restarts on failure. This is optional — in normal operation, LibreWolf +# manages usher's lifecycle via Native Messaging. This task ensures +# usher is available for pre-LibreWolf power monitoring. +$ErrorActionPreference = "Stop" + +$taskName = "VestibuleUsher" +$usherPath = Join-Path $env:LOCALAPPDATA "Vestibule\usher.exe" + +if (-not (Test-Path $usherPath)) { + Write-Error "usher.exe not found at $usherPath" + Write-Host " Run install-native-host.ps1 first" + exit 1 +} + +# Remove existing task if present (idempotent) +$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue +if ($existing) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false + Write-Host "removed existing task: $taskName" +} + +$action = New-ScheduledTaskAction -Execute $usherPath +$trigger = New-ScheduledTaskTrigger -AtLogOn +$settings = New-ScheduledTaskSettingsSet ` + -RestartCount 3 ` + -RestartInterval (New-TimeSpan -Minutes 1) ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries + +Register-ScheduledTask ` + -TaskName $taskName ` + -Action $action ` + -Trigger $trigger ` + -Settings $settings ` + -Description "Vestibule usher — kiosk unlock helper + power monitor" ` + -RunLevel Limited + +Write-Host "installed scheduled task: $taskName" +Write-Host " starts at logon, restarts up to 3x on failure" +Write-Host "" +Write-Host "To start now: Start-ScheduledTask -TaskName $taskName" +Write-Host "To check: Get-ScheduledTask -TaskName $taskName | Get-ScheduledTaskInfo" +Write-Host "To view logs: Event Viewer > Windows Logs > Application" diff --git a/scripts/kiosk-launch.ps1 b/scripts/kiosk-launch.ps1 new file mode 100644 index 0000000..d811fb1 --- /dev/null +++ b/scripts/kiosk-launch.ps1 @@ -0,0 +1,225 @@ +# kiosk-launch.ps1 — per-user bootstrap + kiosk browser supervisor (Windows) +# +# This is the process Windows launches in AssignedAccess single-app kiosk +# mode (the "Vestibule Kiosk" Start Menu shortcut points here). It runs as +# the kiosk user, NOT as an administrator, and must never prompt. +# +# Responsibilities, in order: +# 1. Read deployment config (C:\ProgramData\Vestibule\kiosk.env) +# 2. Locate the browser (LibreWolf or Firefox, per kiosk.env) +# 3. Ensure usher.exe is installed per-user (%LOCALAPPDATA%\Vestibule) +# 4. Ensure the Native Messaging host is registered under HKCU +# 5. Ensure the dedicated vestibule-profile exists +# 6. Launch the browser --kiosk, wait, relaunch on exit (crash recovery) +# +# Everything is logged to %LOCALAPPDATA%\Vestibule\kiosk-launch.log so a +# headless kiosk can be diagnosed after the fact. The log rotates at +# 512 KiB so a crash-loop cannot fill the disk. +# +# Why per-user bootstrap instead of provisioning-time HKU writes: the +# kiosk account's profile (and HKCU hive) does not exist until first +# logon, and AssignedAccess kiosk sessions never run RunOnce entries. +# Registering from inside the kiosk session is the only path that needs +# no admin rights — it matches the project's no-elevation philosophy. +# +# Params: +# -InstallRoot Vestibule install dir (default: C:\Program Files\Vestibule) +# -MaxRestarts Browser relaunches before this launcher exits and lets +# AssignedAccess restart it (default: 50) +param( + [string]$InstallRoot = "C:\Program Files\Vestibule", + [int]$MaxRestarts = 50 +) + +$ErrorActionPreference = "Continue" # kiosk must never die on a soft error +$hostName = "com.vestibule.usher" +$extId = "vestibule@vestibule.kiosk" +$profileName = "vestibule-profile" +$logMaxBytes = 524288 + +# ─── Logging ────────────────────────────────────────────────────────── + +function Get-LogPath { + $dir = Join-Path $env:LOCALAPPDATA "Vestibule" + New-Item -ItemType Directory -Force -Path $dir | Out-Null + return (Join-Path $dir "kiosk-launch.log") +} + +function Log($msg) { + $line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $msg + Write-Host $line + try { + $path = Get-LogPath + if ((Get-Item $path -ErrorAction SilentlyContinue).Length -gt $logMaxBytes) { + Move-Item $path "$path.old" -Force + } + Add-Content -Path $path -Value $line -Encoding UTF8 + } catch { } +} + +# ─── Config ─────────────────────────────────────────────────────────── + +function Read-KioskEnv { + # KEY=VALUE lines from C:\ProgramData\Vestibule\kiosk.env, written by + # provision-kiosk.ps1. Operators may edit it to change the home URL + # without re-running provisioning. + $env_ = @{} + $envFile = Join-Path $env:ProgramData "Vestibule\kiosk.env" + if (Test-Path $envFile) { + foreach ($line in Get-Content $envFile) { + if ($line -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.*)\s*$') { + $env_[$matches[1]] = $matches[2] + } + } + } + return $env_ +} + +# ─── Browser discovery (LibreWolf or Firefox) ─────────────────────── + +$librewolfSearchPaths = @( + "${env:ProgramFiles}\LibreWolf\librewolf.exe", + "${env:ProgramFiles(x86)}\LibreWolf\librewolf.exe", + "${env:LOCALAPPDATA}\Programs\LibreWolf\librewolf.exe" +) + +$firefoxSearchPaths = @( + "${env:ProgramFiles}\Mozilla Firefox\firefox.exe", + "${env:ProgramFiles}\Mozilla Firefox ESR\firefox.exe", + "${env:ProgramFiles(x86)}\Mozilla Firefox\firefox.exe", + "${env:ProgramFiles(x86)}\Mozilla Firefox ESR\firefox.exe", + "${env:LOCALAPPDATA}\Mozilla Firefox\firefox.exe" +) + +function Find-InPaths($paths, $exeName) { + $hit = $paths | Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($hit) { return $hit } + # Registry App Paths step-down: per-machine first, then per-user. + $regRoots = @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths", + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths") + return $regRoots | + ForEach-Object { (Get-ItemProperty (Join-Path $_ $exeName) -ErrorAction SilentlyContinue)."(default)" } | + Where-Object { $_ -and (Test-Path $_) } | + Select-Object -First 1 +} + +function Find-LibreWolf { return Find-InPaths $librewolfSearchPaths "librewolf.exe" } +function Find-Firefox { return Find-InPaths $firefoxSearchPaths "firefox.exe" } + +function Resolve-Browser([string]$preference) { + # Returns @{ Kind = ...; Exe = ... } or $null. The preference comes + # from kiosk.env (VESTIBULE_BROWSER); "auto" steps down LibreWolf -> + # Firefox — same order as provision-kiosk.ps1. + $lw = Find-LibreWolf + $ff = Find-Firefox + switch ($preference) { + "firefox" { if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } } + "librewolf" { if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } } + default { + if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } + if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } + } + } + # Preference not satisfiable: step down to whatever exists. + if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } + if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } + return $null +} + +# ─── Per-user bootstrap steps ───────────────────────────────────────── + +function Ensure-Usher { + $src = Join-Path $InstallRoot "bin\usher.exe" + $dir = Join-Path $env:LOCALAPPDATA "Vestibule" + $dst = Join-Path $dir "usher.exe" + if (-not (Test-Path $src)) { + Log "usher source missing: $src — native messaging will not work" + return $false + } + if (-not (Test-Path $dst)) { + New-Item -ItemType Directory -Force -Path $dir | Out-Null + Copy-Item $src $dst -Force + Log "installed usher per-user: $dst" + } + return $true +} + +function Ensure-NativeHostRegistration([string]$usherPath) { + $manifestPath = Join-Path $env:LOCALAPPDATA "Vestibule\$hostName.json" + $dir = Split-Path -Parent $manifestPath + if (-not (Test-Path $dir)) { + New-Item -ItemType Directory -Force -Path $dir | Out-Null + } + if (-not (Test-Path $manifestPath)) { + $manifest = @{ + name = $hostName + description = "Vestibule native helper" + path = $usherPath + type = "stdio" + allowed_extensions = @($extId) + } + $manifest | ConvertTo-Json -Depth 5 | Set-Content $manifestPath -Encoding UTF8 + Log "wrote native host manifest: $manifestPath" + } + $regKey = "HKCU:\Software\Mozilla\NativeMessagingHosts\$hostName" + if (-not (Test-Path $regKey)) { + New-Item -Path $regKey -Force | Out-Null + } + $current = (Get-ItemProperty $regKey -ErrorAction SilentlyContinue)."(default)" + if ($current -ne $manifestPath) { + Set-ItemProperty -Path $regKey -Name "(default)" -Value $manifestPath + Log "registered native host: $regKey -> $manifestPath" + } +} + +function Ensure-Profile([string]$browserExe) { + # -CreateProfile is idempotent: an existing profile is left untouched. + # It writes to profiles.ini in the kiosk user's own profile dir. + & $browserExe -CreateProfile $profileName 2>$null | Out-Null + Log "ensured profile: $profileName" +} + +# ─── Main ───────────────────────────────────────────────────────────── + +Log "=== vestibule kiosk-launch starting (pid $PID) ===" +$config = Read-KioskEnv +$homeUrl = $config["VESTIBULE_HOME_URL"] +if (-not $homeUrl) { $homeUrl = "about:blank" } +$browserPref = $config["VESTIBULE_BROWSER"] +if (-not $browserPref) { $browserPref = "auto" } + +$browser = Resolve-Browser $browserPref +if (-not $browser) { + Log "FATAL: no LibreWolf or Firefox found in any known location" + Start-Sleep -Seconds 30 # let AssignedAccess's watchdog see us exit + exit 3 +} +$browserExe = $browser.Exe +Log "browser ($($browser.Kind)): $browserExe" + +if (Ensure-Usher) { + Ensure-NativeHostRegistration (Join-Path $env:LOCALAPPDATA "Vestibule\usher.exe") +} +Ensure-Profile $browserExe +Log "home url: $homeUrl" + +$restarts = 0 +while ($true) { + # Supervision loop: relaunch the browser until the restart budget is + # spent, then hand the job to AssignedAccess's own watchdog. + Log "launching $($browser.Kind) (kiosk mode, restart #$restarts)" + try { + $proc = Start-Process -FilePath $browserExe ` + -ArgumentList @("--kiosk", "-P", $profileName, "-no-remote", $homeUrl) ` + -PassThru -Wait + Log "$($browser.Kind) exited with code $($proc.ExitCode)" + } catch { + Log "launch failed: $($_.Exception.Message)" + } + $restarts++ + if ($restarts -gt $MaxRestarts) { + Log "restart budget exhausted — exiting so AssignedAccess takes over" + exit 0 + } + Start-Sleep -Seconds 5 +} diff --git a/scripts/make-icons.py b/scripts/make-icons.py new file mode 100644 index 0000000..38934fb --- /dev/null +++ b/scripts/make-icons.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Generate the Vestibule icon set (SVG + PNG sizes + Windows .ico). + +Design: a doorway arch (a vestibule is an entrance hall) in cyan on a +deep-slate rounded square, with an amber visitor dot — the kiosk is the +doorway, the public is the visitor. + +Outputs (into /packaging/icons/): + vestibule.svg scalable source (Flatpak/scalable) + vestibule-.png 16..256 px hicolor sizes (Flatpak) + vestibule.ico multi-size Windows icon (Inno Setup) + +Usage: python3 scripts/make-icons.py +""" +import os + +from PIL import Image, ImageDraw + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +OUT = os.path.join(REPO, "packaging", "icons") + +BG = (15, 23, 42, 255) # slate-900 +BG_EDGE = (30, 41, 59, 255) # slate-800 rim +CYAN = (56, 189, 248, 255) # sky-400 arch +AMBER = (251, 191, 36, 255) # amber-400 visitor + +SIZES = [16, 32, 48, 64, 128, 256] +SIMPLE_SIZE_MAX = 32 # at or below: legibility beats detail + + +def geometry(size: int) -> dict: + """Scale table: every dimension derives from the canvas size.""" + s = size + inset_x = int(s * 0.30) + arch_w = s - 2 * inset_x + return { + "s": s, + "radius": max(2, int(s * 0.18)), + "rim_w": max(1, int(s * 0.023)), + "stroke": max(2, int(s * 0.075)), + "inset_x": inset_x, + "arch_top": int(s * 0.18), + "leg_bottom": int(s * 0.82), + "arch_w": arch_w, + "arch_cx": s // 2, + "arch_r": arch_w // 2, + "dot_r": max(2, int(s * 0.085)), + } + + +def draw_arch(d, g, stroke, with_threshold=True): + """Doorway arch (dome + legs) + visitor dot; threshold optional + (small canvases drop it for legibility).""" + s, x, w = g["s"], g["inset_x"], g["arch_w"] + dome_bottom = g["arch_top"] + g["arch_r"] + # Dome (top half circle outline). + d.arc([x, g["arch_top"], x + w, g["arch_top"] + w], + start=180, end=360, fill=CYAN, width=stroke) + # Legs. + d.line([x, dome_bottom, x, g["leg_bottom"]], fill=CYAN, width=stroke) + d.line([s - x, dome_bottom, s - x, g["leg_bottom"]], fill=CYAN, width=stroke) + if with_threshold: + d.line([x - int(s * 0.04), g["leg_bottom"], s - x + int(s * 0.04), g["leg_bottom"]], + fill=CYAN, width=stroke) + # Visitor: amber dot on the threshold, centered. + dot_cy = g["leg_bottom"] - int(s * 0.02) - g["dot_r"] + d.ellipse([g["arch_cx"] - g["dot_r"], dot_cy - g["dot_r"], + g["arch_cx"] + g["dot_r"], dot_cy + g["dot_r"]], fill=AMBER) + + +def draw_full_icon(size: int) -> Image.Image: + """Full-detail icon: rimmed background + arch.""" + g = geometry(size) + s = g["s"] + img = Image.new("RGBA", (s, s), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + # Background: rounded square with a subtle rim. + d.rounded_rectangle([0, 0, s - 1, s - 1], radius=g["radius"], fill=BG) + d.rounded_rectangle([g["rim_w"], g["rim_w"], s - 1 - g["rim_w"], s - 1 - g["rim_w"]], + radius=max(1, g["radius"] - g["rim_w"]), outline=BG_EDGE, + width=g["rim_w"]) + draw_arch(d, g, g["stroke"]) + return img + + +def draw_simple_icon(size: int) -> Image.Image: + """Legibility variant for small canvases: rim and threshold dropped, + minimum stroke widths.""" + g = geometry(size) + s = g["s"] + img = Image.new("RGBA", (s, s), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + d.rounded_rectangle([0, 0, s - 1, s - 1], radius=g["radius"], fill=BG) + draw_arch(d, g, max(2, g["stroke"]), with_threshold=False) + return img + + +def draw_icon(size: int) -> Image.Image: + """Step-down: small canvases use the simple variant, all others full.""" + if size <= SIMPLE_SIZE_MAX: + return draw_simple_icon(size) + return draw_full_icon(size) + + +SVG = """ + + + + + + + + + + + +""" + + +def main(): + os.makedirs(OUT, exist_ok=True) + + with open(os.path.join(OUT, "vestibule.svg"), "w") as f: + f.write(SVG) + + rendered = {size: render_and_save(size) for size in SIZES} + + # Windows .ico: multi-size, PNG-compressed entries for large sizes. + rendered[256].save( + os.path.join(OUT, "vestibule.ico"), + format="ICO", + sizes=[(s, s) for s in (16, 32, 48, 64, 128, 256)], + append_images=[rendered[s] for s in (128, 64, 48, 32, 16)], + ) + + print(f"wrote icon set to {OUT}") + print("\n".join( + f" {name} ({os.path.getsize(os.path.join(OUT, name))} bytes)" + for name in sorted(os.listdir(OUT)) + )) + + +def render_and_save(size: int) -> Image.Image: + img = draw_icon(size) + img.save(os.path.join(OUT, f"vestibule-{size}.png")) + return img + + +if __name__ == "__main__": + main() diff --git a/scripts/provision-kiosk.ps1 b/scripts/provision-kiosk.ps1 new file mode 100644 index 0000000..843792a --- /dev/null +++ b/scripts/provision-kiosk.ps1 @@ -0,0 +1,683 @@ +# provision-kiosk.ps1 — Windows kiosk provisioning wizard +# +# Turns a Windows Pro/Enterprise/Education machine into a Vestibule +# kiosk without manual OS configuration: +# +# 1. Stage install files to C:\Program Files\Vestibule (if not already +# installed there by the Inno Setup installer) +# 2. Create the dedicated kiosk local account +# 3. Build the extension XPI and deploy a merged policies.json to the +# browser's distribution directory (policy force-installs the +# extension, so no about:debugging step on the kiosk) — works for +# LibreWolf and Firefox alike +# 4. Write C:\ProgramData\Vestibule\kiosk.env (home URL + browser) +# 5. Create the Start Menu shortcut with a stable AppUserModelID +# 6. Apply AssignedAccess single-app kiosk config via the MDM WMI +# bridge; on Enterprise/Education, step down to Shell Launcher if +# the bridge rejects the XML +# 7. Configure automatic logon for the kiosk account +# +# Exit codes: +# 0 success (no reboot needed) +# 10 success, reboot required to activate +# 2 unsupported platform (not Windows / Home edition / not elevated) +# 3 prerequisite missing (browser, usher binary, install files) +# 4 kiosk account error +# 5 lockdown apply failed (AssignedAccess AND Shell Launcher) +# 6 invalid parameters +# +# Unattended example: +# powershell -ExecutionPolicy Bypass -File provision-kiosk.ps1 ` +# -KioskUser Kiosk -KioskPassword 'S3cure!' ` +# -HomeUrl https://checkin.example.org -Quiet -Restart +# +# Interactive (wizard prompts): +# powershell -ExecutionPolicy Bypass -File provision-kiosk.ps1 + +param( + [ValidateSet("auto", "librewolf", "firefox")] + [string]$Browser = "auto", # auto: step-down order LibreWolf -> Firefox + [string]$KioskUser = "VestibuleKiosk", + [string]$KioskPassword, # generated + printed if omitted + [string]$HomeUrl, # default about:blank + [string]$InstallRoot, # default: detected below + [switch]$Quiet, # no prompts (unattended) + [switch]$Restart, # auto-reboot when required + [switch]$Check, # validate only, change nothing + [switch]$ShellLauncher, # force Shell Launcher (skip bridge) + [switch]$NoAutoLogon, # skip automatic logon config + [switch]$InstallOverridesCfg # also deploy librewolf.overrides.cfg +) + +$ErrorActionPreference = "Stop" + +$AUMID = "Vestibule.Kiosk" +$ExtId = "vestibule@vestibule.kiosk" +$ProgramDataDir = Join-Path $env:ProgramData "Vestibule" +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +# ─── Small helpers ──────────────────────────────────────────────────── + +function Fail([int]$code, [string]$msg) { + Write-Host "" + Write-Host "ERROR: $msg" -ForegroundColor Red + Write-Host " exiting with code $code" + exit $code +} + +function Info($msg) { Write-Host $msg } +function Ok($msg) { Write-Host " [ok] $msg" -ForegroundColor Green } +function Step($msg) { Write-Host ""; Write-Host "==> $msg" -ForegroundColor Cyan } + +function Test-Elevated { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + return ([Security.Principal.WindowsPrincipal]$id).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Get-WindowsEdition { + return (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").EditionID +} + +# ─── Browser discovery (pipeline, first hit wins) ─────────────────── + +$librewolfSearchPaths = @( + "${env:ProgramFiles}\LibreWolf\librewolf.exe", + "${env:ProgramFiles(x86)}\LibreWolf\librewolf.exe", + "${env:LOCALAPPDATA}\Programs\LibreWolf\librewolf.exe" +) + +$firefoxSearchPaths = @( + "${env:ProgramFiles}\Mozilla Firefox\firefox.exe", + "${env:ProgramFiles}\Mozilla Firefox ESR\firefox.exe", + "${env:ProgramFiles(x86)}\Mozilla Firefox\firefox.exe", + "${env:ProgramFiles(x86)}\Mozilla Firefox ESR\firefox.exe", + "${env:LOCALAPPDATA}\Mozilla Firefox\firefox.exe" +) + +function Find-InPaths($paths, $exeName) { + $hit = $paths | Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($hit) { return $hit } + # Registry App Paths step-down: per-machine first, then per-user. + $regRoots = @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths", + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths") + return $regRoots | + ForEach-Object { (Get-ItemProperty (Join-Path $_ $exeName) -ErrorAction SilentlyContinue)."(default)" } | + Where-Object { $_ -and (Test-Path $_) } | + Select-Object -First 1 +} + +function Find-LibreWolf { Find-InPaths $librewolfSearchPaths "librewolf.exe" } + +function Find-Firefox { + # Firefox and Firefox ESR. Both read the same distribution/policies + # mechanism; ESR is recommended for kiosks (slower release cadence). + Find-InPaths $firefoxSearchPaths "firefox.exe" +} + +function Resolve-Browser { + # Returns @{ Kind = 'librewolf'|'firefox'; Exe = path } or $null. + # Explicit -Browser wins; auto steps down LibreWolf -> Firefox + # (privacy defaults + trademark-safe, then fully supported Firefox). + $lw = Find-LibreWolf + $ff = Find-Firefox + switch ($Browser) { + "librewolf" { + if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } + return $null + } + "firefox" { + if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } + return $null + } + default { + if ($lw) { return @{ Kind = "librewolf"; Exe = $lw } } + if ($ff) { return @{ Kind = "firefox"; Exe = $ff } } + return $null + } + } +} + +function New-RandomPassword { + # 20 chars, unambiguous classes — strong enough for a locked-down + # kiosk account that is never typed by a human. + $chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!#%+" + $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() + $bytes = New-Object byte[] 20 + $rng.GetBytes($bytes) + return (-join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })) +} + +# ─── Pre-flight checks ──────────────────────────────────────────────── + +Step "Pre-flight checks" + +if ($env:OS -ne "Windows_NT") { + Fail 2 "this script configures Windows kiosk lockdown; on Linux use scripts/provision-kiosk.sh" +} + +if (-not (Test-Elevated)) { + Fail 2 "must run elevated (right-click PowerShell -> Run as administrator)" +} + +$edition = Get-WindowsEdition +Info "Windows edition: $edition" +if ($edition -like "Core*") { + Fail 2 ("Windows Home (edition '$edition') does not support AssignedAccess or " + + "Shell Launcher. Use Windows Pro, Enterprise, or Education, or deploy " + + "the Linux (cage) variant.") +} +$shellLauncherCapable = ($edition -like "Enterprise*" -or + $edition -like "Education*" -or + $edition -like "IoTEnterprise*") + +# Install root: explicit param > already-staged install > repo checkout. +if (-not $InstallRoot) { + if (Test-Path (Join-Path $scriptDir "..\bin\usher.exe")) { + $InstallRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path + } else { + $InstallRoot = "C:\Program Files\Vestibule" + } +} +Info "install root: $InstallRoot" + +$browser = Resolve-Browser +if ($browser) { + Ok "browser ($($browser.Kind)): $($browser.Exe)" + $browserExe = $browser.Exe +} else { + $browserExe = $null + Write-Host " [!!] No LibreWolf or Firefox found in standard locations" -ForegroundColor Yellow +} + +$repoRoot = if (Test-Path (Join-Path $scriptDir "..\extension\manifest.json")) { + (Resolve-Path (Join-Path $scriptDir "..")).Path +} else { $null } + +$usherStaged = Test-Path (Join-Path $InstallRoot "bin\usher.exe") + +# ─── Interactive prompts (skipped with -Quiet) ──────────────────────── + +if (-not $Quiet -and -not $Check) { + if (-not $HomeUrl) { + $HomeUrl = Read-Host "Kiosk home URL [about:blank]" + if (-not $HomeUrl) { $HomeUrl = "about:blank" } + } + if (-not $KioskPassword) { + $KioskPassword = New-RandomPassword + Write-Host "" + Write-Host "Generated kiosk account password (needed for auto-logon; save it now):" -ForegroundColor Yellow + Write-Host " $KioskUser / $KioskPassword" -ForegroundColor Yellow + Write-Host "" + } +} + +if (-not $HomeUrl) { $HomeUrl = "about:blank" } + +if ($Check) { + Step "Check complete (no changes made)" + Info "edition : $edition ($( + if ($shellLauncherCapable) {'AssignedAccess + Shell Launcher step-down'} else {'AssignedAccess only'}))" + Info "browser : $(if ($browser) {"$($browser.Kind) ($($browser.Exe))"} else {'MISSING -> would exit 3'})" + Info "install root : $InstallRoot (usher staged: $usherStaged)" + Info "kiosk user : $KioskUser" + Info "home URL : $HomeUrl" + Info "auto-logon : $(if ($NoAutoLogon) {'disabled'} else {'enabled'})" + if (-not $browser) { Fail 3 "LibreWolf/Firefox not found" } + if (-not $usherStaged -and -not $repoRoot) { Fail 3 "no staged install and no repo checkout with a built usher" } + Ok "all prerequisites satisfied — re-run without -Check to provision" + exit 0 +} + +if (-not $KioskPassword) { + $KioskPassword = New-RandomPassword + Write-Host "Generated kiosk account password (save it now): $KioskUser / $KioskPassword" -ForegroundColor Yellow +} + +if (-not $browser) { Fail 3 "No supported browser found — install LibreWolf (librewolf.net) or Firefox (mozilla.org), then re-run" } + +# ─── 1. Stage install files ─────────────────────────────────────────── + +Step "Stage install files ($InstallRoot)" + +if (-not $usherStaged) { + if (-not $repoRoot) { + Fail 3 ("usher.exe not found at '$InstallRoot\bin'. Install via the " + + "Vestibule Setup .exe, or build from source: cd helper; cargo build --release") + } + New-Item -ItemType Directory -Force -Path "$InstallRoot\bin" | Out-Null + New-Item -ItemType Directory -Force -Path "$InstallRoot\scripts" | Out-Null + Copy-Item (Join-Path $repoRoot "helper\target\release\usher.exe") "$InstallRoot\bin\usher.exe" -Force + Copy-Item (Join-Path $repoRoot "scripts\*.ps1") "$InstallRoot\scripts\" -Force + Ok "staged usher.exe + scripts" +} +if (-not (Test-Path "$InstallRoot\extension\manifest.json") -and $repoRoot) { + New-Item -ItemType Directory -Force -Path "$InstallRoot\extension" | Out-Null + Copy-Item (Join-Path $repoRoot "extension\*") "$InstallRoot\extension\" -Recurse -Force + Ok "staged extension source" +} +if (-not (Test-Path "$InstallRoot\extension\manifest.json")) { + Fail 3 "extension files missing under '$InstallRoot\extension'" +} + +# ─── 2. Kiosk account ───────────────────────────────────────────────── + +Step "Kiosk account '$KioskUser'" + +$secure = ConvertTo-SecureString $KioskPassword -AsPlainText -Force +if (Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue) { + Set-LocalUser -Name $KioskUser -Password $secure -PasswordNeverExpires $true + Ok "account exists — password reset, never expires" +} else { + try { + New-LocalUser -Name $KioskUser -Password $secure ` + -AccountNeverExpires -PasswordNeverExpires ` + -PasswordChangeNotAllowed ` + -Description "Vestibule kiosk account (auto-provisioned)" | Out-Null + Ok "created" + } catch { + Fail 4 "could not create kiosk account: $($_.Exception.Message)" + } +} + +# ─── 3. XPI + policies.json ─────────────────────────────────────────── + +Step "Extension XPI + $($browser.Kind) policies" + +$xpiPath = Join-Path $InstallRoot "extension\vestibule.xpi" +$xpiTmp = Join-Path $env:TEMP "vestibule-xpi.zip" +if (Test-Path $xpiTmp) { Remove-Item $xpiTmp -Force } +Compress-Archive -Path (Join-Path $InstallRoot "extension\*") ` + -DestinationPath $xpiTmp -Force +Move-Item $xpiTmp $xpiPath -Force +Ok "built $(Split-Path -Leaf $xpiPath)" + +# Distribution dir sits next to the browser executable (LibreWolf and +# Firefox share the mechanism). +$browserDir = Split-Path -Parent $browserExe +$distDir = Join-Path $browserDir "distribution" +New-Item -ItemType Directory -Force -Path $distDir | Out-Null + +$policiesPath = Join-Path $distDir "policies.json" +# The shipped distribution/policies.json is backed up once, then ours +# is deep-merged on top so the browser's own hardening survives. +if (Test-Path $policiesPath) { + $bak = "$policiesPath.vestibule-bak" + if (-not (Test-Path $bak)) { Copy-Item $policiesPath $bak -Force } + Ok "backed up existing policies.json -> vestibule-bak" +} + +# ConvertFrom-Json in Windows PowerShell 5.1 yields PSCustomObjects and +# has no -AsHashtable; walk the tree into real hashtables so the deep +# merge below can mutate in place. +function ConvertTo-HashtableDeep($node) { + if ($node -is [System.Management.Automation.PSCustomObject]) { + $h = @{} + foreach ($p in $node.PSObject.Properties) { $h[$p.Name] = ConvertTo-HashtableDeep $p.Value } + return $h + } + if ($node -is [System.Collections.IEnumerable] -and $node -isnot [string]) { + $arr = @() + foreach ($item in $node) { $arr += ,(ConvertTo-HashtableDeep $item) } + return $arr + } + return $node +} + +function Merge-Policy([hashtable]$base, [hashtable]$overlay) { + foreach ($k in $overlay.Keys) { + if ($base.ContainsKey($k) -and $base[$k] -is [hashtable] -and $overlay[$k] -is [hashtable]) { + Merge-Policy $base[$k] $overlay[$k] + } else { + $base[$k] = $overlay[$k] + } + } +} + +$policies = @{ policies = @{} } +if (Test-Path $policiesPath) { + try { + $existing = ConvertTo-HashtableDeep (Get-Content $policiesPath -Raw | ConvertFrom-Json) + if ($existing -is [hashtable] -and $existing.Count -gt 0) { $policies = $existing } + } catch { $policies = @{ policies = @{} } } +} +$canonical = ConvertTo-HashtableDeep (Get-Content (Join-Path $scriptDir "..\config\policies.json") -Raw | ConvertFrom-Json) +Merge-Policy $policies $canonical + +# Policy-install the extension: force_installed survives the "*" blocked +# wildcard in ExtensionSettings and re-installs itself on every startup. +$xpiUrl = ([uri]$xpiPath).AbsoluteUri +$policies.policies.ExtensionSettings = @{ + "*" = @{ + blocked_install_message = "Extensions are not allowed on this kiosk." + install_sources = @() + installation_mode = "blocked" + } + $ExtId = @{ + installation_mode = "force_installed" + install_url = $xpiUrl + } +} + +# WriteAllText = UTF-8 without BOM. Set-Content -Encoding UTF8 in +# Windows PowerShell 5.1 emits a BOM, which Gecko's policy loader is not +# guaranteed to tolerate. +[System.IO.File]::WriteAllText($policiesPath, ($policies | ConvertTo-Json -Depth 10)) +Ok "deployed policies.json (extension force-installed from $xpiUrl)" + +if ($InstallOverridesCfg) { + # librewolf.overrides.cfg is a LibreWolf-specific autoconfig file; it + # has no effect on Firefox (Firefox ignores it safely). + if ($browser.Kind -eq "librewolf") { + $src = Join-Path $scriptDir "..\config\librewolf.overrides.cfg" + if (Test-Path $src) { + Copy-Item $src (Join-Path $browserDir "librewolf.overrides.cfg") -Force + Ok "deployed librewolf.overrides.cfg (SSO/telehealth compat)" + } + } else { + Info "skipped librewolf.overrides.cfg (LibreWolf-only; browser is $($browser.Kind))" + } +} + +# ─── 4. ProgramData config ──────────────────────────────────────────── + +Step "Deployment config" +New-Item -ItemType Directory -Force -Path $ProgramDataDir | Out-Null +@" +VESTIBULE_HOME_URL=$HomeUrl +VESTIBULE_BROWSER=$($browser.Kind) +"@ | Set-Content (Join-Path $ProgramDataDir "kiosk.env") -Encoding UTF8 +Ok "kiosk.env written (home URL: $HomeUrl, browser: $($browser.Kind))" + +# ─── 5. Start Menu shortcut with AUMID ──────────────────────────────── + +Step "Kiosk shortcut (AUMID: $AUMID)" + +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; + +// Sets the System.AppUserModel.ID property on a .lnk file. AssignedAccess +// single-app kiosk mode launches desktop apps by AUMID, so the shortcut +// must carry a stable explicit AUMID. +public static class ShortcutAumid { + [ComImport, Guid("00021401-0000-0000-C000-000000000046")] + private class ShellLinkCoClass {} + + [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), + Guid("0000010B-0000-0000-C000-000000000046")] + private interface IPersistFile { + void GetClassID(out Guid pClassID); + [PreserveSig] int IsDirty(); + void Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, uint dwMode); + void Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, + [MarshalAs(UnmanagedType.Bool)] bool fRemember); + void SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName); + void GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string ppszFileName); + } + + [ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IPropertyStore { + int GetCount(out uint cProps); + int GetAt(uint iProp, out PropertyKey pkey); + int GetValue(ref PropertyKey key, out PropVariant pv); + int SetValue(ref PropertyKey key, ref PropVariant pv); + int Commit(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct PropertyKey { public Guid fmtid; public uint pid; } + + [StructLayout(LayoutKind.Explicit)] + private struct PropVariant { + [FieldOffset(0)] public ushort vt; + [FieldOffset(8)] public IntPtr pointerValue; + } + + [DllImport("ole32.dll")] + private static extern int CoInitialize(IntPtr reserved); + [DllImport("ole32.dll")] + private static extern void CoUninitialize(); + [DllImport("ole32.dll")] + private static extern int CoCreateInstance(ref Guid clsid, IntPtr outer, + uint context, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object obj); + [DllImport("ole32.dll")] + private static extern IntPtr CoTaskMemAlloc(uint bytes); + [DllImport("ole32.dll")] + private static extern void CoTaskMemFree(IntPtr p); + + private const ushort VT_LPWSTR = 31; + private const uint STGM_READWRITE = 2; + private static readonly Guid ClsidShellLink = + new Guid("00021401-0000-0000-C000-000000000046"); + private static readonly Guid IidPersistFile = + new Guid("0000010B-0000-0000-C000-000000000046"); + private static readonly Guid IidPropertyStore = + new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"); + private static readonly PropertyKey PkeyAppUserModelId = + new PropertyKey { + fmtid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), + pid = 5 + }; + + public static int SetLnkAumid(string lnkPath, string aumid) { + int initHr = CoInitialize(IntPtr.Zero); // S_OK (0) or S_FALSE (1) + try { + Guid clsid = ClsidShellLink, iidPf = IidPersistFile; + object pfObj; + int hr = CoCreateInstance(ref clsid, IntPtr.Zero, 1 /*CLSCTX_INPROC_SERVER*/, + ref iidPf, out pfObj); + if (hr != 0) return hr; + IPersistFile persist = (IPersistFile)pfObj; + persist.Load(lnkPath, STGM_READWRITE); + + IPropertyStore store = (IPropertyStore)pfObj; + PropVariant pv = new PropVariant(); + pv.vt = VT_LPWSTR; + pv.pointerValue = Marshal.StringToCoTaskMemUni(aumid); + try { + hr = store.SetValue(ref PkeyAppUserModelId, ref pv); + if (hr != 0) return hr; + hr = store.Commit(); + if (hr != 0) return hr; + } finally { + CoTaskMemFree(pv.pointerValue); + } + persist.Save(lnkPath, true); + return 0; + } finally { + if (initHr == 0) CoUninitialize(); + } + } +} +"@ + +$startMenuDir = Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs" +$lnkPath = Join-Path $startMenuDir "Vestibule Kiosk.lnk" +$launcher = Join-Path $InstallRoot "scripts\kiosk-launch.ps1" + +$ws = New-Object -ComObject WScript.Shell +$sc = $ws.CreateShortcut($lnkPath) +$sc.TargetPath = "powershell.exe" +$sc.Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$launcher`"" +$sc.WorkingDirectory = $InstallRoot +$sc.IconLocation = "$browserExe,0" +$sc.Description = "Vestibule kiosk (AssignedAccess shell)" +$sc.Save() + +$hr = [ShortcutAumid]::SetLnkAumid($lnkPath, $AUMID) +if ($hr -ne 0) { Fail 5 "could not set AUMID on shortcut (HRESULT 0x$($hr.ToString('X8')))" } +Ok "shortcut: $lnkPath" + +# Verify the shell can resolve the AUMID (Get-StartApps indexes the +# Start Menu; retry briefly because indexing is asynchronous). +$aumidFound = $false +for ($i = 0; $i -lt 3 -and -not $aumidFound; $i++) { + Start-Sleep -Seconds 2 + $aumidFound = [bool](Get-StartApps | Where-Object { $_.AppID -eq $AUMID }) +} +if (-not $aumidFound) { + Fail 5 "AUMID '$AUMID' not visible to Get-StartApps — AssignedAccess would reject it. Reboot and re-run." +} +Ok "AUMID resolves via Get-StartApps" + +# ─── 6. Lockdown: AssignedAccess (Shell Launcher step-down) ──────── + +$shellLauncherArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$launcher`"" +$lockdownApplied = $false +$rebootNeeded = $false + +function Test-ShellLauncherClass { + return [bool](Get-CimClass -Namespace "root\standardcimv2\embedded" ` + -ClassName "WESL_UserSetting" -ErrorAction SilentlyContinue) +} + +function Enable-ShellLauncherFeature { + try { + Enable-WindowsOptionalFeature -Online ` + -FeatureName "Client-DeviceLockdown" -All -NoRestart -ErrorAction Stop | Out-Null + return $true + } catch { + try { + Enable-WindowsOptionalFeature -Online ` + -FeatureName "Client-EmbeddedShellLauncher" -All -NoRestart -ErrorAction Stop | Out-Null + return $true + } catch { return $false } + } +} + +function Invoke-ShellLauncher { + if (-not (Test-ShellLauncherClass)) { + if (-not (Enable-ShellLauncherFeature)) { return $false } + if (-not (Test-ShellLauncherClass)) { + # Feature enabled but class appears after reboot. + $script:rebootNeeded = $true + return $false + } + } + $wesl = [wmiclass]"\\.\root\standardcimv2\embedded:WESL_UserSetting" + # SetCustomShell's parameter list has drifted across Windows builds + # (4-arg v1 and 5-arg variants with a custom return-code map). Try each + # known shape; the first that returns 0 wins. + $r = $null + foreach ($call in @( + { $wesl.SetCustomShell($KioskUser, "powershell.exe", $shellLauncherArgs, $null, 0) }, + { $wesl.SetCustomShell($KioskUser, "powershell.exe", $shellLauncherArgs, 0) }, + { $wesl.SetCustomShell($KioskUser, "powershell.exe", $shellLauncherArgs) } + )) { + try { + $r = & $call + if ($r.ReturnValue -eq 0) { break } + } catch { $r = $null } + } + if ($r -and $r.ReturnValue -eq 0) { + $script:lockdownApplied = $true + $script:shellLauncherMode = $true + Ok "Shell Launcher custom shell set for '$KioskUser'" + return $true + } + Write-Host " [!!] SetCustomShell failed (last result: $(if ($r) {$r.ReturnValue} else {'exception'}))" -ForegroundColor Yellow + return $false +} + +function Invoke-AssignedAccess { + $profileId = "{$([guid]::NewGuid().ToString())}" + $xml = @" + + + + + + + + + + $KioskUser + + + + +"@ + try { + $instances = @(Get-CimInstance -Namespace "root\cimv2\mdm\dmmap" ` + -ClassName "MDM_AssignedAccess" -ErrorAction Stop) + } catch { + Write-Host " [!!] MDM WMI bridge unavailable: $($_.Exception.Message)" -ForegroundColor Yellow + return $false + } + foreach ($inst in $instances) { + try { + $res = Invoke-CimMethod -InputObject $inst ` + -MethodName "SetSingleAppKiosk" ` + -Arguments @{ AssignedAccessConfiguration = $xml } -ErrorAction Stop + if ($res.ReturnValue -eq 0) { + $script:lockdownApplied = $true + $script:shellLauncherMode = $false + Ok "AssignedAccess single-app kiosk configured via MDM bridge" + return $true + } + Write-Host " [!!] SetSingleAppKiosk returned $($res.ReturnValue) on one enrollment" -ForegroundColor Yellow + } catch { + Write-Host " [!!] bridge call failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + return $false +} + +Step "OS-level lockdown" +if ($ShellLauncher) { + Info "mode: Shell Launcher (forced by parameter)" + [void](Invoke-ShellLauncher) +} else { + Info "mode: AssignedAccess via MDM WMI bridge" + if (-not (Invoke-AssignedAccess)) { + if ($shellLauncherCapable) { + Info "bridge failed — stepping down to Shell Launcher (supported on $edition)" + [void](Invoke-ShellLauncher) + } + } +} +if (-not $lockdownApplied) { + Fail 5 ("could not apply AssignedAccess or Shell Launcher. Apply manually: " + + "Settings > Accounts > Other users > Set up kiosk, or use -ShellLauncher on Enterprise/Education.") +} + +# ─── 7. Automatic logon ─────────────────────────────────────────────── + +Step "Automatic logon" +if ($NoAutoLogon) { + Info "skipped (-NoAutoLogon) — kiosk starts after manual logon as '$KioskUser'" +} else { + $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" + Set-ItemProperty $wl -Name "AutoAdminLogon" -Value "1" -Type String + Set-ItemProperty $wl -Name "DefaultUserName" -Value $KioskUser -Type String + Set-ItemProperty $wl -Name "DefaultDomainName" -Value $env:COMPUTERNAME -Type String + # NOTE: DefaultPassword is stored in plaintext in the registry. On a + # locked-down kiosk appliance this is an accepted trade-off (documented + # in DEPLOYMENT.md); hold Shift during boot to bypass auto-logon. + Set-ItemProperty $wl -Name "DefaultPassword" -Value $KioskPassword -Type String + Ok "auto-logon configured for '$KioskUser' (Shift at logon bypasses it)" +} + +# ─── Summary ────────────────────────────────────────────────────────── + +Step "Provisioning complete" +Info "kiosk user : $KioskUser" +Info "home URL : $HomeUrl" +Info "lockdown : $(if ($shellLauncherMode) {'Shell Launcher'} else {'AssignedAccess'})" +Info "browser : $($browser.Kind) ($($browser.Exe))" +Info "policies : $policiesPath" + +$rebootNeeded = $rebootNeeded -or (-not $NoAutoLogon) +if ($rebootNeeded) { + Info "reboot required to activate the kiosk." + if ($Restart) { + Info "restarting in 10 seconds (-Restart)..." + shutdown.exe /r /t 10 /c "Vestibule kiosk activation" + exit 0 + } + exit 10 +} +Ok "kiosk will start the next time '$KioskUser' logs on" +exit 0 diff --git a/scripts/provision-kiosk.sh b/scripts/provision-kiosk.sh new file mode 100755 index 0000000..3331bdc --- /dev/null +++ b/scripts/provision-kiosk.sh @@ -0,0 +1,761 @@ +#!/bin/sh +# provision-kiosk.sh — Linux kiosk provisioning wizard +# +# Turns a systemd Linux machine into a Vestibule kiosk without manual OS +# configuration: +# +# 1. Verify prerequisites: systemd, cage, a Gecko browser (LibreWolf +# or Firefox — native package, Flatpak, or snap), usher, python3 +# 2. Create the dedicated kiosk user (password stays locked) +# 3. Stage files under /opt/vestibule (usher, extension XPI, configs) +# 4. Install /usr/local/bin/vestibule-kiosk-launch +# 5. Write /etc/vestibule/kiosk.env (home URL, browser + flavor) +# 6. Register the usher Native Messaging host for the kiosk user +# 7. Deploy a merged policies.json (policy force-installs the +# extension) where the chosen browser reads it +# 8. Generate + enable vestibule-kiosk.service (cage on a VT) +# +# The unit IS the graphical session: no display manager, no autologin +# config. cage takes the VT and runs the browser as the kiosk user, +# restarted by systemd if anything dies. +# +# Browser support (auto-detect picks the first available, LibreWolf +# preferred): librewolf native, librewolf flatpak, firefox native, +# firefox flatpak, firefox snap. +# +# Exit codes: +# 0 success +# 2 not root / not supported (no systemd) +# 3 prerequisite missing (cage, browser, usher, python3, dbus) +# 4 kiosk user error +# 5 unit install / enable failure +# 6 invalid parameters +# +# Unattended examples: +# sudo ./provision-kiosk.sh --home-url https://checkin.example.org \ +# --kiosk-user kiosk --librewolf flatpak --yes --start +# sudo ./provision-kiosk.sh --firefox native \ +# --home-url https://lobby.example.org --yes +# +# Interactive: +# sudo ./provision-kiosk.sh +# +# POSIX sh — no bashisms. Runs on any minimal Linux base. + +set -eu + +HOST_NAME="com.vestibule.usher" +EXT_ID="vestibule@vestibule.kiosk" +LW_FLATPAK_APP="io.gitlab.librewolf-community" +FF_FLATPAK_APP="org.mozilla.firefox" +OPT_ROOT="/opt/vestibule" +ENV_DIR="/etc/vestibule" +UNIT_SRC_DIR=$(cd "$(dirname "$0")" && pwd) +PROJECT_ROOT=$(cd "${UNIT_SRC_DIR}/.." && pwd) +UNIT_DST="/etc/systemd/system/vestibule-kiosk.service" +LAUNCH_DST="/usr/local/bin/vestibule-kiosk-launch" +USHER_DST="/usr/local/bin/usher" + +KIOSK_USER="vestibule-kiosk" +TTY_NUM="2" +BROWSER_REQ="auto" +FLAVOR_REQ="auto" +BROWSER_FLAGS_SEEN="" +HOME_URL="about:blank" +USHER_SRC="" +QUIET=0 +CHECK=0 +ASSUME_YES=0 +START_NOW=0 + +usage() { + sed -n '2,48p' "$0" | sed 's/^# \{0,1\}//' + cat <<'EOF' + +Options: + --home-url URL Kiosk home URL (default: about:blank) + --kiosk-user NAME Kiosk account name (default: vestibule-kiosk) + --tty N VT for the cage session, 1-12 (default: 2) + --librewolf FLAVOR Use LibreWolf: native | flatpak | auto + --firefox FLAVOR Use Firefox: native | flatpak | snap | auto + (default: auto-detect, LibreWolf preferred; + --librewolf and --firefox are mutually exclusive) + --usher-bin PATH Explicit usher binary to install + --start Start the kiosk session immediately + --yes Skip the confirmation prompt (unattended) + --quiet Minimal output + --check Validate prerequisites only; change nothing + -h, --help This text + +Firefox notes: Firefox ESR is recommended for kiosk stability. The +extension, usher, and lockdown policies are identical for both browsers. +Vestibule configures an existing Firefox install; it never downloads or +redistributes Firefox (Mozilla trademark policy). +EOF +} + +die() { + code="$1"; msg="$2" + echo "" + echo "ERROR: ${msg}" >&2 + echo " exiting with code ${code}" >&2 + exit "${code}" +} +info() { echo "$1"; } +ok() { echo " [ok] $1"; } +step() { echo ""; echo "==> $1"; } + +# First executable in the argument list wins; empty arguments are skipped. +# Single home for every ordered-path probe in this script. +first_executable() { + for cand in "$@"; do + if [ -n "${cand}" ] && [ -x "${cand}" ]; then + printf '%s\n' "${cand}" + return 0 + fi + done + return 1 +} + +# ─── Argument parsing ───────────────────────────────────────────────── + +while [ $# -gt 0 ]; do + case "$1" in + --home-url) HOME_URL="$2"; shift 2 ;; + --kiosk-user) KIOSK_USER="$2"; shift 2 ;; + --tty) TTY_NUM="$2"; shift 2 ;; + --librewolf) BROWSER_REQ="librewolf"; FLAVOR_REQ="$2" + BROWSER_FLAGS_SEEN="${BROWSER_FLAGS_SEEN} librewolf"; shift 2 ;; + --firefox) BROWSER_REQ="firefox"; FLAVOR_REQ="$2" + BROWSER_FLAGS_SEEN="${BROWSER_FLAGS_SEEN} firefox"; shift 2 ;; + --usher-bin) USHER_SRC="$2"; shift 2 ;; + --start) START_NOW=1; shift ;; + --yes|-y) ASSUME_YES=1; shift ;; + --quiet) QUIET=1; shift ;; + --check) CHECK=1; shift ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; die 6 "unknown option: $1" ;; + esac +done + +if [ "$(printf '%s' "${BROWSER_FLAGS_SEEN}" | wc -w)" -gt 1 ]; then + die 6 "--librewolf and --firefox are mutually exclusive (saw:${BROWSER_FLAGS_SEEN})" +fi + +if [ "${BROWSER_REQ}" = "librewolf" ]; then + case "${FLAVOR_REQ}" in + native|flatpak|auto) ;; + *) die 6 "--librewolf must be native, flatpak, or auto (got: ${FLAVOR_REQ})" ;; + esac +fi +if [ "${BROWSER_REQ}" = "firefox" ]; then + case "${FLAVOR_REQ}" in + native|flatpak|snap|auto) ;; + *) die 6 "--firefox must be native, flatpak, snap, or auto (got: ${FLAVOR_REQ})" ;; + esac +fi + +case "${TTY_NUM}" in + ''|*[!0-9]*) die 6 "--tty must be a number (got: ${TTY_NUM})" ;; +esac +[ "${TTY_NUM}" -ge 1 ] && [ "${TTY_NUM}" -le 12 ] || die 6 "--tty must be 1-12" + +case "${HOME_URL}" in + http://*|https://*|about:*) ;; + *) die 6 "--home-url must start with http://, https://, or about: (got: ${HOME_URL})" ;; +esac + +# ─── Detection ──────────────────────────────────────────────────────── + +step "Pre-flight detection" + +if [ ! -d /run/systemd/system ]; then + die 2 "systemd is not the running init system — this provisioning path targets systemd" +fi +ok "systemd" + +# Distro + package manager (informational: we print install commands, +# we never auto-install — operator stays in control of the base image). +# NOTE: os-release is sourced in a SUBSHELL — its standard fields +# (HOME_URL, SUPPORT_URL, ...) would otherwise clobber our variables. +DISTRO_ID="unknown" +PKG_MGR="none" +if [ -r /etc/os-release ]; then + DISTRO_ID=$( + # shellcheck disable=SC1091 + . /etc/os-release + printf '%s' "${ID:-unknown}" + ) +fi +for pm in apt-get dnf pacman zypper; do + if command -v "${pm}" >/dev/null 2>&1; then PKG_MGR="${pm}"; break; fi +done +[ "${QUIET}" -eq 1 ] || info "distro: ${DISTRO_ID} (pkg mgr: ${PKG_MGR})" + +CAGE_BIN="" +if command -v cage >/dev/null 2>&1; then + CAGE_BIN=$(command -v cage) +elif [ -x /usr/bin/cage ]; then + CAGE_BIN="/usr/bin/cage" +fi +if [ -n "${CAGE_BIN}" ]; then + ok "cage: ${CAGE_BIN}" +fi + +# ── LibreWolf detection ────────────────────────────────────────────── + +# Native LibreWolf: binary + its distribution directory. +LW_NATIVE_BIN=$(first_executable \ + "$(command -v librewolf 2>/dev/null || true)" \ + /usr/lib/librewolf/librewolf \ + /usr/local/lib/librewolf/librewolf \ + /opt/librewolf/librewolf \ + /usr/local/bin/librewolf || true) +LW_DIST="" +if [ -n "${LW_NATIVE_BIN}" ]; then + LW_REAL=$(readlink -f "${LW_NATIVE_BIN}" 2>/dev/null || echo "${LW_NATIVE_BIN}") + LW_REAL_DIR=$(dirname "${LW_REAL}") + # A real LibreWolf install dir carries application.ini + browser/; + # /usr/bin/librewolf may be a wrapper script or symlink — don't trust + # its directory unless those markers are there. + if [ -d "${LW_REAL_DIR}/distribution" ] || [ -f "${LW_REAL_DIR}/application.ini" ] \ + || [ -d "${LW_REAL_DIR}/browser" ]; then + LW_DIST="${LW_REAL_DIR}/distribution" + else + # Wrapper-script install: pick the first candidate whose parent dir + # exists — provisioning creates distribution/ inside it. + for dist in \ + /usr/lib/librewolf/distribution \ + /usr/share/librewolf/distribution \ + /opt/librewolf/distribution; do + if [ -d "$(dirname "${dist}")" ]; then LW_DIST="${dist}"; break; fi + done + fi + ok "librewolf (native): ${LW_NATIVE_BIN} (policies: ${LW_DIST})" +fi + +# Flatpak LibreWolf: system installation only — a per-user install would +# be invisible to the kiosk account. +LW_FLATPAK=0 +if command -v flatpak >/dev/null 2>&1; then + if flatpak info --system "${LW_FLATPAK_APP}" >/dev/null 2>&1; then + LW_FLATPAK=1 + ok "librewolf (flatpak, system): ${LW_FLATPAK_APP}" + elif flatpak info --user "${LW_FLATPAK_APP}" >/dev/null 2>&1; then + info " [!!] ${LW_FLATPAK_APP} is installed per-USER — the kiosk account cannot see it." + info " reinstall system-wide: flatpak install --system flathub ${LW_FLATPAK_APP}" + fi +fi + +# ── Firefox detection ──────────────────────────────────────────────── + +# Native Firefox. Canonical distro paths first (Debian/Ubuntu: +# /usr/lib/firefox, Fedora: /usr/lib64/firefox, Arch: /usr/bin symlink), +# then tarball-style installs (/opt, /usr/local). command -v results +# that resolve into /snap or a flatpak export are routed to their own +# flavors below. +FF_NATIVE_BIN="" +FF_NATIVE_REAL="" +FF_DISTRO=0 +FF_DIST="" +for cand in \ + /usr/lib/firefox/firefox \ + /usr/lib64/firefox/firefox \ + /usr/bin/firefox \ + /opt/firefox/firefox \ + /usr/local/firefox/firefox \ + /usr/local/bin/firefox \ + "$(command -v firefox 2>/dev/null || true)"; do + if [ -z "${cand}" ] || [ ! -x "${cand}" ]; then + continue + fi + real=$(readlink -f "${cand}" 2>/dev/null || echo "${cand}") + case "${real}" in + /snap/*) continue ;; # snap firefox, handled below + */flatpak/*|*/.local/share/flatpak/*) continue ;; # flatpak export + esac + case "${cand}" in + /usr/lib/firefox/*|/usr/lib64/firefox/*|/usr/bin/*|/usr/share/*) + FF_DISTRO=1 ;; + *) FF_DISTRO=0 ;; + esac + FF_NATIVE_BIN="${cand}" + FF_NATIVE_REAL="${real}" + break +done + +if [ -n "${FF_NATIVE_BIN}" ]; then + if [ "${FF_DISTRO}" -eq 1 ]; then + # Distro package: /etc/firefox/policies is the canonical, + # update-safe location (Mozilla's documented Linux path). + FF_DIST="/etc/firefox/policies" + else + # Tarball-style install: policies live beside the binary, in the + # distribution/ directory — same mechanism as Windows. + FF_DIST="$(dirname "${FF_NATIVE_REAL}")/distribution" + fi + ok "firefox (native): ${FF_NATIVE_BIN} (policies: ${FF_DIST})" +fi + +# Firefox snap (Ubuntu's default): the sandbox home is +# ~/snap/firefox/common — policies and NM manifests for the kiosk user +# go there. Detected via snap list or a /usr/bin/firefox wrapper that +# resolves into /snap. +FF_SNAP=0 +if command -v snap >/dev/null 2>&1; then + if snap list firefox >/dev/null 2>&1; then + FF_SNAP=1 + fi +fi +if [ "${FF_SNAP}" -eq 0 ]; then + ff_probe="" + if [ -e /usr/bin/firefox ]; then + ff_probe=$(readlink -f /usr/bin/firefox 2>/dev/null || true) + elif [ -e /snap/bin/firefox ]; then + ff_probe=$(readlink -f /snap/bin/firefox 2>/dev/null || true) + fi + case "${ff_probe}" in + /snap/*) FF_SNAP=1 ;; + esac +fi +if [ "${FF_SNAP}" -eq 1 ]; then + ok "firefox (snap): /snap/bin/firefox (policies: ~kiosk/snap/firefox/common/.mozilla/policies)" +fi + +# Firefox Flatpak: system installation only. +FF_FLATPAK=0 +if command -v flatpak >/dev/null 2>&1; then + if flatpak info --system "${FF_FLATPAK_APP}" >/dev/null 2>&1; then + FF_FLATPAK=1 + ok "firefox (flatpak, system): ${FF_FLATPAK_APP}" + elif flatpak info --user "${FF_FLATPAK_APP}" >/dev/null 2>&1; then + info " [!!] ${FF_FLATPAK_APP} is installed per-USER — the kiosk account cannot see it." + info " reinstall system-wide: flatpak install --system flathub ${FF_FLATPAK_APP}" + fi +fi + +# ── Resolve browser + flavor ───────────────────────────────────────── +# +# Step-down resolution: each probe names one flavor; the first available +# wins. An explicit --librewolf/--firefox flavor request pins the probe to +# that flavor alone; "auto" walks the whole ladder. + +BROWSER="" +FLAVOR="" + +flavor_available() { + # Lookup table: browser:flavor -> availability test. + case "$1:$2" in + librewolf:native) [ -n "${LW_NATIVE_BIN}" ] ;; + librewolf:flatpak) [ "${LW_FLATPAK}" -eq 1 ] ;; + firefox:native) [ -n "${FF_NATIVE_BIN}" ] ;; + firefox:flatpak) [ "${FF_FLATPAK}" -eq 1 ] ;; + firefox:snap) [ "${FF_SNAP}" -eq 1 ] ;; + *) return 1 ;; + esac +} + +resolve_flavor() { + # $1 = browser, $2 = flavor + if [ "${FLAVOR_REQ}" = "auto" ] || [ "${FLAVOR_REQ}" = "$2" ]; then + if flavor_available "$1" "$2"; then + BROWSER="$1"; FLAVOR="$2"; return 0 + fi + fi + return 1 +} + +resolve_librewolf() { + resolve_flavor librewolf native && return 0 + resolve_flavor librewolf flatpak && return 0 + return 1 +} + +resolve_firefox() { + resolve_flavor firefox native && return 0 + resolve_flavor firefox flatpak && return 0 + resolve_flavor firefox snap && return 0 + return 1 +} + +if [ "${BROWSER_REQ}" = "librewolf" ]; then + resolve_librewolf +elif [ "${BROWSER_REQ}" = "firefox" ]; then + resolve_firefox +else + # Auto step-down: LibreWolf (privacy defaults + trademark-safe), then + # Firefox (fully supported alternative). + FLAVOR_REQ="auto" + resolve_librewolf || resolve_firefox +fi + +BROWSER_MISSING="" +if [ -z "${BROWSER}" ]; then + if [ "${BROWSER_REQ}" = "librewolf" ]; then + BROWSER_MISSING="librewolf" + elif [ "${BROWSER_REQ}" = "firefox" ]; then + BROWSER_MISSING="firefox" + else + BROWSER_MISSING="browser (librewolf or firefox)" + fi +fi + +# usher source: explicit flag > existing install > repo build > staged. +if [ -z "${USHER_SRC}" ]; then + if [ -x "${USHER_DST}" ]; then + USHER_SRC="${USHER_DST}" + elif [ -x "${PROJECT_ROOT}/helper/target/release/usher" ]; then + USHER_SRC="${PROJECT_ROOT}/helper/target/release/usher" + elif [ -x "${OPT_ROOT}/bin/usher" ]; then + USHER_SRC="${OPT_ROOT}/bin/usher" + fi +fi + +PYTHON_BIN="" +if command -v python3 >/dev/null 2>&1; then + PYTHON_BIN=$(command -v python3) +fi + +DBUS_RUN="" +if command -v dbus-run-session >/dev/null 2>&1; then + DBUS_RUN=$(command -v dbus-run-session) +fi + +# ─── Missing-prerequisite report ────────────────────────────────────── + +MISSING="" +if [ -z "${CAGE_BIN}" ]; then MISSING="${MISSING} cage"; fi +if [ -n "${BROWSER_MISSING}" ]; then MISSING="${MISSING} ${BROWSER_MISSING}"; fi +if [ -z "${USHER_SRC}" ]; then MISSING="${MISSING} usher"; fi +if [ -z "${PYTHON_BIN}" ]; then MISSING="${MISSING} python3"; fi +if [ -z "${DBUS_RUN}" ]; then MISSING="${MISSING} dbus"; fi + +if [ -n "${MISSING}" ]; then + echo "" + info "Missing prerequisites:${MISSING}" + info "Install them, then re-run this script. Per-distro commands:" + echo "" + case "${PKG_MGR}" in + apt-get) + echo " sudo apt-get install -y cage dbus python3 firefox-esr" + echo " # LibreWolf: deb repo — https://librewolf.net/installation/linux/" + echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}" + echo " # or Firefox Flatpak: flatpak install --system flathub ${FF_FLATPAK_APP}" ;; + dnf) + echo " sudo dnf install -y cage dbus python3 firefox" + echo " # LibreWolf: https://librewolf.net/installation/linux/" + echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}" ;; + pacman) + echo " sudo pacman -S --needed cage dbus python3 firefox" + echo " # LibreWolf: AUR (librewolf / librewolf-bin)" + echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}" ;; + zypper) + echo " sudo zypper install cage dbus python3 MozillaFirefox" + echo " # LibreWolf: https://librewolf.net/installation/linux/" + echo " # or Flatpak: flatpak install --system flathub ${LW_FLATPAK_APP}" ;; + *) + echo " # Install cage, dbus, python3, and a browser (firefox or" + echo " # librewolf) from your distribution." + echo " # cage from source: https://github.com/cage-kiosk/cage" + echo " # Flatpak browsers: flatpak install --system flathub ${FF_FLATPAK_APP}" ;; + esac + echo "" + echo " usher: build from source — cd helper && cargo build --release" + die 3 "prerequisites not met" +fi +ok "usher: ${USHER_SRC}" +ok "python3: ${PYTHON_BIN}" +ok "browser: ${BROWSER} (${FLAVOR})" + +# ─── Policy destination preview (needs KIOSK_HOME for sandboxed flavors) + +policy_dir_for_kiosk_home() { + # $1 = kiosk home + case "${BROWSER}:${FLAVOR}" in + librewolf:native) printf '%s' "${LW_DIST}" ;; + librewolf:flatpak) printf '%s' "/var/lib/flatpak/app/${LW_FLATPAK_APP}/current/active/files/librewolf/distribution" ;; + firefox:native) printf '%s' "${FF_DIST}" ;; + firefox:flatpak) printf '%s' "$1/.var/app/${FF_FLATPAK_APP}/.mozilla/policies" ;; + firefox:snap) printf '%s' "$1/snap/firefox/common/.mozilla/policies" ;; + esac +} + +if [ "${CHECK}" -eq 1 ]; then + step "Check complete (no changes made)" + info "kiosk user : ${KIOSK_USER} (created if absent)" + info "tty : /dev/tty${TTY_NUM}" + info "browser : ${BROWSER} (${FLAVOR})" + info "home URL : ${HOME_URL}" + info "policies dir : $(policy_dir_for_kiosk_home "/home/${KIOSK_USER}")" + info "opt root : ${OPT_ROOT}" + ok "all prerequisites satisfied — re-run without --check to provision" + exit 0 +fi + +[ "$(id -u)" -eq 0 ] || die 2 "must run as root (sudo)" + +# ─── Confirmation ───────────────────────────────────────────────────── + +if [ "${ASSUME_YES}" -eq 0 ] && [ "${QUIET}" -eq 0 ]; then + echo "" + printf "Provision kiosk (user=%s, tty=%s, browser=%s/%s, url=%s)? [y/N] " \ + "${KIOSK_USER}" "${TTY_NUM}" "${BROWSER}" "${FLAVOR}" "${HOME_URL}" + read -r answer + case "${answer}" in + y|Y|yes|YES) ;; + *) info "aborted"; exit 0 ;; + esac +fi + +# ─── 1. Kiosk user ──────────────────────────────────────────────────── + +step "Kiosk user '${KIOSK_USER}'" +KIOSK_HOME="" +if id -u "${KIOSK_USER}" >/dev/null 2>&1; then + KIOSK_HOME=$(getent passwd "${KIOSK_USER}" | cut -d: -f6) + ok "exists (home: ${KIOSK_HOME})" +else + useradd -m -s /bin/sh "${KIOSK_USER}" || die 4 "useradd failed" + KIOSK_HOME=$(getent passwd "${KIOSK_USER}" | cut -d: -f6) + ok "created with locked password (no password login is possible)" +fi +[ -n "${KIOSK_HOME}" ] || die 4 "could not resolve home directory" + +# ─── 2. Stage /opt/vestibule ────────────────────────────────────────── + +step "Stage ${OPT_ROOT}" +mkdir -p "${OPT_ROOT}/bin" "${OPT_ROOT}/extension" "${OPT_ROOT}/config" "${OPT_ROOT}/docs" + +install -m 0755 "${USHER_SRC}" "${OPT_ROOT}/bin/usher" +install -m 0755 "${USHER_SRC}" "${USHER_DST}" +ok "usher installed: ${USHER_DST}" + +# Build the XPI (a zip of extension/) — python3 zipfile, no zip binary +# dependency. If an XPI is already staged and no source tree is present, +# keep the staged one. +XPI_SRC_DIR="${PROJECT_ROOT}/extension" +XPI_DST="${OPT_ROOT}/extension/vestibule.xpi" +if [ -f "${XPI_SRC_DIR}/manifest.json" ]; then + "${PYTHON_BIN}" - "${XPI_SRC_DIR}" "${XPI_DST}" <<'PYEOF' +import os, sys, zipfile +src_dir, dst = sys.argv[1], sys.argv[2] +with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as z: + for root, dirs, files in os.walk(src_dir): + dirs.sort() + for f in sorted(files): + full = os.path.join(root, f) + z.write(full, os.path.relpath(full, src_dir)) +PYEOF + chmod 0644 "${XPI_DST}" + ok "built extension XPI: ${XPI_DST}" +elif [ -f "${XPI_DST}" ]; then + ok "using staged XPI: ${XPI_DST}" +else + die 3 "no extension source at ${XPI_SRC_DIR} and no staged XPI" +fi + +for f in config/policies.json config/librewolf.overrides.cfg \ + config/vestibule.toml.example; do + if [ -f "${PROJECT_ROOT}/${f}" ]; then + install -m 0644 "${PROJECT_ROOT}/${f}" "${OPT_ROOT}/${f}" + fi +done +if [ -f "${PROJECT_ROOT}/LICENSE" ]; then install -m 0644 "${PROJECT_ROOT}/LICENSE" "${OPT_ROOT}/LICENSE"; fi +if [ -f "${PROJECT_ROOT}/DEPLOYMENT.md" ]; then install -m 0644 "${PROJECT_ROOT}/DEPLOYMENT.md" "${OPT_ROOT}/docs/DEPLOYMENT.md"; fi +if [ -f "${PROJECT_ROOT}/README.md" ]; then install -m 0644 "${PROJECT_ROOT}/README.md" "${OPT_ROOT}/docs/README.md"; fi +ok "staged config + docs" + +# ─── 3. Launcher + kiosk.env ────────────────────────────────────────── + +step "Launcher + session config" +install -m 0755 "${UNIT_SRC_DIR}/vestibule-kiosk-launch" "${LAUNCH_DST}" +ok "installed ${LAUNCH_DST}" + +mkdir -p "${ENV_DIR}" +cat > "${ENV_DIR}/kiosk.env" <= 0.1.2). +# In a VM without GPU: add WLR_LIBINPUT_NO_DEVICES=1 and WLR_RENDERER=pixman +# as separate Environment entries in the systemd unit, not here. +VESTIBULE_CAGE_ARGS="-d" +EOF +chmod 0644 "${ENV_DIR}/kiosk.env" +ok "wrote ${ENV_DIR}/kiosk.env (browser: ${BROWSER}/${FLAVOR})" + +# ─── 4. Native Messaging host for the kiosk user ────────────────────── + +step "Native Messaging host (kiosk user)" +NM_MANIFEST_BODY=$(cat < "${nm_dir}/${HOST_NAME}.json" + chmod 0644 "${nm_dir}/${HOST_NAME}.json" +done +chown -R "${KIOSK_USER}:${KIOSK_USER}" "${KIOSK_HOME}/.librewolf" \ + "${KIOSK_HOME}/.mozilla" "${KIOSK_HOME}/.var" "${KIOSK_HOME}/snap" 2>/dev/null || \ + chown -R "${KIOSK_USER}" "${KIOSK_HOME}/.librewolf" "${KIOSK_HOME}/.mozilla" +ok "manifests installed for ${KIOSK_USER} (librewolf + firefox, native + flatpak + snap)" + +# ─── 5. policies.json ───────────────────────────────────────────────── + +step "${BROWSER} policies" + +# For sandboxed flavors (flatpak/snap) the browser cannot read /opt — +# its filesystem is the kiosk home remap. Copy the XPI to a +# sandbox-visible path and point install_url there. Native flavors read +# /opt/vestibule directly. +XPI_INSTALL_URL="file://${XPI_DST}" +if [ "${FLAVOR}" = "flatpak" ]; then + if [ "${BROWSER}" = "firefox" ]; then + SANDBOX_APP_DIR="${KIOSK_HOME}/.var/app/${FF_FLATPAK_APP}" + else + SANDBOX_APP_DIR="${KIOSK_HOME}/.var/app/${LW_FLATPAK_APP}" + fi + mkdir -p "${SANDBOX_APP_DIR}" + install -m 0644 "${XPI_DST}" "${SANDBOX_APP_DIR}/vestibule.xpi" + chown -R "${KIOSK_USER}:${KIOSK_USER}" "${SANDBOX_APP_DIR}" 2>/dev/null || \ + chown -R "${KIOSK_USER}" "${SANDBOX_APP_DIR}" + # Inside the sandbox $HOME is the kiosk home path — that is where the + # browser must find the XPI. + XPI_INSTALL_URL="file://${KIOSK_HOME}/vestibule.xpi" + ok "XPI copied to sandbox-visible ${SANDBOX_APP_DIR}/vestibule.xpi" +elif [ "${FLAVOR}" = "snap" ]; then + SANDBOX_APP_DIR="${KIOSK_HOME}/snap/firefox/common" + mkdir -p "${SANDBOX_APP_DIR}" + install -m 0644 "${XPI_DST}" "${SANDBOX_APP_DIR}/vestibule.xpi" + chown -R "${KIOSK_USER}:${KIOSK_USER}" "${SANDBOX_APP_DIR}" 2>/dev/null || \ + chown -R "${KIOSK_USER}" "${SANDBOX_APP_DIR}" + XPI_INSTALL_URL="file://${KIOSK_HOME}/vestibule.xpi" + ok "XPI copied to snap-visible ${SANDBOX_APP_DIR}/vestibule.xpi" +fi + +POLICY_DIR=$(policy_dir_for_kiosk_home "${KIOSK_HOME}") +mkdir -p "${POLICY_DIR}" +POLICY_DST="${POLICY_DIR}/policies.json" +if [ -f "${POLICY_DST}" ] && [ ! -f "${POLICY_DST}.vestibule-bak" ]; then + cp "${POLICY_DST}" "${POLICY_DST}.vestibule-bak" + ok "backed up existing policies.json -> vestibule-bak" +fi + +"${PYTHON_BIN}" - "${PROJECT_ROOT}/config/policies.json" "${POLICY_DST}" \ + "${XPI_INSTALL_URL}" "${EXT_ID}" <<'PYEOF' +import json, sys +canonical_path, target, xpi_url, ext_id = sys.argv[1:5] + +policies = {"policies": {}} +try: + with open(target) as f: + existing = json.load(f) + if isinstance(existing, dict): + policies = existing +except (OSError, ValueError): + pass + +with open(canonical_path) as f: + canonical = json.load(f) + +def deep_merge(base, overlay): + for k, v in overlay.items(): + if k in base and isinstance(base[k], dict) and isinstance(v, dict): + deep_merge(base[k], v) + else: + base[k] = v + +deep_merge(policies, canonical) + +# Policy-install the extension: force_installed survives the "*" blocked +# wildcard and re-installs itself on every browser start. +policies.setdefault("policies", {}) +policies["policies"]["ExtensionSettings"] = { + "*": { + "blocked_install_message": "Extensions are not allowed on this kiosk.", + "install_sources": [], + "installation_mode": "blocked", + }, + ext_id: { + "installation_mode": "force_installed", + "install_url": xpi_url, + }, +} + +with open(target, "w") as f: + json.dump(policies, f, indent=2) + f.write("\n") +PYEOF +if [ -n "${POLICY_DIR##${KIOSK_HOME}*}" ]; then + # System-level policy file — root-owned is correct. + : +else + # Policy file inside the kiosk home (flatpak/snap flavors) — the + # browser reads it as the kiosk user. + chown "${KIOSK_USER}:${KIOSK_USER}" "${POLICY_DST}" 2>/dev/null || \ + chown "${KIOSK_USER}" "${POLICY_DST}" +fi +ok "deployed ${POLICY_DST} (extension force-installed from ${XPI_INSTALL_URL})" + +if [ "${FLAVOR}" = "flatpak" ] && [ "${BROWSER}" = "librewolf" ]; then + info "NOTE: the Flatpak app dir is replaced on every LibreWolf update." + info " re-run this script after updates (the extension's own session" + info " sanitization is unaffected — this file is layer 1 of 3)." +fi +if [ "${FLAVOR}" = "flatpak" ] || [ "${FLAVOR}" = "snap" ]; then + info "NOTE: for ${BROWSER} ${FLAVOR}, policies and the XPI live in the" + info " kiosk user's home — they survive browser updates (unlike a" + info " LibreWolf-Flatpak system-dir deploy)." +fi + +# ─── 6. systemd unit ────────────────────────────────────────────────── + +step "systemd unit" +sed -e "s|__KIOSK_USER__|${KIOSK_USER}|g" -e "s|__TTY__|${TTY_NUM}|g" \ + "${UNIT_SRC_DIR}/vestibule-kiosk.service.in" > "${UNIT_DST}" +chmod 0644 "${UNIT_DST}" +ok "generated ${UNIT_DST} (tty${TTY_NUM}, user ${KIOSK_USER})" + +systemctl daemon-reload || die 5 "systemctl daemon-reload failed" +systemctl enable vestibule-kiosk.service >/dev/null 2>&1 || die 5 "enable failed" +ok "enabled vestibule-kiosk.service" + +if [ "${START_NOW}" -eq 1 ]; then + systemctl start vestibule-kiosk.service || die 5 "start failed — check: journalctl -u vestibule-kiosk.service" + ok "started — the kiosk should be running on tty${TTY_NUM}" +else + info "start now with: sudo systemctl start vestibule-kiosk.service" + info "or reboot — the unit starts automatically at boot." +fi + +# ─── Summary ────────────────────────────────────────────────────────── + +step "Provisioning complete" +info "kiosk user : ${KIOSK_USER} (locked password)" +info "session : cage on tty${TTY_NUM} via systemd" +info "browser : ${BROWSER} (${FLAVOR})" +info "home URL : ${HOME_URL}" +info "policies : ${POLICY_DST}" +info "logs : journalctl -u vestibule-kiosk.service -f" +info "escape hatch : Ctrl+Alt+F3 (VT switching; requires VESTIBULE_CAGE_ARGS=-d)" +exit 0 diff --git a/scripts/test-native-messaging.py b/scripts/test-native-messaging.py new file mode 100755 index 0000000..0747111 --- /dev/null +++ b/scripts/test-native-messaging.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Smoke test for usher native messaging — production protocol. + +Tests the full Argon2id unlock round-trip without LibreWolf. The +protocol steps are a table: each step declares its label, request, +timeout, and pass criterion. The driver runs the table in one pass and +collects failure messages — adding a protocol step is one table entry. + +Steps: + 1. hello handshake + 2. ping/pong + 3. set-unlock "test-password" -> unlock-set ok=true (hash stored to disk) + 4. unlock "wrong-password" -> granted=false, reason="invalid" + 5. unlock "test-password" -> granted=true + 6. simulate-wake -> Wake event + +Cleans up the hash file before and after so the test is idempotent. + +Usage: + python3 scripts/test-native-messaging.py [path/to/usher] +""" +import contextlib +import json +import os +import select +import struct +import subprocess +import sys + +DEFAULT_USHER = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "helper", "target", "release", "usher", +) +USHER = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_USHER + +# Hash file path — must match helper/src/storage.rs +HASH_FILE = os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + "vestibule", "unlock.hash" +) + +STANDARD_TIMEOUT_S = 10 +ARGON2_TIMEOUT_S = 15 # Argon2id at 64 MiB takes ~1-3 s +WAKE_TIMEOUT_S = 5 +EXIT_TIMEOUT_S = 5 + + +def cleanup_hash(): + with contextlib.suppress(FileNotFoundError): + os.remove(HASH_FILE) + + +def send(proc, obj): + data = json.dumps(obj).encode("utf-8") + proc.stdin.write(struct.pack(" {response}") + return criterion(response) + return [message for message in map(run, STEPS) if message] + + +def shutdown(proc): + """Close stdin; usher must exit promptly. Returns failure messages.""" + proc.stdin.close() + try: + proc.wait(timeout=EXIT_TIMEOUT_S) + except subprocess.TimeoutExpired: + proc.kill() + return [f"usher did not exit within {EXIT_TIMEOUT_S}s of stdin close"] + return [] + + +def main(): + if not os.path.exists(USHER): + print(f"error: usher binary not found at {USHER}", file=sys.stderr) + print(" build it first: (cd helper && cargo build --release)", file=sys.stderr) + sys.exit(1) + + # Remove any hash left over from a previous run. + cleanup_hash() + + print(f"launching {USHER}") + proc = subprocess.Popen( + [USHER], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + ) + + failures = run_steps(proc) + failures.extend(shutdown(proc)) + + stderr = proc.stderr.read().decode("utf-8", errors="replace").strip() + if stderr: + print("\n--- stderr ---") + print(stderr) + print("--- end stderr ---\n") + + # Leave no hash behind. + cleanup_hash() + + if failures: + print("FAIL:") + print("\n".join(f" - {failure}" for failure in failures)) + sys.exit(1) + + print("\nOK — usher production protocol works: Argon2id unlock + wake events.") + + +if __name__ == "__main__": + main() diff --git a/scripts/test-provision-linux.sh b/scripts/test-provision-linux.sh new file mode 100755 index 0000000..d2b74fd --- /dev/null +++ b/scripts/test-provision-linux.sh @@ -0,0 +1,443 @@ +#!/bin/sh +# test-provision-linux.sh — rootless integration test for the Linux +# kiosk provisioning pipeline (LibreWolf AND Firefox). +# +# Runs provision-kiosk.sh and deprovision-kiosk.sh against a sandbox: +# all privileged destinations (/opt, /etc, /usr/local/bin, systemd) are +# rewritten to a temp directory, and privileged commands (useradd, +# systemctl, chown, id) are replaced with shims. No root required, no +# real system mutation — this is what CI runs on every push. +# +# Scenarios: +# 1. --librewolf native full provision + deprovision cycle +# 2. --firefox native /etc/firefox/policies path (distro Firefox) +# 3. --firefox flatpak kiosk-home policy path + sandbox XPI copy +# +# What it verifies per scenario: +# provision: kiosk user creation, /opt staging, XPI build (valid zip +# with manifest.json), launcher install, kiosk.env browser +# + flavor, Native Messaging manifests (all five Gecko +# locations, valid JSON, correct path), policies.json +# deep-merge (the browser's own keys survive, ours added, +# extension force-installed with the right install_url), +# unit generation + enable. +# deprovision: unit removed, policies.json equal to the pre-provision +# baseline byte-for-byte, manifests + sandbox XPI copies +# removed, staged files removed. +# +# After the scenarios, the launcher dispatch is exercised directly: all +# four env permutations (firefox/flatpak, firefox/native, +# librewolf/flatpak, defaults) run against browser shims and the exec'd +# command line is asserted. +# +# Usage: sh scripts/test-provision-linux.sh (KEEP_SANDBOX=1 to inspect) +# Exit: 0 pass, 1 fail +# +# POSIX sh — no bashisms. + +set -u + +REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) +SANDBOX=$(mktemp -d) +PASS=0 +FAIL=0 +KIOSK_USER="vestibule-kiosk" + +cleanup() { + if [ "${KEEP_SANDBOX:-0}" = "1" ]; then + say "sandbox kept at: ${SANDBOX}" + else + rm -rf "${SANDBOX}" + fi +} +trap cleanup EXIT INT TERM + +say() { printf '%s\n' "$1"; } +pass() { PASS=$((PASS+1)); say " [pass] $1"; } +fail() { FAIL=$((FAIL+1)); say " [FAIL] $1"; } +check() { # check + desc="$1"; shift + if "$@" >/dev/null 2>&1; then pass "${desc}"; else fail "${desc}"; fi +} + +# ─── Sandbox layout ─────────────────────────────────────────────────── +# +# repo/ copy of the real repo (scripts/, config/, extension/) +# root/opt fake /opt/vestibule +# root/etc fake /etc/vestibule + /etc/systemd/system +# root/etc/firefox fake /etc/firefox/policies (distro Firefox) +# root/usrlocal fake /usr/local/bin +# root/usr/lib/firefox fake distro Firefox install +# librewolf/ fake native LibreWolf install (wrapper in PATH) +# home/ fake kiosk user home +# bin/ PATH shims (privileged + browser + usher) + +mkdir -p "${SANDBOX}/repo" "${SANDBOX}/root/opt" "${SANDBOX}/root/etc/systemd/system" \ + "${SANDBOX}/root/etc/firefox/policies" "${SANDBOX}/root/usrlocal" \ + "${SANDBOX}/root/usr/lib/firefox" "${SANDBOX}/bin" \ + "${SANDBOX}/librewolf/browser" "${SANDBOX}/librewolf/distribution" \ + "${SANDBOX}/home" + +cp -r "${REPO_ROOT}/scripts" "${REPO_ROOT}/config" "${REPO_ROOT}/extension" "${SANDBOX}/repo/" +mkdir -p "${SANDBOX}/repo/helper/target/release" +printf '#!/bin/sh\nexit 0\n' > "${SANDBOX}/repo/helper/target/release/usher" +chmod +x "${SANDBOX}/repo/helper/target/release/usher" + +# Fake native LibreWolf: wrapper in PATH + real install dir with markers. +printf '#!/bin/sh\nexec "%s/librewolf/librewolf" "$@"\n' "${SANDBOX}" > "${SANDBOX}/bin/librewolf" +chmod +x "${SANDBOX}/bin/librewolf" +# Fake LibreWolf binary: logs its argv (launcher dispatch assertions). +printf '#!/bin/sh\nprintf "%%s\\n" "librewolf $*" >> "%s/browser.log"\n' "${SANDBOX}" \ + > "${SANDBOX}/librewolf/librewolf" +chmod +x "${SANDBOX}/librewolf/librewolf" +touch "${SANDBOX}/librewolf/application.ini" +# Fake Firefox in PATH for the launcher dispatch assertions. Provision +# detection never consults it: the canonical /usr/lib/firefox path wins. +printf '#!/bin/sh\nprintf "%%s\\n" "firefox $*" >> "%s/browser.log"\n' "${SANDBOX}" \ + > "${SANDBOX}/bin/firefox" +chmod +x "${SANDBOX}/bin/firefox" +# Pre-seed LibreWolf's own shipped policies — the merge must preserve +# them and the deprovision must return this baseline byte-for-byte. +cat > "${SANDBOX}/librewolf/distribution/policies.json" <<'EOF' +{ + "policies": { + "DisableAppUpdate": true, + "LibreWolfOwnSetting": "must-survive" + } +} +EOF +cp "${SANDBOX}/librewolf/distribution/policies.json" "${SANDBOX}/original-lw-policies.json" + +# Fake distro Firefox at /usr/lib/firefox (canonical Debian/Arch layout; +# Fedora uses /usr/lib64 — same code path). No PATH wrapper: detection +# must find it via the canonical path. +printf '#!/bin/sh\nexit 0\n' > "${SANDBOX}/root/usr/lib/firefox/firefox" +chmod +x "${SANDBOX}/root/usr/lib/firefox/firefox" +touch "${SANDBOX}/root/usr/lib/firefox/application.ini" +# Pre-seed Firefox's own policies in /etc/firefox/policies. +cat > "${SANDBOX}/root/etc/firefox/policies/policies.json" <<'EOF' +{ + "policies": { + "DisableTelemetry": true, + "FirefoxOwnSetting": "must-survive" + } +} +EOF +cp "${SANDBOX}/root/etc/firefox/policies/policies.json" "${SANDBOX}/original-ff-policies.json" + +# ─── PATH shims ─────────────────────────────────────────────────────── + +printf '#!/bin/sh\n# id shim: "id -u" -> 0 (root); "id -u NAME" -> uid or fail if absent\nif [ "$1" = "-u" ] && [ $# -eq 1 ]; then echo 0; exit 0; fi\nname="$2"\nif grep -q "^${name}:" "%s/passwd" 2>/dev/null; then echo 1500; exit 0; fi\nexit 1\n' \ + "${SANDBOX}" > "${SANDBOX}/bin/id" +printf '#!/bin/sh\necho useradd "$@" >> "%s/priv.log"\nmkdir -p "%s/home/vestibule-kiosk"\nprintf "vestibule-kiosk:x:1500:1500::%s/home/vestibule-kiosk:/bin/sh\\n" >> "%s/passwd"\n' \ + "${SANDBOX}" "${SANDBOX}" "${SANDBOX}" "${SANDBOX}" > "${SANDBOX}/bin/useradd" +printf '#!/bin/sh\nif [ "$1" = "passwd" ] && [ "$2" = "vestibule-kiosk" ]; then grep "^vestibule-kiosk:" "%s/passwd"; fi\nexit 0\n' \ + "${SANDBOX}" > "${SANDBOX}/bin/getent" +printf '#!/bin/sh\necho systemctl "$@" >> "%s/priv.log"\nexit 0\n' "${SANDBOX}" > "${SANDBOX}/bin/systemctl" +printf '#!/bin/sh\nexit 0\n' > "${SANDBOX}/bin/chown" +printf '#!/bin/sh\necho userdel "$@" >> "%s/priv.log"\ngrep -v "^vestibule-kiosk:" "%s/passwd" > "%s/passwd.tmp" && mv "%s/passwd.tmp" "%s/passwd"\nexit 0\n' \ + "${SANDBOX}" "${SANDBOX}" "${SANDBOX}" "${SANDBOX}" "${SANDBOX}" > "${SANDBOX}/bin/userdel" +# cage shim: log the invocation, then exec the client after "--". The +# harness pins VESTIBULE_CAGE_ARGS to a single flag (-d) in every +# launcher test, so exactly two arguments precede the client. +printf '#!/bin/sh\nprintf "%%s\\n" "cage $*" >> "%s/browser.log"\nshift 2\nexec "$@"\n' "${SANDBOX}" > "${SANDBOX}/bin/cage" +# dbus-run-session shim: pass straight through to the compositor. +printf '#!/bin/sh\nshift\nexec "$@"\n' > "${SANDBOX}/bin/dbus-run-session" +# flatpak shim: system installs "exist" (info --system succeeds), user +# installs do not, and "run" is a logged browser launch. +printf '#!/bin/sh\nif [ "$1" = "info" ]; then [ "$2" = "--system" ] && exit 0; exit 1; fi\nif [ "$1" = "run" ]; then printf "%%s\\n" "flatpak $*" >> "%s/browser.log"; exit 0; fi\nexit 1\n' \ + "${SANDBOX}" > "${SANDBOX}/bin/flatpak" +for f in "${SANDBOX}/bin/"*; do chmod +x "$f"; done + +# ─── Rewrite privileged paths in the scripts under test ─────────────── + +rewrite() { + sed -e "s|/run/systemd/system|${SANDBOX}/run-systemd|g" \ + -e "s|/opt/vestibule|${SANDBOX}/root/opt/vestibule|g" \ + -e "s|/etc/vestibule|${SANDBOX}/root/etc/vestibule|g" \ + -e "s|/etc/systemd/system|${SANDBOX}/root/etc/systemd/system|g" \ + -e "s|/etc/firefox|${SANDBOX}/root/etc/firefox|g" \ + -e "s|/usr/lib/firefox|${SANDBOX}/root/usr/lib/firefox|g" \ + -e "s|/usr/local/bin|${SANDBOX}/root/usrlocal|g" \ + -e "s|/usr/lib/librewolf|${SANDBOX}/librewolf|g" \ + "$1" > "$2" +} +mkdir -p "${SANDBOX}/run-systemd" +rewrite "${REPO_ROOT}/scripts/provision-kiosk.sh" "${SANDBOX}/repo/scripts/provision-kiosk.sh" +rewrite "${REPO_ROOT}/scripts/deprovision-kiosk.sh" "${SANDBOX}/repo/scripts/deprovision-kiosk.sh" +rewrite "${REPO_ROOT}/scripts/vestibule-kiosk-launch" "${SANDBOX}/repo/scripts/vestibule-kiosk-launch" + +export PATH="${SANDBOX}/bin:${PATH}" + +NM="${SANDBOX}/home/vestibule-kiosk" +UNIT="${SANDBOX}/root/etc/systemd/system/vestibule-kiosk.service" + +run_provision() { + sh "${SANDBOX}/repo/scripts/provision-kiosk.sh" "$@" \ + --kiosk-user "${KIOSK_USER}" --tty 2 --yes \ + > "${SANDBOX}/provision.out" 2>&1 +} +run_deprovision() { + sh "${SANDBOX}/repo/scripts/deprovision-kiosk.sh" \ + --kiosk-user "${KIOSK_USER}" \ + --remove-user --remove-opt --remove-usher --yes \ + > "${SANDBOX}/deprovision.out" 2>&1 +} +report_on_fail() { + if [ "$1" -ne 0 ]; then + sed -n '1,60p' "${SANDBOX}/provision.out" 2>/dev/null + sed -n '1,60p' "${SANDBOX}/deprovision.out" 2>/dev/null + fi +} + +assert_common_provision() { + # Everything browser-independent: staging, launcher, unit, NM. + check "usher installed" test -x "${SANDBOX}/root/usrlocal/usher" + check "usher staged under /opt" test -x "${SANDBOX}/root/opt/vestibule/bin/usher" + check "launcher installed" test -x "${SANDBOX}/root/usrlocal/vestibule-kiosk-launch" + check "XPI built" test -f "${SANDBOX}/root/opt/vestibule/extension/vestibule.xpi" + if python3 -c " +import zipfile, json +z = zipfile.ZipFile('${SANDBOX}/root/opt/vestibule/extension/vestibule.xpi') +m = json.loads(z.read('manifest.json')) +assert m['version'] == '1.2.2', m['version'] +"; then pass "XPI valid zip with manifest.json v1.2.2"; else fail "XPI valid zip with manifest.json v1.2.2"; fi + + check "unit generated" test -f "${UNIT}" + check "unit User substituted" grep -q '^User=vestibule-kiosk$' "${UNIT}" + check "unit TTYPath substituted" grep -q '^TTYPath=/dev/tty2$' "${UNIT}" + check "unit conflicts getty" grep -q 'Conflicts=getty@tty2.service' "${UNIT}" + check "unit execs launcher" grep -q 'ExecStart=/usr/local/bin/vestibule-kiosk-launch' "${UNIT}" + check "systemctl daemon-reload" grep -q "daemon-reload" "${SANDBOX}/priv.log" + check "systemctl enable" grep -q "enable vestibule-kiosk.service" "${SANDBOX}/priv.log" + + # Native Messaging manifests: all five Gecko locations, valid JSON. + for loc in ".librewolf" ".mozilla" \ + ".var/app/io.gitlab.librewolf-community/.librewolf" \ + ".var/app/org.mozilla.firefox/.mozilla" \ + "snap/firefox/common/.mozilla"; do + f="${NM}/${loc}/native-messaging-hosts/com.vestibule.usher.json" + check "NM manifest ${loc}" test -f "${f}" + if [ -f "${f}" ] && python3 -c " +import json +m = json.load(open('${f}')) +assert m['path'] == '${SANDBOX}/root/usrlocal/usher', m['path'] +assert m['allowed_extensions'] == ['vestibule@vestibule.kiosk'] +assert m['type'] == 'stdio' +"; then pass "NM manifest ${loc} valid"; else fail "NM manifest ${loc} valid"; fi + done +} + +assert_common_deprovision() { + check "unit removed" test ! -f "${UNIT}" + check "launcher removed" test ! -e "${SANDBOX}/root/usrlocal/vestibule-kiosk-launch" + check "kiosk.env removed" test ! -e "${SANDBOX}/root/etc/vestibule" + check "/opt/vestibule removed" test ! -e "${SANDBOX}/root/opt/vestibule" + check "usher removed" test ! -e "${SANDBOX}/root/usrlocal/usher" + check "userdel called" grep -q "userdel" "${SANDBOX}/priv.log" + check "NM manifests removed" test ! -e "${NM}/.librewolf/native-messaging-hosts/com.vestibule.usher.json" + check "NM manifests removed (firefox flatpak)" test ! -e "${NM}/.var/app/org.mozilla.firefox/.mozilla/native-messaging-hosts/com.vestibule.usher.json" + check "NM manifests removed (firefox snap)" test ! -e "${NM}/snap/firefox/common/.mozilla/native-messaging-hosts/com.vestibule.usher.json" + check "sandbox XPI copy removed" test ! -e "${NM}/.var/app/org.mozilla.firefox/vestibule.xpi" +} + +# ══════════════════════════════════════════════════════════════════════ +# Scenario 1: LibreWolf, native +# ══════════════════════════════════════════════════════════════════════ + +say "" +say "==> scenario 1: provision --librewolf native" +if run_provision --librewolf native --home-url https://checkin.example.org; then + pass "provision-kiosk.sh exited 0" +else + fail "provision-kiosk.sh exited nonzero" + report_on_fail 1 +fi + +assert_common_provision + +check "kiosk.env written" test -f "${SANDBOX}/root/etc/vestibule/kiosk.env" +check "kiosk.env home URL" grep -q 'VESTIBULE_HOME_URL="https://checkin.example.org"' "${SANDBOX}/root/etc/vestibule/kiosk.env" +check "kiosk.env browser librewolf" grep -q 'VESTIBULE_BROWSER="librewolf"' "${SANDBOX}/root/etc/vestibule/kiosk.env" +check "kiosk.env flavor native" grep -q 'VESTIBULE_BROWSER_FLAVOR="native"' "${SANDBOX}/root/etc/vestibule/kiosk.env" + +POL="${SANDBOX}/librewolf/distribution/policies.json" +if python3 -c " +import json +p = json.load(open('${POL}'))['policies'] +assert p['LibreWolfOwnSetting'] == 'must-survive', 'pre-existing key lost' +assert p['DisablePrivateBrowsing'] is True, 'canonical key missing' +assert p['SanitizeOnShutdown']['Cookies'] is True, 'nested key missing' +es = p['ExtensionSettings'] +assert es['*']['installation_mode'] == 'blocked' +assert es['vestibule@vestibule.kiosk']['installation_mode'] == 'force_installed' +assert es['vestibule@vestibule.kiosk']['install_url'] == 'file://${SANDBOX}/root/opt/vestibule/extension/vestibule.xpi', es['vestibule@vestibule.kiosk']['install_url'] +"; then pass "policies.json deep-merge correct (librewolf)"; else fail "policies.json deep-merge correct (librewolf)"; fi +check "policies backup created" test -f "${POL}.vestibule-bak" + +say "" +say "==> scenario 1: deprovision" +if run_deprovision; then + pass "deprovision-kiosk.sh exited 0" +else + fail "deprovision-kiosk.sh exited nonzero" + report_on_fail 1 +fi +assert_common_deprovision +if cmp -s "${POL}" "${SANDBOX}/original-lw-policies.json"; then + pass "librewolf policies.json equals the pre-provision baseline (byte-for-byte)" +else + fail "librewolf policies.json equals the pre-provision baseline (byte-for-byte)" +fi + +# ══════════════════════════════════════════════════════════════════════ +# Scenario 2: Firefox, native (distro package -> /etc/firefox/policies) +# ══════════════════════════════════════════════════════════════════════ + +say "" +say "==> scenario 2: provision --firefox native" +if run_provision --firefox native --home-url https://portal.example.org; then + pass "provision-kiosk.sh exited 0" +else + fail "provision-kiosk.sh exited nonzero" + report_on_fail 1 +fi + +assert_common_provision + +FFPOL="${SANDBOX}/root/etc/firefox/policies/policies.json" +check "kiosk.env browser firefox" grep -q 'VESTIBULE_BROWSER="firefox"' "${SANDBOX}/root/etc/vestibule/kiosk.env" +check "kiosk.env flavor native (ff)" grep -q 'VESTIBULE_BROWSER_FLAVOR="native"' "${SANDBOX}/root/etc/vestibule/kiosk.env" +check "firefox policies file deployed" test -f "${FFPOL}" +if python3 -c " +import json +p = json.load(open('${FFPOL}'))['policies'] +assert p['FirefoxOwnSetting'] == 'must-survive', 'pre-existing Firefox key lost' +assert p['DisablePrivateBrowsing'] is True, 'canonical key missing' +assert p['SanitizeOnShutdown']['Cookies'] is True, 'nested key missing' +es = p['ExtensionSettings'] +assert es['vestibule@vestibule.kiosk']['installation_mode'] == 'force_installed' +assert es['vestibule@vestibule.kiosk']['install_url'] == 'file://${SANDBOX}/root/opt/vestibule/extension/vestibule.xpi', es['vestibule@vestibule.kiosk']['install_url'] +"; then pass "firefox /etc/firefox policies deep-merge correct"; else fail "firefox /etc/firefox policies deep-merge correct"; fi +check "firefox policies backup created" test -f "${FFPOL}.vestibule-bak" +# Firefox provisioning must not touch the LibreWolf install. +if cmp -s "${POL}" "${SANDBOX}/original-lw-policies.json"; then + pass "librewolf policies untouched by firefox provision" +else + fail "librewolf policies untouched by firefox provision" +fi + +say "" +say "==> scenario 2: deprovision" +if run_deprovision; then + pass "deprovision-kiosk.sh exited 0" +else + fail "deprovision-kiosk.sh exited nonzero" + report_on_fail 1 +fi +assert_common_deprovision +if cmp -s "${FFPOL}" "${SANDBOX}/original-ff-policies.json"; then + pass "firefox /etc/firefox policies.json equals the pre-provision baseline (byte-for-byte)" +else + fail "firefox /etc/firefox policies.json equals the pre-provision baseline (byte-for-byte)" +fi + +# ══════════════════════════════════════════════════════════════════════ +# Scenario 3: Firefox, Flatpak (kiosk-home policy path + XPI copy) +# ══════════════════════════════════════════════════════════════════════ + +say "" +say "==> scenario 3: provision --firefox flatpak" +if run_provision --firefox flatpak --home-url https://lobby.example.org; then + pass "provision-kiosk.sh exited 0" +else + fail "provision-kiosk.sh exited nonzero" + report_on_fail 1 +fi + +assert_common_provision + +FFHOME_POL="${NM}/.var/app/org.mozilla.firefox/.mozilla/policies/policies.json" +check "kiosk.env browser firefox (fp)" grep -q 'VESTIBULE_BROWSER="firefox"' "${SANDBOX}/root/etc/vestibule/kiosk.env" +check "kiosk.env flavor flatpak (fp)" grep -q 'VESTIBULE_BROWSER_FLAVOR="flatpak"' "${SANDBOX}/root/etc/vestibule/kiosk.env" +check "XPI copied to flatpak home" test -f "${NM}/.var/app/org.mozilla.firefox/vestibule.xpi" +check "flatpak policies file deployed" test -f "${FFHOME_POL}" +if python3 -c " +import json +p = json.load(open('${FFHOME_POL}'))['policies'] +es = p['ExtensionSettings'] +assert es['vestibule@vestibule.kiosk']['installation_mode'] == 'force_installed' +# install_url must point INSIDE the sandbox-visible home, not /opt. +assert es['vestibule@vestibule.kiosk']['install_url'] == 'file://${NM}/vestibule.xpi', es['vestibule@vestibule.kiosk']['install_url'] +"; then pass "flatpak install_url points at sandbox-visible XPI"; else fail "flatpak install_url points at sandbox-visible XPI"; fi + +say "" +say "==> scenario 3: deprovision" +if run_deprovision; then + pass "deprovision-kiosk.sh exited 0" +else + fail "deprovision-kiosk.sh exited nonzero" + report_on_fail 1 +fi +assert_common_deprovision +check "flatpak-home policies removed" test ! -e "${FFHOME_POL}" + +# ════════════════════════════════════════════════════════════════════ +# Launcher dispatch: all four env permutations run against browser +# shims; the exec'd command line is asserted, not just parsed. +# ════════════════════════════════════════════════════════════════════ + +say "" +say "==> launcher dispatch" +LAUNCHER="${SANDBOX}/repo/scripts/vestibule-kiosk-launch" +KIOSK_ENV="${SANDBOX}/root/etc/vestibule/kiosk.env" +URL="https://launch.example.org" + +write_kiosk_env() { + mkdir -p "${SANDBOX}/root/etc/vestibule" + { + printf 'VESTIBULE_HOME_URL="%s"\n' "${URL}" + printf 'VESTIBULE_CAGE_ARGS="-d"\n' + [ -n "${1:-}" ] && printf 'VESTIBULE_BROWSER="%s"\n' "$1" + [ -n "${2:-}" ] && printf 'VESTIBULE_BROWSER_FLAVOR="%s"\n' "$2" + } > "${KIOSK_ENV}" +} + +launcher_check() { + # $1 = description, $2 = expected command line (fixed string) + desc="$1"; expect="$2" + rm -f "${SANDBOX}/browser.log" + sh "${LAUNCHER}" > "${SANDBOX}/launcher.out" 2>&1 + if grep -qF "${expect}" "${SANDBOX}/browser.log" 2>/dev/null; then + pass "launcher dispatches ${desc}" + else + fail "launcher dispatches ${desc} (log: $(cat "${SANDBOX}/browser.log" 2>/dev/null || echo empty))" + fi +} + +write_kiosk_env firefox flatpak +launcher_check "firefox/flatpak -> flatpak run" \ + "flatpak run org.mozilla.firefox --kiosk -P vestibule-profile -no-remote ${URL}" + +write_kiosk_env firefox native +launcher_check "firefox/native -> firefox" \ + "firefox --kiosk -P vestibule-profile -no-remote ${URL}" + +write_kiosk_env librewolf flatpak +launcher_check "librewolf/flatpak -> flatpak run" \ + "flatpak run io.gitlab.librewolf-community --kiosk -P vestibule-profile -no-remote ${URL}" + +write_kiosk_env "" "" +launcher_check "defaults -> librewolf native" \ + "librewolf --kiosk -P vestibule-profile -no-remote ${URL}" + +# ─── Summary ────────────────────────────────────────────────────────── + +say "" +if [ "${FAIL}" -eq 0 ]; then + say "OK — ${PASS} assertions passed: kiosk provision/deprovision works for LibreWolf native, Firefox native, and Firefox Flatpak." + exit 0 +else + say "FAIL: ${FAIL} failed of $((PASS+FAIL))" + exit 1 +fi diff --git a/scripts/test-url-policy.js b/scripts/test-url-policy.js new file mode 100644 index 0000000..9da2c6f --- /dev/null +++ b/scripts/test-url-policy.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node +// test-url-policy.js — unit tests for the Vestibule URL policy engine. +// +// The engine (extension/url-policy.js) is pure logic with no browser +// dependencies, so Node loads it directly through the module.exports +// arm of its UMD-lite export. Run: node scripts/test-url-policy.js +// +// Structure follows the project's table-driven test convention: +// every case is a row; the driver is one loop; the failure report +// names the case, the input, and both expectation and result. + +"use strict"; + +const P = require("../extension/url-policy.js"); + +let passed = 0; +let failed = 0; + +function check(label, actual, expected) { + const ok = actual === expected; + if (ok) { + passed += 1; + } else { + failed += 1; + console.error(` [FAIL] ${label}`); + console.error(` expected: ${JSON.stringify(expected)}`); + console.error(` actual: ${JSON.stringify(actual)}`); + } +} + +// ─── Entry normalization ──────────────────────────────────────────── + +console.log("==> normalizeDomainEntry"); + +const NORMALIZE_CASES = [ + // [input, expected hostname] + ["example.org", "example.org"], + [" Example.ORG ", "example.org"], // surrounding whitespace + case + ["*.example.org", "example.org"], // wildcard prefix stripped + ["https://portal.example.org/welcome", "portal.example.org"], // pasted URL + ["http://example.org:8080/path?q=1", "example.org"], // port and path dropped + ["https://Example.Org/", "example.org"], + ["not a domain", null], // spaces inside — no hostname + ["", null], + [null, null], + [" ", null], +]; + +NORMALIZE_CASES.forEach(([input, expected]) => { + check(`normalize(${JSON.stringify(input)})`, P.normalizeDomainEntry(input), expected); +}); + +// ─── Home hostname extraction ─────────────────────────────────────── + +console.log("==> homeHostnameOf"); + +check("http home", P.homeHostnameOf("http://kiosk.example.org/start"), "kiosk.example.org"); +check("https home", P.homeHostnameOf("https://portal.example.org/"), "portal.example.org"); +check("about:blank is not a home origin", P.homeHostnameOf("about:blank"), null); +check("empty", P.homeHostnameOf(""), null); +check("missing", P.homeHostnameOf(undefined), null); + +// ─── Safelist mode decisions ──────────────────────────────────────── + +console.log("==> safelist mode"); + +const SAFE = { mode: "safelist", safelist: ["example.org", "cdn.example.net"] }; +const sub = { mainFrame: true }; // subresource context would be {mainFrame:false} + +check("listed domain allowed", P.shouldBlockRequest("https://example.org/welcome", SAFE, sub), false); +check("subdomain of listed domain allowed", P.shouldBlockRequest("https://portal.example.org/", SAFE, sub), false); +check("deep subdomain allowed", P.shouldBlockRequest("https://a.b.example.org/x", SAFE, sub), false); +check("second entry allowed", P.shouldBlockRequest("https://cdn.example.net/lib.js", SAFE, sub), false); +check("unlisted domain blocked", P.shouldBlockRequest("https://evil.com/", SAFE, sub), true); +check("sibling domain blocked", P.shouldBlockRequest("https://evilexample.org/", SAFE, sub), true); +check("query-string smuggle blocked", P.shouldBlockRequest("https://evil.com/?q=example.org", SAFE, sub), true); +check("path smuggle blocked", P.shouldBlockRequest("https://evil.com/example.org", SAFE, sub), true); +check("userinfo smuggle blocked", P.shouldBlockRequest("https://example.org@evil.com/", SAFE, sub), true); +check("empty hostname (file:) blocked", P.shouldBlockRequest("file:///etc/passwd", SAFE, sub), true); + +// Internal schemes — the browser machinery must keep working. +check("about:blank always allowed", P.shouldBlockRequest("about:blank", SAFE, sub), false); +check("moz-extension always allowed", P.shouldBlockRequest("moz-extension://abc/blocked.html?u=x", SAFE, sub), false); +check("chrome: always allowed", P.shouldBlockRequest("chrome://global/skin/", SAFE, sub), false); +check("resource: always allowed", P.shouldBlockRequest("resource://gre/modules/", SAFE, sub), false); + +// data:/blob: — subresource yes, top-level no. +check("data: subresource allowed", P.shouldBlockRequest("data:image/png;base64,AAA", SAFE, { mainFrame: false }), false); +check("blob: subresource allowed", P.shouldBlockRequest("blob:https://example.org/uuid", SAFE, { mainFrame: false }), false); +check("data: top-level blocked", P.shouldBlockRequest("data:text/html,", SAFE, { mainFrame: true }), true); +check("blob: top-level blocked", P.shouldBlockRequest("blob:https://example.org/uuid", SAFE, { mainFrame: true }), true); + +// Empty safelist — the safe-by-default posture: nothing external loads. +const EMPTY = { mode: "safelist", safelist: [] }; +check("empty safelist blocks http", P.shouldBlockRequest("https://example.org/", EMPTY, sub), true); +check("empty safelist allows about:blank", P.shouldBlockRequest("about:blank", EMPTY, sub), false); + +// Unnormalized entries in the list still match (storage written by +// hand, older tools, etc. — the engine normalizes on read). +const RAW = { mode: "safelist", safelist: ["https://Example.ORG/path"] }; +check("raw URL entry normalizes on read", P.shouldBlockRequest("https://sub.example.org/", RAW, sub), false); + +// ─── Home-origin guarantee ────────────────────────────────────────── + +console.log("==> home-origin guarantee"); + +const HOME_CTX = { mainFrame: true, homeHostname: "lobby.example.org" }; +check("home origin passes empty safelist", P.shouldBlockRequest("https://lobby.example.org/start", EMPTY, HOME_CTX), false); +check("home origin passes with safelist active", P.shouldBlockRequest("https://lobby.example.org/", SAFE, HOME_CTX), false); +check("subdomain of home is NOT auto-covered", P.shouldBlockRequest("https://www.lobby.example.org/", EMPTY, HOME_CTX), true); +check("sibling of home blocked", P.shouldBlockRequest("https://lobby.example.org.evil.com/", EMPTY, HOME_CTX), true); +check("home exemption applies to subresources too", P.shouldBlockRequest("https://lobby.example.org/app.js", EMPTY, { mainFrame: false, homeHostname: "lobby.example.org" }), false); + +// ─── Legacy modes (behavior unchanged from 1.2.0) ────────────────── + +console.log("==> legacy modes"); + +check("open never blocks", P.shouldBlockRequest("https://anything.anywhere/", { mode: "open" }, sub), false); +check("blocklist blocks substring", P.shouldBlockRequest("https://example.org/blocked/x", { mode: "blocklist", blocklist: ["example.org/blocked"] }, sub), true); +check("blocklist allows the rest", P.shouldBlockRequest("https://example.org/ok", { mode: "blocklist", blocklist: ["example.org/blocked"] }, sub), false); +check("allowlist allows listed substring", P.shouldBlockRequest("https://example.org/x", { mode: "allowlist", allowlist: ["example.org"] }, sub), false); +check("allowlist blocks unlisted", P.shouldBlockRequest("https://other.org/", { mode: "allowlist", allowlist: ["example.org"] }, sub), true); +check("allowlist with empty list allows everything (documented legacy quirk)", P.shouldBlockRequest("https://anything.org/", { mode: "allowlist", allowlist: [] }, sub), false); +check("substring allowlist passes query-string smuggle (the weakness safelist fixes)", P.shouldBlockRequest("https://evil.com/?q=example.org", { mode: "allowlist", allowlist: ["example.org"] }, sub), false); + +// ─── Fail-closed on unknown mode ──────────────────────────────────── + +console.log("==> unknown mode fails closed"); + +const GARBAGE = { mode: "alllowlist", safelist: [] }; // corrupted policy +check("unknown mode blocks external", P.shouldBlockRequest("https://evil.com/", GARBAGE, sub), true); +check("unknown mode keeps internal pages working", P.shouldBlockRequest("about:blank", GARBAGE, sub), false); +check("unknown mode keeps home working", P.shouldBlockRequest("https://home.org/", GARBAGE, { mainFrame: true, homeHostname: "home.org" }), false); + +// ─── First-boot startup adoption ──────────────────────────────────── + +console.log("==> startup adoption"); + +const DEFAULTS = { mode: "safelist", safelist: [], homeUrl: "about:blank" }; + +let adopted = P.adoptStartupPolicy( + ["about:blank", "https://checkin.example.org/welcome"], DEFAULTS); +check("provisioned startup page adopted", adopted !== null, true); +check("adopted home URL is the startup page", + !!(adopted && adopted.homeUrl === "https://checkin.example.org/welcome"), true); +check("adopted safelist is the page's domain", + !!(adopted && adopted.safelist.length === 1 && adopted.safelist[0] === "checkin.example.org"), true); + +check("no http startup tabs → no adoption", + P.adoptStartupPolicy(["about:blank"], DEFAULTS), null); +check("no tabs at all → no adoption", P.adoptStartupPolicy([], DEFAULTS), null); +check("configured home → no adoption", + P.adoptStartupPolicy(["https://checkin.example.org/"], { ...DEFAULTS, homeUrl: "https://other.example.org/" }), null); +check("non-empty safelist → no adoption", + P.adoptStartupPolicy(["https://checkin.example.org/"], { ...DEFAULTS, safelist: ["existing.example.org"] }), null); +check("null policy → no adoption", P.adoptStartupPolicy(["https://x.example.org/"], null), null); + +// The adopted policy must actually admit the startup page through the +// engine (home guarantee + safelist entry). +check("adopted policy admits the startup page", + !!(adopted && !P.shouldBlockRequest(adopted.homeUrl, adopted, { mainFrame: true })), true); +check("adopted policy still blocks other domains", + !!(adopted && P.shouldBlockRequest("https://evil.com/", adopted, { mainFrame: true })), true); +check("adopted policy admits subdomains of the home domain", + !!(adopted && !P.shouldBlockRequest("https://portal.checkin.example.org/", adopted, { mainFrame: true })), true); + +// ─── Report ───────────────────────────────────────────────────────── + +console.log(""); +if (failed === 0) { + console.log(`OK — ${passed}/${passed + failed} URL policy assertions pass.`); + process.exit(0); +} +console.log(`FAIL: ${failed} of ${passed + failed} assertions failed.`); +process.exit(1); diff --git a/scripts/validate.sh b/scripts/validate.sh new file mode 100755 index 0000000..3a5b248 --- /dev/null +++ b/scripts/validate.sh @@ -0,0 +1,203 @@ +#!/bin/sh +# validate.sh — local pre-ship validation for the Vestibule release. +# Mirrors the CI checks that can run without Windows/flatpak/cargo. +# +# Usage: sh scripts/validate.sh (from anywhere inside the repo copy) +# Exit: 0 all pass, 1 failure + +REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "${REPO_ROOT}" + +FAILURES=0 +say() { printf '%s\n' "$1"; } +pass() { say " [pass] $1"; } +fail() { FAILURES=$((FAILURES+1)); say " [FAIL] $1"; } + +say "==> POSIX sh syntax" +for f in scripts/*.sh scripts/vestibule-kiosk-launch packaging/vestibule-flatpak-cli packaging/build-flatpak.sh; do + if sh -n "$f" 2>/dev/null; then pass "$f"; else fail "$f"; fi +done + +say "==> Python syntax" +for f in scripts/*.py; do + if python3 -m py_compile "$f" 2>/dev/null; then pass "$f"; else fail "$f"; fi +done + +say "==> JSON validity" +for f in extension/manifest.json config/policies.json config/com.vestibule.usher.linux.json \ + config/com.vestibule.usher.windows.json packaging/net.dcos.Vestibule.json; do + if python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then pass "$f"; else fail "$f"; fi +done + +say "==> XML validity" +if python3 -c "import xml.dom.minidom; xml.dom.minidom.parse('packaging/net.dcos.Vestibule.metainfo.xml')" 2>/dev/null; then + pass "metainfo.xml" +else + fail "metainfo.xml" +fi + +say "==> YAML validity (ci.yml)" +if python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" 2>/dev/null; then + pass "ci.yml" +else + fail "ci.yml" +fi + +say "==> Version consistency (1.2.2)" +v_cargo=$(sed -n 's/^version = "\(.*\)"/\1/p' helper/Cargo.toml | head -1) +v_manifest=$(python3 -c "import json; print(json.load(open('extension/manifest.json'))['version'])") +v_metainfo=$(python3 -c "import re; print(re.search(r'release version=\"([^\"]+)\"', open('packaging/net.dcos.Vestibule.metainfo.xml').read()).group(1))") +v_iss=$(sed -n 's/^#define MyAppVersion "\(.*\)"/\1/p' packaging/vestibule.iss | head -1) +v_cli=$(sed -n 's/^VERSION="\(.*\)"/\1/p' packaging/vestibule-flatpak-cli | head -1) +for pair in "Cargo.toml:$v_cargo" "manifest.json:$v_manifest" "metainfo:$v_metainfo" "vestibule.iss:$v_iss" "flatpak-cli:$v_cli"; do + name=${pair%%:*}; val=${pair#*:} + if [ "$val" = "1.2.2" ]; then pass "$name = $val"; else fail "$name = $val (expected 1.2.2)"; fi +done + +say "==> PowerShell sanity (structure checks)" +if python3 - <<'PYEOF' +import sys, re + +files = [ + "scripts/provision-kiosk.ps1", + "scripts/deprovision-kiosk.ps1", + "scripts/kiosk-launch.ps1", + "packaging/build-installer.ps1", +] +problems = [] +for path in files: + src = open(path, encoding="utf-8").read() + lines = src.splitlines() + # Here-string terminators must start at column 0. + for i, ln in enumerate(lines, 1): + if re.match(r'^\s+@"|^\s+@\'', ln): + problems.append(f"{path}:{i} here-string opener must be at column 0") + # Brace/paren/bracket balance outside strings and comments (heuristic). + # Order matters: strip quoted strings FIRST (strings may contain '#'), + # then strip trailing comments (comments may contain quotes). + cleaned = [] + in_herestring = False + for ln in lines: + if in_herestring: + if ln.startswith('"@') or ln.startswith("'@"): + in_herestring = False + continue + if ln.lstrip().startswith('@"') or ln.lstrip().startswith("@'"): + in_herestring = True + continue + no_strings = re.sub(r'"[^"]*"', '""', ln) + no_strings = re.sub(r"'[^']*'", "''", no_strings) + no_comment = re.sub(r'#.*$', '', no_strings) + cleaned.append(no_comment) + blob = "\n".join(cleaned) + for open_c, close_c in [("{", "}"), ("(", ")"), ("[", "]")]: + if blob.count(open_c) != blob.count(close_c): + problems.append( + f"{path}: unbalanced {open_c}{close_c} " + f"({blob.count(open_c)} vs {blob.count(close_c)})") +if problems: + print("\n".join(problems)) + sys.exit(1) +print(f"{len(files)} files structurally consistent") +PYEOF +then + pass "PowerShell structural checks" +else + fail "PowerShell structural checks" +fi + +say "==> URL policy unit tests (node)" +# The engine is pure JavaScript with no browser dependencies; Node runs +# the same file the background script loads. CI runs this gate on every +# push; on a node-less machine it is reported, not silently skipped. +if command -v node >/dev/null 2>&1; then + if node scripts/test-url-policy.js >/tmp/vestibule-urlpolicy.log 2>&1; then + pass "scripts/test-url-policy.js" + else + fail "scripts/test-url-policy.js" + tail -20 /tmp/vestibule-urlpolicy.log + fi +else + say " [warn] node not found — URL policy unit tests skipped (CI runs them)" +fi + +say "==> XPI build smoke test" +tmp_xpi=$(mktemp -u).xpi +if python3 - "$REPO_ROOT/extension" "$tmp_xpi" <<'PYEOF' +import os, sys, zipfile +src_dir, dst = sys.argv[1], sys.argv[2] +with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as z: + for root, dirs, files in os.walk(src_dir): + dirs.sort() + for f in sorted(files): + full = os.path.join(root, f) + z.write(full, os.path.relpath(full, src_dir)) +with zipfile.ZipFile(dst) as z: + m = __import__("json").loads(z.read("manifest.json")) + assert m["version"] == "1.2.2" + assert m["browser_specific_settings"]["gecko"]["id"] == "vestibule@vestibule.kiosk" + assert m["background"]["scripts"][0] == "url-policy.js", "policy engine must load first" + for name in ["background.js", "url-policy.js", "content.js", "admin.html", "unlock.html", "blocked.html"]: + assert name in z.namelist(), name +PYEOF +then + pass "XPI builds; manifest + all entry points present" +else + fail "XPI build" +fi +rm -f "$tmp_xpi" + +say "==> Icon assets" +for f in packaging/icons/vestibule.ico packaging/icons/vestibule.svg packaging/icons/vestibule-256.png; do + if [ -s "$f" ]; then pass "$f"; else fail "$f"; fi +done + +say "==> Historic artifact scrub (history/)" +# The 2001 VB6 artifact and every other file in the tree must stay free +# of the scrubbed identifiers: the school initials, the employer names, +# the original unlock code, and the employer-branded project name. +if python3 - <<'PYEOF' +import os +import sys + +FORBIDDEN = ("gl" "ths", # school initials + "advanced technical " "solutions", # employer name + "ats" "inc", # original unlock code + "ats" "browser", # employer-branded project name + "school" "version") # renamed form's old filename +hits = [] +for root, dirs, files in os.walk("."): + dirs[:] = [d for d in dirs if d not in (".git", "__pycache__")] + for name in files: + path = os.path.join(root, name) + try: + blob = open(path, "rb").read().decode("utf-8", "ignore").lower() + except OSError: + continue + hits.extend(f"{path}: {token}" for token in FORBIDDEN if token in blob) +if hits: + print("\n".join(hits)) + sys.exit(1) +PYEOF +then + pass "no scrub-list identifiers anywhere in the tree" +else + fail "scrubbed identifiers present (see list above)" +fi + +say "==> Rootless provision/deprovision integration test" +if sh scripts/test-provision-linux.sh > /tmp/vestibule-inttest.log 2>&1; then + pass "3-scenario integration test (LibreWolf native + Firefox native + Firefox Flatpak)" +else + fail "integration test" + tail -40 /tmp/vestibule-inttest.log +fi + +say "" +if [ "${FAILURES}" -eq 0 ]; then + say "OK — all local validation passed." + exit 0 +else + say "FAIL: ${FAILURES} check(s) failed" + exit 1 +fi diff --git a/scripts/vestibule-kiosk-launch b/scripts/vestibule-kiosk-launch new file mode 100755 index 0000000..ceb4915 --- /dev/null +++ b/scripts/vestibule-kiosk-launch @@ -0,0 +1,73 @@ +#!/bin/sh +# vestibule-kiosk-launch — the process the kiosk systemd unit execs. +# +# Reads /etc/vestibule/kiosk.env, then starts the cage compositor with +# the configured Gecko browser (LibreWolf or Firefox; native, Flatpak, +# or snap) as its only client. dbus-run-session provides the session +# bus that both cage children and sandboxed browsers need. +# +# Installed to /usr/local/bin by provision-kiosk.sh. Edit +# /etc/vestibule/kiosk.env to change behavior — never this file. +# +# kiosk.env keys: +# VESTIBULE_HOME_URL page the kiosk opens (default: about:blank) +# VESTIBULE_BROWSER librewolf (default) | firefox +# VESTIBULE_BROWSER_FLAVOR native (default) | flatpak | snap +# VESTIBULE_CAGE_ARGS extra cage flags (default: -d) +# +# Dispatch is step-down: flavor first (flatpak runs the sandbox app), +# then browser name (native and snap flavors share the plain exec). +# +# POSIX sh — no bashisms. Runs on any minimal Linux base. + +set -eu + +ENV_FILE="/etc/vestibule/kiosk.env" +if [ -r "${ENV_FILE}" ]; then + . "${ENV_FILE}" +fi + +: "${VESTIBULE_HOME_URL:=about:blank}" +: "${VESTIBULE_BROWSER:=librewolf}" +: "${VESTIBULE_BROWSER_FLAVOR:=native}" +# cage flags: "-d" allows VT switching (admin escape hatch — switch away +# from the kiosk with Ctrl+Alt+F3 etc. on cage >= 0.1.2). Set to "" on +# older cage builds that reject it. +: "${VESTIBULE_CAGE_ARGS:=-d}" + +LW_FLATPAK_APP="io.gitlab.librewolf-community" +FF_FLATPAK_APP="org.mozilla.firefox" + +# Gecko defaults to X11 and cage ships no Xwayland: force native Wayland +# or the browser exits with "cannot open display". Applies to every +# Gecko flavor — LibreWolf and Firefox alike. +MOZ_ENABLE_WAYLAND=1 +GDK_BACKEND=wayland +XDG_SESSION_TYPE=wayland +export MOZ_ENABLE_WAYLAND GDK_BACKEND XDG_SESSION_TYPE + +CAGE="cage" +command -v cage >/dev/null 2>&1 || CAGE="/usr/bin/cage" + +URL="${VESTIBULE_HOME_URL}" + +if [ "${VESTIBULE_BROWSER_FLAVOR}" = "flatpak" ]; then + FLATPAK_APP="${LW_FLATPAK_APP}" + case "${VESTIBULE_BROWSER}" in + firefox) FLATPAK_APP="${FF_FLATPAK_APP}" ;; + esac + # shellcheck disable=SC2086 # VESTIBULE_CAGE_ARGS is intentionally word-split + exec dbus-run-session -- "${CAGE}" ${VESTIBULE_CAGE_ARGS} -- \ + flatpak run "${FLATPAK_APP}" \ + --kiosk -P vestibule-profile -no-remote "${URL}" +fi + +# native and snap flavors both exec the browser by name — the snap +# wrapper ships the same CLI. +BROWSER_BIN="librewolf" +case "${VESTIBULE_BROWSER}" in + firefox) BROWSER_BIN="firefox" ;; +esac +# shellcheck disable=SC2086 # VESTIBULE_CAGE_ARGS is intentionally word-split +exec dbus-run-session -- "${CAGE}" ${VESTIBULE_CAGE_ARGS} -- \ + "${BROWSER_BIN}" --kiosk -P vestibule-profile -no-remote "${URL}" diff --git a/scripts/vestibule-kiosk.service.in b/scripts/vestibule-kiosk.service.in new file mode 100644 index 0000000..fd64c8b --- /dev/null +++ b/scripts/vestibule-kiosk.service.in @@ -0,0 +1,29 @@ +[Unit] +Description=Vestibule kiosk (cage + LibreWolf) +Documentation=file:/opt/vestibule/docs/DEPLOYMENT.md +# The cage compositor takes a VT and runs LibreWolf as the kiosk user in +# a full-screen, no-chrome Wayland session. No display manager is needed: +# this unit IS the graphical session, started directly at boot. +After=systemd-user-sessions.service dbus.service +Conflicts=getty@tty__TTY__.service + +[Service] +Type=simple +User=__KIOSK_USER__ +# PAMName=login gives the service login-session semantics: pam_systemd +# creates the runtime directory (XDG_RUNTIME_DIR) that Wayland needs. +# The account's password stays locked — nothing authenticates to get in. +PAMName=login +TTYPath=/dev/tty__TTY__ +StandardInput=tty +StandardOutput=journal +StandardError=journal +UtmpIdentifier=tty__TTY__ +# The launcher reads /etc/vestibule/kiosk.env (home URL, LibreWolf +# flavor, cage args) and execs: dbus-run-session -- cage -- librewolf. +ExecStart=/usr/local/bin/vestibule-kiosk-launch +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/scripts/vestibule-usher.service b/scripts/vestibule-usher.service new file mode 100755 index 0000000..f10dd1f --- /dev/null +++ b/scripts/vestibule-usher.service @@ -0,0 +1,24 @@ +[Unit] +Description=Vestibule usher — kiosk unlock helper + power monitor +After=graphical-session.target +PartOf=graphical-session.target + +[Service] +Type=simple +# usher is launched by LibreWolf via Native Messaging when the extension +# connects. This systemd unit is a watchdog that ensures usher is +# available even before LibreWolf starts, and restarts it if it crashes +# outside of a Native Messaging session. +# +# In typical kiosk operation, LibreWolf manages usher's lifecycle. +# This unit is the step-down supervisor for bare-metal deployments +# where usher must run ahead of the browser for unlock and power +# monitoring. +ExecStart=%h/.local/bin/usher +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=graphical-session.target