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:
commit
8872e73e76
21 files changed
+2914
No files matched your search
@@ -0,0 +1,58 @@
|
||||
// Mint DSH's own browser-session cookie for a given authority.
|
||||
//
|
||||
// Format (dsh-client-connection BrowserAuth): cookie name `dsh-auth-` +
|
||||
// base64url(sha256(authority)), value `v1.<b64u(json)>.<b64u(hmac-sha256(secret, body))>`
|
||||
// with json {version:1, authority, issuedAt, expiresAt}. The 32-byte secret is the
|
||||
// credential record `client-connection/browser-session` (kind grant, payload.secret).
|
||||
//
|
||||
// The target DSH accepts proxied requests only when Host matches the cookie's authority,
|
||||
// so the proxy rewrites Host to the loopback authority it forwards to and attaches a
|
||||
// cookie minted for exactly that authority. The browser never sees this cookie.
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
const DAY = 86400000
|
||||
const b64u = (b) => Buffer.from(b).toString('base64url')
|
||||
|
||||
export function cookieName(authority) {
|
||||
return 'dsh-auth-' + b64u(crypto.createHash('sha256').update(authority).digest())
|
||||
}
|
||||
|
||||
export function mintCookie(authority, secretB64u, maxAgeDays = 1) {
|
||||
const secret = Buffer.from(String(secretB64u || ''), 'base64url')
|
||||
if (secret.length !== 32) return null
|
||||
const issuedAt = Date.now()
|
||||
const body = b64u(JSON.stringify({ version: 1, authority, issuedAt, expiresAt: issuedAt + maxAgeDays * DAY }))
|
||||
const sig = b64u(crypto.createHmac('sha256', secret).update(body).digest())
|
||||
return `${cookieName(authority)}=v1.${body}.${sig}`
|
||||
}
|
||||
|
||||
/** Read the browser-session signing secret through ctx.credentials (no file access). */
|
||||
export async function readBrowserSecret(ctx) {
|
||||
let creds
|
||||
try { creds = typeof ctx.get === 'function' ? ctx.get('credentials') : ctx.credentials } catch { creds = undefined }
|
||||
if (!creds || typeof creds.readRecord !== 'function') return ''
|
||||
try {
|
||||
const rec = await creds.readRecord('client-connection/browser-session')
|
||||
return rec?.payload?.secret || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** Small cache: re-mint a cookie per authority once an hour. */
|
||||
export class CookieJar {
|
||||
constructor(ctx) {
|
||||
this.ctx = ctx
|
||||
this.cache = new Map()
|
||||
this.secret = ''
|
||||
}
|
||||
|
||||
async get(authority) {
|
||||
const hit = this.cache.get(authority)
|
||||
if (hit && hit.at > Date.now() - 3600000) return hit.cookie
|
||||
if (!this.secret) this.secret = await readBrowserSecret(this.ctx)
|
||||
const cookie = mintCookie(authority, this.secret, 1)
|
||||
if (cookie) this.cache.set(authority, { cookie, at: Date.now() })
|
||||
return cookie
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user