OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / start-blind-local.mjs

171 lines · javascript ref:
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { spawn } from 'node:child_process'

const children = new Set()
let closing = false
let temporaryRuntimeDir = null

function requiredUnsignedEnvironment (name, maximum = 0xffffffff, minimum = 0) {
  const value = process.env[name]
  if (typeof value !== 'string' || !/^(0|[1-9][0-9]*)$/.test(value)) {
    throw new Error(`${name} is required from the signed launch topology as an unsigned integer`)
  }
  const decoded = Number(value)
  if (!Number.isSafeInteger(decoded) || decoded < minimum || decoded > maximum) {
    throw new Error(`${name} is outside its signed launch-topology range`)
  }
  return decoded
}

function requiredTopologyHash () {
  const value = process.env.HIVERELAY_BLIND_LAUNCH_TOPOLOGY_HASH
  if (typeof value !== 'string' || !/^[0-9a-fA-F]{64}$/.test(value)) {
    throw new Error('HIVERELAY_BLIND_LAUNCH_TOPOLOGY_HASH must be the exact 32-byte signed topology hash in hex')
  }
  return value.toLowerCase()
}

function canonicalSocketPath (value, field) {
  if (typeof value !== 'string' || !path.isAbsolute(value) || value.includes('\0') || path.normalize(value) !== value) {
    throw new Error(`${field} must be a canonical absolute Unix socket path`)
  }
  if (Buffer.byteLength(value) > 100) throw new Error(`${field} exceeds the portable Unix socket path bound`)
  return value
}

function launch (entrypoint, env) {
  const child = spawn(process.execPath, [entrypoint], { env, stdio: 'inherit' })
  children.add(child)
  child.once('exit', () => children.delete(child))
  return child
}

function waitForExit (child) {
  if (child.exitCode != null || child.signalCode != null) return Promise.resolve()
  return new Promise(resolve => child.once('exit', resolve))
}

async function waitForSockets (child, unarySocketPath, streamSocketPath) {
  for (let attempt = 0; attempt < 200; attempt++) {
    if (child.exitCode != null || child.signalCode != null) {
      throw new Error(`blind daemon exited before readiness sockets were bound (${child.exitCode ?? child.signalCode})`)
    }
    try {
      const [unary, stream] = await Promise.all([
        fs.lstat(unarySocketPath),
        fs.lstat(streamSocketPath)
      ])
      if (!unary.isSocket() || !stream.isSocket()) throw new Error('blind daemon readiness paths are not both Unix sockets')
      if (unary.dev === stream.dev && unary.ino === stream.ino) throw new Error('blind daemon readiness paths resolve to one inode')
      if ((unary.mode & 0o777) !== 0o660 || (stream.mode & 0o777) !== 0o660) {
        throw new Error('blind daemon readiness sockets are not both mode 0660')
      }
      return
    } catch (error) {
      if (!error || (error.code !== 'ENOENT' && !String(error.message).includes('readiness'))) throw error
    }
    await new Promise(resolve => setTimeout(resolve, 25))
  }
  throw new Error('blind daemon did not create both private sockets within five seconds')
}

async function close (code = 0) {
  if (closing) return
  closing = true
  const running = [...children]
  for (const child of running) child.kill('SIGTERM')
  const exited = Promise.all(running.map(waitForExit))
  const grace = new Promise(resolve => setTimeout(resolve, 5000))
  await Promise.race([exited, grace])
  for (const child of running) {
    if (child.exitCode == null && child.signalCode == null) child.kill('SIGKILL')
  }
  await Promise.all(running.map(waitForExit))
  if (temporaryRuntimeDir) await fs.rm(temporaryRuntimeDir, { recursive: true, force: true })
  process.exit(code)
}

async function main () {
  const launchTopologyHash = requiredTopologyHash()
  const endpointId = requiredUnsignedEnvironment('HIVERELAY_BLIND_ENDPOINT_ID', 0xff, 1)
  if (typeof process.getuid !== 'function' || typeof process.getgid !== 'function') {
    throw new Error('the local two-process launcher requires POSIX peer credentials')
  }
  const localUid = process.getuid()
  const localGid = process.getgid()

  let runtimeDir = process.env.HIVERELAY_BLIND_RUNTIME_DIR
  if (!runtimeDir) {
    const created = await fs.mkdtemp(path.join(os.tmpdir(), 'hiverelay-blind-'))
    runtimeDir = await fs.realpath(created)
    temporaryRuntimeDir = runtimeDir
  }
  if (!path.isAbsolute(runtimeDir) || path.normalize(runtimeDir) !== runtimeDir || await fs.realpath(runtimeDir) !== runtimeDir) {
    throw new Error('HIVERELAY_BLIND_RUNTIME_DIR must be one canonical non-symlink absolute directory')
  }

  const unarySocketPath = canonicalSocketPath(
    process.env.HIVERELAY_BLIND_UNARY_SOCKET || path.join(runtimeDir, 'unary.sock'),
    'HIVERELAY_BLIND_UNARY_SOCKET'
  )
  const streamSocketPath = canonicalSocketPath(
    process.env.HIVERELAY_BLIND_STREAM_SOCKET || path.join(runtimeDir, 'stream.sock'),
    'HIVERELAY_BLIND_STREAM_SOCKET'
  )
  if (unarySocketPath === streamSocketPath) throw new Error('blind unary and stream socket paths must be unequal')

  // Both development children necessarily run as the invoking local account.
  // Production uses the image-frozen unequal UIDs in docker-compose.blind.yml.
  const daemonUid = process.env.HIVERELAY_BLIND_DAEMON_UID == null
    ? localUid
    : requiredUnsignedEnvironment('HIVERELAY_BLIND_DAEMON_UID')
  const daemonGid = process.env.HIVERELAY_BLIND_DAEMON_GID == null
    ? localGid
    : requiredUnsignedEnvironment('HIVERELAY_BLIND_DAEMON_GID')
  const sharedGid = process.env.HIVERELAY_BLIND_SHARED_GID == null
    ? localGid
    : requiredUnsignedEnvironment('HIVERELAY_BLIND_SHARED_GID')
  if (daemonUid !== localUid || daemonGid !== localGid || sharedGid !== localGid) {
    throw new Error('local launcher UID/GID topology must match the invoking POSIX account; use packaged services for unequal production identities')
  }

  const env = {
    ...process.env,
    HIVERELAY_BLIND_RUNTIME_DIR: runtimeDir,
    HIVERELAY_BLIND_UNARY_SOCKET: unarySocketPath,
    HIVERELAY_BLIND_STREAM_SOCKET: streamSocketPath,
    HIVERELAY_BLIND_LAUNCH_TOPOLOGY_HASH: launchTopologyHash,
    HIVERELAY_BLIND_ENDPOINT_ID: String(endpointId),
    HIVERELAY_BLIND_ENDPOINT_IDS: String(endpointId),
    HIVERELAY_BLIND_EDGE_UID: String(localUid),
    HIVERELAY_BLIND_DAEMON_UID: String(daemonUid),
    HIVERELAY_BLIND_DAEMON_GID: String(daemonGid),
    HIVERELAY_BLIND_SHARED_GID: String(sharedGid)
  }

  const daemon = launch('packages/blind-daemon/cli.js', env)
  try {
    await waitForSockets(daemon, unarySocketPath, streamSocketPath)
  } catch (error) {
    process.stderr.write(`[blind-launcher] ${error.message}\n`)
    await close(1)
    return
  }
  const edge = launch('packages/blind-edge/cli.js', env)

  for (const child of [daemon, edge]) {
    child.once('exit', code => {
      if (!closing) close(code === 0 ? 1 : (code || 1))
    })
  }
  process.once('SIGINT', () => close(0))
  process.once('SIGTERM', () => close(0))
}

main().catch(error => {
  process.stderr.write(`[blind-launcher] ${error.message}\n`)
  close(1)
})