OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge / lib / server.js

68 lines · javascript ref:
// server.js — the bare-http1 adapter. Deliberately thin: it reads the request, calls the router,
// writes the response. All decisions live in router.js, which is testable without a socket.

import http from 'bare-http1'
import { handleRequest } from './router.js'

const MAX_BODY = 1024 * 1024 // an inbox POST is a small JSON document; anything larger is abuse

export function createServer (ctx) {
  return http.createServer(async (req, res) => {
    let out
    try {
      const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`)
      const body = await readBody(req)
      if (body === TOO_LARGE) {
        res.writeHead(413, { 'content-type': 'text/plain' })
        return res.end('payload too large')
      }

      out = await handleRequest({
        method: req.method,
        path: url.pathname,
        query: Object.fromEntries(url.searchParams),
        headers: req.headers,
        body
      }, ctx)
    } catch (err) {
      // A crash here would take the daemon down on a malformed request from anyone on the
      // internet. Log it and return 500 — never let an inbound request kill the process.
      ctx.log?.({ msg: 'unhandled error', error: err.message, stack: err.stack })
      out = { status: 500, headers: { 'content-type': 'text/plain' }, body: 'internal error' }
    }

    // ⛔ LOG WHAT THE FEDIVERSE ASKS US FOR. There was no request log at all, so "prsm.space accepted
    // our activity and shows nothing" had two indistinguishable explanations — it never dereferenced
    // our actor or objects, or it did and got something wrong — and no way to tell them apart from
    // this side. Method, path, status and user-agent, one line; the same reasoning as the mirror's
    // trigger counters and the drain's empty-pass line.
    //
    // ⚠ Deliberately NOT the body, and not headers: an inbox POST carries a signature and a private
    // message could carry content. What is logged is what a normal access log holds.
    try {
      const ua = String(req.headers['user-agent'] || '')
      ctx.log?.({ msg: 'req', method: req.method, path: new URL(req.url, 'http://x').pathname, status: out.status, ua: ua.slice(0, 80) })
    } catch {}

    res.writeHead(out.status, out.headers || {})
    res.end(out.body || '')
  })
}

const TOO_LARGE = Symbol('too-large')

function readBody (req) {
  return new Promise((resolve, reject) => {
    if (req.method === 'GET' || req.method === 'HEAD') return resolve(null)
    const chunks = []
    let size = 0
    req.on('data', (c) => {
      size += c.length
      if (size > MAX_BODY) { resolve(TOO_LARGE); req.destroy(); return }
      chunks.push(c)
    })
    req.on('end', () => resolve(chunks.length ? Buffer.concat(chunks).toString('utf8') : null))
    req.on('error', reject)
  })
}