Files
orca/cloud/apps/sandbox-control/image/desktop-proxy.test.ts
T
Neil 60377d3364 feat(cloud): remote desktop Beta — single-use tickets, fit, clipboard, keyboard release (W10 RD-1..6) (#122)
* feat(cloud): remote desktop single-use tickets, fit, clipboard and keyboard release

- VM: Xvnc on a 0600 socket with no VNC auth plus an X cookie; orca-desktop-proxy (websockify
  subclass) admits one WebSocket per unused 60 s ticket from the same localhost origin, refuses
  rebinding Hosts, scrubs tickets from logs, serves the page with connect-src 'self'.
- Worker: POST /desktop mints a fresh ticket per call (schemaVersion 2).
- Desktop: no secret in the tab URL; main hands the page tickets on load and reconnect (backoff,
  floor, pause with Reconnect), only for the exact URL of a live forward it opened; keyboard stays
  in the desktop until Ctrl+Cmd / Ctrl+Alt; a paste key pushes this computer's clipboard; sandbox
  copies reach this computer after a copy gesture.
- UI: keyboard hint, asleep/starting neutral tone and mapping, persistent error toasts, Beta badge.

* fix(cloud): remote desktop review fixes

- Security review: noVNC host/port/encrypt pinned to the page's origin, CSP connect-src 'self',
  exact-URL + live-forward trust, mint floor and per-minute cap, copy gate on copy gestures only,
  X display cookie.
- Design review: archived is its own neutral state, starting covers waking, page messages say
  "Wake it in Orca", pause overlay is a status for calm states and focuses Reconnect, hint wraps
  and hides from assistive tech when faded.
- FE review: paste follows the keyboard layout, AltGr never releases, page calls settle on
  navigation, state notices only on change, hint once per load/release, paste always pushes,
  no committed bytecode, session tests.

* fix(cloud): remote desktop archived page message, hint layout, fixtures

* fix(cloud): remote desktop copy gate counts double-clicks (pointer events have no click count)

* chore(cloud): remote desktop lint (exhaustive pause reasons, no test casts or wall clock)
2026-11-09 17:36:19 -08:00

80 lines
3.9 KiB
TypeScript

// The remote desktop's ticket + origin gate (vm/orca-desktop-proxy), exercised in python3 with a
// stub websockify. The real thing runs on a VM in the W3 desktop e2e (see the w3-desktop report).
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const proxy = fileURLToPath(new URL('./vm/orca-desktop-proxy', import.meta.url).href)
const hasPython = spawnSync('python3', ['--version']).status === 0
function runPython(body: string): { status: number | null; stdout: string; stderr: string } {
const stub = mkdtempSync(join(tmpdir(), 'orca-desktop-proxy-'))
mkdirSync(join(stub, 'websockify'))
writeFileSync(join(stub, 'websockify', '__init__.py'), '')
writeFileSync(join(stub, 'websockify', 'websocketproxy.py'), 'class ProxyRequestHandler:\n pass\n')
const prelude = [
'import importlib.machinery, importlib.util, os, sys, tempfile, hashlib',
`loader = importlib.machinery.SourceFileLoader('proxy', ${JSON.stringify(proxy)})`,
'spec = importlib.util.spec_from_loader("proxy", loader)',
'p = importlib.util.module_from_spec(spec); loader.exec_module(p)',
'd = tempfile.mkdtemp()',
'def mint(t, expires):',
' open(os.path.join(d, hashlib.sha256(t.encode()).hexdigest()), "w").write(str(expires))',
''
].join('\n')
const run = spawnSync('python3', ['-c', `${prelude}\n${body}`], {
env: { ...process.env, PYTHONPATH: stub, PYTHONDONTWRITEBYTECODE: '1' },
encoding: 'utf8'
})
return { status: run.status, stdout: run.stdout, stderr: run.stderr }
}
describe.skipIf(!hasPython)('orca-desktop-proxy', () => {
it('admits a ticket once, before it expires, and never a malformed one', () => {
const run = runPython(
[
'T = "A" * 43; U = "B" * 43; X = "C" * 43',
'mint(T, 2000); mint(U, 2000); mint(X, 999)',
'print(p.consume_ticket(d, T, now=1000))', // fresh
'print(p.consume_ticket(d, T, now=1000))', // replay
'print(p.consume_ticket(d, X, now=1000))', // expired
'print(p.consume_ticket(d, X, now=1000))', // and it was still spent
'print(p.consume_ticket(d, "B" * 42, now=1000))', // malformed
'print(p.consume_ticket(d, "../" + "B" * 40, now=1000))', // path tricks never reach the disk
'print(p.consume_ticket(d, U, now=1000))',
'print(sorted(os.listdir(d)))'
].join('\n')
)
expect(run.stderr).toBe('')
expect(run.stdout.trim().split('\n')).toEqual(['True', 'False', 'False', 'False', 'False', 'False', 'True', '[]'])
})
it('wants the page and the WebSocket on one localhost origin', () => {
const run = runPython(
[
'cases = [',
' ({"Host": "localhost:6081", "Origin": "http://localhost:6081"}, True),',
' ({"Host": "127.0.0.1:6091", "Origin": "http://127.0.0.1:6091"}, True),',
' ({"Host": "localhost:6081", "Origin": "http://localhost:3000"}, False),',
' ({"Host": "localhost:6081", "Origin": "http://evil.example"}, False),',
' ({"Host": "localhost:6081"}, False),',
' ({"Host": "evil.example:6081", "Origin": "http://evil.example:6081"}, False),',
' ({"Host": "localhost.evil.example", "Origin": "http://localhost.evil.example"}, False),',
' ({"Host": "localhost:6081", "Origin": "https://localhost:6081"}, False),',
']',
'print(all(p.same_loopback_origin(h) == want for h, want in cases))'
].join('\n')
)
expect(run.stderr).toBe('')
expect(run.stdout.trim()).toBe('True')
})
it('scrubs tickets from anything it logs', () => {
const run = runPython('print(p.scrub("GET /websockify?ticket=SECRETSECRET&x=1 HTTP/1.1"))')
expect(run.stdout.trim()).toBe('GET /websockify?ticket=<redacted>&x=1 HTTP/1.1')
})
})