mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
* 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)
80 lines
3.9 KiB
TypeScript
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')
|
|
})
|
|
})
|