OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / tcp-transport-auth.test.js

101 lines · javascript ref:
// 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)