OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / shard-http-adapter.test.js

93 lines · javascript ref:
import test from 'brittle'
import { EventEmitter } from 'node:events'
import { Readable } from 'node:stream'
import {
  handleShardHttp,
  resolveShardRoute,
  createShardHttpState
} from 'p2p-hiveservices/builtin/shard-store/http-adapter.js'

const HASH = 'a'.repeat(64)

// The rate-limit gate runs before any service call, so a minimal fake service
// that reports "not held" is enough to exercise it: allowed requests fall
// through to 404, throttled ones short-circuit to 429.
const fakeService = { async has () { return { present: false, byteLength: 0 } } }

function fakeReq (method, url, headers = {}) {
  const req = Readable.from([])
  req.method = method
  req.url = url
  req.headers = { ...headers }
  req.socket = { remoteAddress: headers.remoteAddress || '203.0.113.7' }
  return req
}

function fakeRes () {
  const res = new EventEmitter()
  res.statusCode = null
  res.body = ''
  res.ended = false
  res.writeHead = function writeHead (status) { this.statusCode = status; return this }
  res.end = function end (body = '') { this.body = String(body); this.ended = true; return this }
  res.write = function write () { return true }
  return res
}

test('shard http adapter: enforces per-IP rate limiting (v0.21.1 audit)', async (t) => {
  const state = createShardHttpState()
  const rateLimit = { windowMs: 60000, max: 3 }
  const route = resolveShardRoute('HEAD', '/api/v1/shard/' + HASH)
  t.ok(route && route.kind === 'head', 'HEAD route resolves')

  const codes = []
  for (let i = 0; i < 5; i++) {
    const res = fakeRes()
    await handleShardHttp(fakeService, route, fakeReq('HEAD', '/api/v1/shard/' + HASH), res, { state, rateLimit })
    codes.push(res.statusCode)
  }
  // First `max` requests reach the service (404 = not held); the rest are 429.
  t.alike(codes, [404, 404, 404, 429, 429], 'requests past the cap are throttled')
})

test('shard http adapter: rate-limit buckets are keyed per-IP', async (t) => {
  const state = createShardHttpState()
  const rateLimit = { windowMs: 60000, max: 1 }
  const route = resolveShardRoute('HEAD', '/api/v1/shard/' + HASH)

  const a1 = fakeRes()
  await handleShardHttp(fakeService, route, fakeReq('HEAD', '/h', { remoteAddress: '198.51.100.1' }), a1, { state, rateLimit })
  const a2 = fakeRes()
  await handleShardHttp(fakeService, route, fakeReq('HEAD', '/h', { remoteAddress: '198.51.100.1' }), a2, { state, rateLimit })
  const b1 = fakeRes()
  await handleShardHttp(fakeService, route, fakeReq('HEAD', '/h', { remoteAddress: '198.51.100.2' }), b1, { state, rateLimit })

  t.is(a1.statusCode, 404, 'first request from IP A is allowed')
  t.is(a2.statusCode, 429, 'second request from IP A is throttled')
  t.is(b1.statusCode, 404, 'IP B has its own bucket')
})

test('shard http adapter: rate limiting honors X-Forwarded-For only under trustProxy', async (t) => {
  const state = createShardHttpState()
  const rateLimit = { windowMs: 60000, max: 1 }
  const route = resolveShardRoute('HEAD', '/api/v1/shard/' + HASH)

  // Same socket IP, different XFF. Without trustProxy the XFF is ignored, so
  // both count against the socket bucket (second is throttled).
  const r1 = fakeRes()
  await handleShardHttp(fakeService, route, fakeReq('HEAD', '/h', { remoteAddress: '10.0.0.1', 'x-forwarded-for': '1.1.1.1' }), r1, { state, rateLimit })
  const r2 = fakeRes()
  await handleShardHttp(fakeService, route, fakeReq('HEAD', '/h', { remoteAddress: '10.0.0.1', 'x-forwarded-for': '2.2.2.2' }), r2, { state, rateLimit })
  t.is(r1.statusCode, 404)
  t.is(r2.statusCode, 429, 'XFF ignored without trustProxy')

  // With trustProxy the XFF is the bucket key, so distinct XFFs are separate.
  const p1state = createShardHttpState()
  const p1 = fakeRes()
  await handleShardHttp(fakeService, route, fakeReq('HEAD', '/h', { remoteAddress: '10.0.0.1', 'x-forwarded-for': '1.1.1.1' }), p1, { state: p1state, rateLimit, trustProxy: true })
  const p2 = fakeRes()
  await handleShardHttp(fakeService, route, fakeReq('HEAD', '/h', { remoteAddress: '10.0.0.1', 'x-forwarded-for': '2.2.2.2' }), p2, { state: p1state, rateLimit, trustProxy: true })
  t.is(p1.statusCode, 404)
  t.is(p2.statusCode, 404, 'distinct XFFs are separate buckets under trustProxy')
})