dsh-remote 0.1.0: master/slave remote control for DSH

- authenticated WebSocket link (dependency-free RFC6455 + stream mux), pairing
  codes, per-device keys (hashed), revoke/remove
- /r/<device>/ proxy of the full DSH UI (HTTP, Remote WebSocket, SSE) with
  per-device cookie minting; slave->slave relay through the master
- Devices sidebar panel (all devices' sessions, new session on device),
  Remote devices settings page, finished-session toasts
- plugin + config sync master->slave: registry bundles via pluginManager,
  link: plugins as tarballs, cordis.patch.yml into a managed bundle layer
  (!!js preserved), allow/deny, opt-in per-secret credential sync
- deploy helpers + e2e probes, unit tests
This commit is contained in:
lyuksovannyy committed 2026-10-11 08:23:19 +00:00
commit 8872e73e76
21 files changed
+2914

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
// Offline tests for the link stack: WebSocket framing, mux calls/streams, HTTP + WS
// tunnelling through the proxy helpers, cookie minting, sync allow/deny.
import test from 'node:test'
import assert from 'node:assert/strict'
import crypto from 'node:crypto'
import http from 'node:http'
import { acceptUpgrade, connect, rejectUpgrade } from '../lib/ws.mjs'
import { Mux } from '../lib/mux.mjs'
import { ownHttp, ownUpgrade, viewHttp, viewUpgrade, stripDshCookies } from '../lib/proxy.mjs'
import { cookieName, mintCookie } from '../lib/auth-cookie.mjs'
import { allowed } from '../lib/sync.mjs'
const listen = (srv) => new Promise((r) => srv.listen(0, '127.0.0.1', () => r(srv.address().port)))
async function linkPair({ handlersA = {}, handlersB = {}, onStreamA, onStreamB } = {}) {
const srv = http.createServer()
let serverMux
const ready = new Promise((resolve) => {
srv.on('upgrade', (req, socket, head) => {
if (req.headers['x-key'] !== 'k') return rejectUpgrade(socket, 401, 'nope')
const ws = acceptUpgrade(req, socket, head)
serverMux = new Mux(ws, { odd: false, handlers: handlersA, onStream: onStreamA })
resolve()
})
})
const port = await listen(srv)
const ws = await connect(`ws://127.0.0.1:${port}/link`, { headers: { 'x-key': 'k' } })
const clientMux = new Mux(ws, { odd: true, handlers: handlersB, onStream: onStreamB })
await ready
return { srv, port, serverMux, clientMux, close: () => { clientMux.close(); srv.close() } }
}
test('ws: refused upgrade surfaces HTTP status', async () => {
const srv = http.createServer()
srv.on('upgrade', (req, socket) => rejectUpgrade(socket, 401, 'bad key'))
const port = await listen(srv)
await assert.rejects(connect(`ws://127.0.0.1:${port}/`), (e) => e.status === 401 && /bad key/.test(e.message))
srv.close()
})
test('mux: calls both ways, large binary stream, errors', async () => {
const big = crypto.randomBytes(3 * 1024 * 1024 + 17)
const p = await linkPair({
handlersA: { echo: async (x) => ({ back: x.v }), boom: async () => { throw Object.assign(new Error('kaboom'), { status: 418 }) } },
handlersB: { whoami: async () => 'slave' },
onStreamA: (s) => {
const parts = []
s.on('data', (d) => parts.push(d))
s.on('end', () => { const buf = Buffer.concat(parts); s.head({ size: buf.length }); s.write(buf); s.end() })
s.start()
},
})
assert.deepEqual(await p.clientMux.call('echo', { v: 42 }), { back: 42 })
assert.equal(await p.serverMux.call('whoami'), 'slave')
await assert.rejects(p.clientMux.call('boom'), (e) => e.status === 418 && e.message === 'kaboom')
await assert.rejects(p.clientMux.call('missing'), /unknown method/)
const s = p.clientMux.open('echo', {})
const got = await new Promise((resolve, reject) => {
const parts = []
s.on('data', (d) => parts.push(d))
s.on('end', () => resolve(Buffer.concat(parts)))
s.on('error', reject)
s.write(big)
s.end()
})
assert.equal(got.length, big.length)
assert.ok(got.equals(big))
p.close()
})
test('proxy: HTTP + WebSocket through the link with cookie + host rewrite', async () => {
// "Remote DSH": checks host + cookie like dsh-client-connection would.
const secret = crypto.randomBytes(32).toString('base64url')
const upstream = http.createServer((req, res) => {
const authority = req.headers.host
const ok = (req.headers.cookie || '').includes(cookieName(authority) + '=v1.')
if (!ok) { res.writeHead(401); return res.end('auth') }
const body = []
req.on('data', (d) => body.push(d))
req.on('end', () => {
res.writeHead(200, { 'content-type': 'text/plain', 'x-path': req.url, 'set-cookie': 'leak=1' })
res.end(`${req.method} ${req.url} ${Buffer.concat(body).toString()} origin=${req.headers.origin || '-'} viewerCookie=${/dsh-auth-viewer/.test(req.headers.cookie) ? 'leaked' : 'stripped'}`)
})
})
upstream.on('upgrade', (req, socket, head) => {
const ok = (req.headers.cookie || '').includes(cookieName(req.headers.host) + '=v1.')
if (!ok) return rejectUpgrade(socket, 401, 'auth')
const ws = acceptUpgrade(req, socket, head)
ws.on('error', () => {})
ws.on('message', (m) => ws.send(`echo:${m}`))
})
const upPort = await listen(upstream)
const target = { host: '127.0.0.1', port: upPort }
const cookie = async (authority) => mintCookie(authority, secret)
const p = await linkPair({
onStreamA: (s) => (s.kind === 'http' ? ownHttp(s, { target, cookie }) : ownUpgrade(s, { target, cookie })),
})
// Viewer webserver that mounts the remote at /r/x/.
const viewer = http.createServer((req, res) => {
viewHttp(req, res, (kind, meta) => p.clientMux.open(kind, meta), { rest: req.url.replace(/^\/r\/x/, ''), basePath: '/r/x' })
})
viewer.on('upgrade', (req, socket, head) => {
viewUpgrade(req, socket, head, (kind, meta) => p.clientMux.open(kind, meta), { rest: req.url.replace(/^\/r\/x/, '') })
})
const vPort = await listen(viewer)
const r = await fetch(`http://127.0.0.1:${vPort}/r/x/api/thing?q=1`, {
method: 'POST',
body: 'hello',
headers: { cookie: 'dsh-auth-viewer=abc; other=1', origin: `http://127.0.0.1:${vPort}` },
})
const text = await r.text()
assert.equal(r.status, 200)
assert.equal(r.headers.get('set-cookie'), null, 'remote set-cookie must not reach the browser')
assert.match(text, /^POST \/api\/thing\?q=1 hello/)
assert.match(text, new RegExp(`origin=http://127.0.0.1:${upPort}`))
assert.match(text, /viewerCookie=stripped/)
const ws = await connect(`ws://127.0.0.1:${vPort}/r/x/api/remote.mux`)
const reply = await new Promise((resolve) => { ws.on('message', (m) => resolve(m)); ws.send('ping-1') })
assert.equal(reply, 'echo:ping-1')
ws.close()
p.close(); viewer.close(); upstream.close()
})
test('cookie + allow/deny helpers', () => {
const secret = crypto.randomBytes(32).toString('base64url')
const c = mintCookie('127.0.0.1:3081', secret)
assert.match(c, /^dsh-auth-[A-Za-z0-9_-]+=v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)
assert.equal(mintCookie('a', 'short'), null)
assert.equal(stripDshCookies('dsh-auth-x=1; a=2; dsh_auth_session=3'), 'a=2')
assert.equal(allowed('@local/foo', {}), true)
assert.equal(allowed('@local/foo', { deny: ['@local/*'] }), false)
assert.equal(allowed('@local/foo', { allow: ['@other/*'] }), false)
assert.equal(allowed('@local/foo', { allow: ['@local/foo'], deny: [] }), true)
})