// The loopback TCP transport (Windows) must be as closed as the 0600 Unix socket.
//
// On Linux the daemon listens on a Unix socket chmod'ed 0600, so only its owner
// can call vault.* / identity-manager.*. Windows' Godot cannot open a Unix
// socket (NetSocketWinSock::open rejects Family::UNIX), so there the daemon
// listens on 127.0.0.1 -- which ANY local process can reach. The per-launch
// token is what stands in for the file mode: a connection whose first frame is
// not {auth: <token>} must be closed without a single RPC being answered.
//
// Cases:
// 1. no auth frame, straight to an RPC -> no reply, connection closed
// 2. wrong token -> no reply, connection closed
// 3. right token, then an RPC -> the RPC is answered
// 4. the Unix socket (Linux) still needs no auth frame
//
// Run: bare test/tcp-transport-auth.test.js (EXIT 0 = pass, 1 = fail)
// Touches no daemon storage; the router is a stub.
import tcp from 'bare-tcp'
import Pipe from 'bare-pipe'
import FramedStream from 'framed-stream'
import b4a from 'b4a'
import { UdsTransport } from '../lib/uds-transport.js'
let failed = 0
function check (label, ok, detail = '') {
if (!ok) failed++
console.log(` ${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' [' + detail + ']' : ''}`)
}
const calls = []
const router = { handle: async (method) => { calls.push(method); return { ok: true, data: { echo: method } } } }
const token = 'k'.repeat(64)
const transport = new UdsTransport(router, {
// A throwaway path, so a transport that ignores `tcp` cannot touch the live socket.
socketPath: '/tmp/keet-tcp-auth-test-' + Bare.pid + '.sock',
tcp: { host: '127.0.0.1', port: 0, token }
})
await transport.start()
const port = transport.port
check('transport reports a bound loopback port', typeof port === 'number' && port > 0, String(port))
if (!port) { transport.stop(); Bare.exit(1) }
// Connect, send `frames`, and report what came back within 1 s.
function attempt (frames) {
return new Promise((resolve) => {
const sock = tcp.createConnection(port, '127.0.0.1')
const framed = new FramedStream(sock)
const got = []
let closed = false
framed.on('data', (buf) => got.push(JSON.parse(b4a.toString(buf, 'utf8'))))
// The server ending the connection reaches us as 'end' (our write side is still
// open, so 'close' alone would wait for us); either one means it hung up.
sock.on('end', () => { closed = true })
sock.on('close', () => { closed = true })
framed.on('error', () => {})
sock.on('connect', () => { for (const f of frames) framed.write(b4a.from(JSON.stringify(f), 'utf8')) })
setTimeout(() => { try { framed.destroy() } catch {} ; resolve({ got, closed }) }, 1000)
})
}
const rpc = { id: 1, method: 'status', params: {} }
const r1 = await attempt([rpc])
check('1. no auth: the RPC is not answered', r1.got.length === 0, JSON.stringify(r1.got))
check('1. no auth: the connection is closed', r1.closed)
const r2 = await attempt([{ auth: 'x'.repeat(64) }, rpc])
check('2. wrong token: the RPC is not answered', r2.got.length === 0, JSON.stringify(r2.got))
check('2. wrong token: the connection is closed', r2.closed)
check('the router saw nothing from unauthenticated clients', calls.length === 0, JSON.stringify(calls))
const r3 = await attempt([{ auth: token }, rpc])
const answer = r3.got.find((m) => m.id === 1)
check('3. right token: the RPC is answered', answer && answer.ok === true && answer.data.echo === 'status',
JSON.stringify(r3.got))
transport.stop()
// 4. The Unix socket is unchanged: no auth frame, the RPC is answered (its
// protection is the 0600 file mode, not a token).
const sockPath = '/tmp/keet-unix-noauth-test-' + Bare.pid + '.sock'
const unix = new UdsTransport(router, { socketPath: sockPath })
await unix.start()
const r4 = await new Promise((resolve) => {
const pipe = new Pipe(sockPath)
const framed = new FramedStream(pipe)
const got = []
framed.on('data', (buf) => got.push(JSON.parse(b4a.toString(buf, 'utf8'))))
framed.on('error', () => {})
pipe.on('connect', () => framed.write(b4a.from(JSON.stringify(rpc), 'utf8')))
setTimeout(() => { try { framed.destroy() } catch {} ; resolve(got) }, 1000)
})
check('4. unix socket: an RPC without auth is answered, as before',
r4.some((m) => m.id === 1 && m.ok === true), JSON.stringify(r4))
unix.stop()
console.log(failed === 0 ? 'RESULT: PASS' : `RESULT: FAIL (${failed})`)
Bare.exit(failed === 0 ? 0 : 1)