Files
dsh-remote/deploy/e2e-signin.mjs
T
lyuksovannyy bfbc3c66e4 dsh-remote 0.5.0: node settings, session-cookie edge auth, sign-in popup
- Settings → Remote devices → Node: type master|slave (stored in state, beats
  config role; Restart DSH button under systemd/launchd), origin (read-only:
  own origin / paired master), 'Master has an auth middleware' toggle
- middleware on: a non-DSH answer on the link (login redirect, 401/403, inline
  HTML) raises auth-required; the UI opens <master>/dsh-remote/signin in a popup,
  the user signs in to the proxy, one click hands the proxy session cookie back
  (postMessage, origin-checked, trusted origins only), link retries
- edge mode 'cookie': pasted authentik_proxy_… (or a whole Cookie header),
  rotated proxy cookies persisted back; pair.mjs/install-mac --edge cookie
- connectMaster serialised; a failed attempt never clears a finished link
- tests: cookie seed/mode, HTML login wall; e2e-signin against a fake proxy
2026-10-12 09:37:21 +00:00

93 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
// E2E for the "auth middleware" flow, run on a slave host against a real master:
// node deploy/e2e-signin.mjs <masterHttpUrl> <pairingCode> <chromeBinary>
// 1. start fake-edge (Authentik-like, user svc / password s3cret) in front of the master
// 2. start a throw-away slave DSH (own DSH_HOME, port 3085) and pair it THROUGH the edge
// using the pasted-cookie mode
// 3. kill the edge session (edge restarts → cookie invalid) and check the slave reports
// authRequired with authMiddleware on, and nothing with it off
// 4. open the slave UI in headless Chromium, trigger the popup flow by calling the
// hand-over (postMessage) the master's /dsh-remote/signin page would send
// 5. verify the link is back up
import { spawn, execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const [master, code, chrome] = process.argv.slice(2)
const here = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(here, '..')
const H = fs.mkdtempSync(path.join(os.tmpdir(), 'dshr-signin-'))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const log = (...a) => console.log(...a)
const procs = []
const stop = () => { for (const p of procs) { try { p.kill('SIGKILL') } catch { /* gone */ } } }
process.on('exit', stop)
const startEdge = () => {
const p = spawn('node', [path.join(here, 'fake-edge.mjs'), '3098', master, 'svc', 's3cret'], { stdio: 'ignore' })
procs.push(p)
return p
}
const cli = (args, env = {}) => execFileSync('node', [path.join(here, 'dshr-cli.mjs'), ...args], { env: { ...process.env, DSH_HOME: H, DSH_PORT: '3085', ...env }, encoding: 'utf8', timeout: 30000 })
let edge = startEdge()
await sleep(800)
// A browser that already signed in would own this cookie: get one from the fake edge.
const r = await fetch('http://127.0.0.1:3098/dsh-remote/enroll', { method: 'POST', headers: { authorization: `Basic ${Buffer.from('svc:s3cret').toString('base64')}` } })
const edgeCookieOf = (resp) => (resp.headers.getSetCookie?.() || []).map((c) => c.split(';')[0]).find((c) => c.startsWith('edge_session=')) || ''
const cookie = edgeCookieOf(r)
log('browser-style edge cookie:', cookie.replace(/=.*/, '=…'))
// Throw-away slave profile.
const env = { ...process.env, DSH_HOME: H, PATH: process.env.PATH }
execFileSync('dsh', ['web', '--dump-config'], { env, stdio: 'ignore', timeout: 40000 })
fs.mkdirSync(path.join(H, 'plugins'), { recursive: true })
fs.cpSync(root, path.join(H, 'plugins', 'dsh-remote'), { recursive: true })
execFileSync('node', [path.join(H, 'plugins', 'dsh-remote', 'deploy', 'install-local.mjs'), '--role', 'slave', '--name', 'signin-test'], { env })
log('pair through the edge with the pasted cookie:')
process.env.DSH_REMOTE_EDGE_COOKIE = cookie
log(execFileSync('node', [path.join(here, 'pair.mjs'), '--master', 'http://127.0.0.1:3098', '--code', code, '--name', 'signin-test', '--edge', 'cookie'], { env: { ...env, DSH_REMOTE_EDGE_COOKIE: cookie }, encoding: 'utf8' }).trim())
const dsh = spawn('dsh', ['web', '--host', '127.0.0.1', '--port', '3085', '--no-open'], { env, stdio: 'ignore' })
procs.push(dsh)
for (let i = 0; i < 60; i++) { try { if ((await fetch('http://127.0.0.1:3085/')).status) break } catch { /* booting */ } await sleep(1000) }
await sleep(6000)
const state = () => JSON.parse(cli(['GET', 'state']).split('\n').slice(1).join('\n'))
let s = state()
log('linked with pasted cookie:', s.master.linked, '| cookies:', s.master.edgeAuth.cookies.join(','))
log('--- edge forgets the session (restart) → authRequired when authMiddleware is on')
cli(['POST', 'node', '{"authMiddleware":true}'])
edge.kill('SIGKILL'); await sleep(500); edge = startEdge(); await sleep(800)
cli(['POST', 'retry-link', '{}'])
await sleep(500)
s = state()
log('linked:', s.master.linked, '| authRequired:', !!s.master.authRequired, '|', s.master.authRequired?.reason?.slice(0, 90))
log('--- same with authMiddleware off → no authRequired')
cli(['POST', 'node', '{"authMiddleware":false}'])
cli(['POST', 'retry-link', '{}'])
s = state()
log('authRequired:', !!s.master.authRequired, '| error:', s.master.error?.slice(0, 80))
log('--- popup hand-over: the master /dsh-remote/signin page postMessages the cookie')
cli(['POST', 'node', '{"authMiddleware":true}'])
const fresh = await fetch('http://127.0.0.1:3098/dsh-remote/enroll', { method: 'POST', headers: { authorization: `Basic ${Buffer.from('svc:s3cret').toString('base64')}` } })
const freshCookie = edgeCookieOf(fresh)
const cookieText = `${freshCookie}; dsh-auth-xyz=ignored`
const sign = await fetch('http://127.0.0.1:3098/dsh-remote/signin?origin=' + encodeURIComponent('http://127.0.0.1:3085'), { headers: { cookie: freshCookie } })
const page = await sign.text()
log('signin page served through the edge:', sign.status, '| offers hand-over:', /Send session/.test(page), '| cookie embedded:', page.includes(freshCookie.split('=')[0]))
const untrusted = await (await fetch('http://127.0.0.1:3098/dsh-remote/signin?origin=' + encodeURIComponent('https://evil.example'), { headers: { cookie: freshCookie } })).text()
log('untrusted origin gets nothing:', /not on this machine's trusted list/.test(untrusted) && !untrusted.includes(freshCookie.split('=')[1]))
// What the UI does when the postMessage arrives:
cli(['POST', 'edge-auth', JSON.stringify({ edgeAuth: { mode: 'cookie', cookie: cookieText } })])
const retry = JSON.parse(cli(['POST', 'retry-link', '{}']).split('\n').slice(1).join('\n'))
await sleep(1500)
s = state()
log('after hand-over → linked:', s.master.linked, '| retry-link:', JSON.stringify(retry).slice(0, 100))
process.exit(s.master.linked ? 0 : 1)