Files
MyIP/tests/guards.test.js
T
jason5ng32andClaude Fable 5.1 d5715104d5 Fix(asn): never cache an upstream fault as an empty answer
Review findings on the ASN Profile work, mostly one class of bug: a failed
upstream read as "no data", which the 7-day edge cache would then pin.

- AS Rank: a 200 carrying GraphQL errors is a failure, not an unknown AS.
- Radar: an answer built from partly failed segments is still shown but
  flagged `incomplete` and kept out of the cache, on the aggregate and on
  the standalone asn view. A 404 is Radar's "no such AS" and counts as
  empty, as does a segment that simply has no figures.
- Reputation: a malformed or non-200 answer is an error.
- Prefixes: a payload without `prefix_origins` is an error; [] stays valid.
- Radar is `empty` when only local CAIDA counts are present.
- requireValidASN enforces AS1–AS4294967295 and canonicalizes; the range
  rule lives once in common/asn-input.js, shared with the frontend.
- The PeeringDB listing lookup goes through fetchUpstream.
- New functions follow the const-arrow convention; test stubs compare
  parsed hostnames instead of URL substrings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 23:43:55 +08:00

410 lines
16 KiB
JavaScript

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { requireReferer, requirePublicIP, requireValidPrefix, requireValidDomain, requireValidProviderId, requireValidRecordType, requireValidReportId, requireValidCountry, requireValidASN, normalizeAsnQuery } from '../common/guards.js';
// Minimal (req, res, next) stubs — just enough to observe what the
// middleware does.
function makeReq({ referer, query = {} } = {}) {
return {
headers: referer ? { referer } : {},
query,
};
}
function makeRes() {
const res = {
statusCode: null,
body: null,
status(code) { this.statusCode = code; return this; },
json(payload) { this.body = payload; return this; },
};
return res;
}
describe('requireReferer', () => {
it('calls next() when the referer is on the allow-list (localhost)', () => {
let nextCalled = false;
const req = makeReq({ referer: 'http://localhost/foo' });
requireReferer(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('returns 403 "What are you doing?" when no referer header', () => {
const res = makeRes();
let nextCalled = false;
requireReferer(makeReq(), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 403);
assert.equal(res.body.error, 'What are you doing?');
assert.equal(nextCalled, false);
});
it('returns 403 "Access denied" when referer is not on the allow-list', () => {
const res = makeRes();
let nextCalled = false;
requireReferer(makeReq({ referer: 'http://evil.example/x' }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 403);
assert.equal(res.body.error, 'Access denied');
assert.equal(nextCalled, false);
});
});
describe('requirePublicIP', () => {
const guard = requirePublicIP();
it('calls next() when ip query param is a valid IPv4', () => {
let nextCalled = false;
guard(makeReq({ query: { ip: '1.1.1.1' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('calls next() when ip query param is a valid IPv6', () => {
let nextCalled = false;
guard(makeReq({ query: { ip: '2001:4860:4860::8888' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('returns 400 "No IP address provided" when ip is missing', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: {} }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'No IP address provided');
assert.equal(nextCalled, false);
});
it('returns 400 "Invalid IP address" when ip is malformed', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { ip: 'nope' } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'Invalid IP address');
assert.equal(nextCalled, false);
});
it('rejects leading-zero IPv4 input before reaching an upstream handler', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { ip: '8.8.8.08' } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'Invalid IP address');
assert.equal(nextCalled, false);
});
// Reserved space is well-formed but unanswerable by any upstream, so it
// gets its own message — rejected before the handler, never fetched.
for (const ip of ['10.0.0.1', '192.168.1.1', '127.0.0.1', '198.18.0.2', 'fd00::1', '::1']) {
it(`returns 400 "Not a public IP address" for ${ip}`, () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { ip } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'Not a public IP address');
assert.equal(nextCalled, false);
});
}
it('honors a custom param name', () => {
const custom = requirePublicIP('target');
let nextCalled = false;
custom(makeReq({ query: { target: '8.8.8.8' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
});
describe('requireValidPrefix', () => {
const guard = requireValidPrefix();
it('calls next() when prefix is a valid IPv4 CIDR', () => {
let nextCalled = false;
guard(makeReq({ query: { prefix: '8.8.8.0/24' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('calls next() when prefix is a valid IPv6 CIDR', () => {
let nextCalled = false;
guard(makeReq({ query: { prefix: '2001:4860:4860::/48' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('returns 400 "No prefix provided" when prefix is missing', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: {} }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'No prefix provided');
assert.equal(nextCalled, false);
});
it('returns 400 "Invalid prefix" when prefix is malformed', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { prefix: '8.8.8.0' } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'Invalid prefix');
assert.equal(nextCalled, false);
});
it('returns 400 when prefix length is out of v4 range', () => {
const res = makeRes();
guard(makeReq({ query: { prefix: '8.8.8.0/33' } }), res, () => {});
assert.equal(res.statusCode, 400);
});
});
describe('requireValidCountry', () => {
const guard = requireValidCountry();
it('calls next() and uppercases the code in place', () => {
let nextCalled = false;
const req = makeReq({ query: { country: 'iq' } });
guard(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
assert.equal(req.query.country, 'IQ');
});
it('returns 400 "No country provided" when country is missing', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: {} }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'No country provided');
assert.equal(nextCalled, false);
});
it('returns 400 "Invalid country" for anything but two letters', () => {
for (const bad of ['USA', 'U', 'U1', 'us,de', 'ÜS']) {
const res = makeRes();
guard(makeReq({ query: { country: bad } }), res, () => {});
assert.equal(res.statusCode, 400, bad);
assert.equal(res.body.error, 'Invalid country', bad);
}
});
});
describe('requireValidProviderId', () => {
const guard = requireValidProviderId();
it('calls next() for a whitelisted provider id', () => {
let nextCalled = false;
guard(makeReq({ query: { id: 'claude' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('returns 400 "No provider id provided" when id is missing', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: {} }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'No provider id provided');
assert.equal(nextCalled, false);
});
it('returns 400 "Invalid provider id" for an unknown id', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { id: 'not-a-provider' } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'Invalid provider id');
assert.equal(nextCalled, false);
});
});
describe('requireValidReportId', () => {
const guard = requireValidReportId();
// A report id is 16 random bytes as base64url — always exactly 22 chars.
const validId = 'AbC123-_xyz987654321ab';
// This guard reads req.params (route param), not req.query.
const makeParamsReq = (params) => ({ headers: {}, params });
it('calls next() for a well-formed 22-char base64url id', () => {
let nextCalled = false;
guard(makeParamsReq({ id: validId }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('returns 400 when the id is missing', () => {
const res = makeRes();
let nextCalled = false;
guard(makeParamsReq({}), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'Invalid report id');
assert.equal(nextCalled, false);
});
it('returns 400 for wrong length or non-base64url characters', () => {
for (const bad of ['short', 'a'.repeat(23), 'a'.repeat(21), `${'a'.repeat(21)}+`, `${'a'.repeat(21)}/`]) {
const res = makeRes();
let nextCalled = false;
guard(makeParamsReq({ id: bad }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400, `should reject "${bad}"`);
assert.equal(nextCalled, false);
}
});
});
describe('requireValidRecordType', () => {
const guard = requireValidRecordType();
it('calls next() for a supported type', () => {
let nextCalled = false;
guard(makeReq({ query: { type: 'CAA' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('uppercases the type in place so the handler switch matches', () => {
const req = makeReq({ query: { type: 'caa' } });
guard(req, makeRes(), () => {});
assert.equal(req.query.type, 'CAA');
});
it('rejects a missing type', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: {} }), res, () => { nextCalled = true; });
assert.equal(nextCalled, false);
assert.equal(res.statusCode, 400);
assert.deepEqual(res.body, { error: 'No record type provided' });
});
it('rejects a type the resolver does not handle, so DoH never forwards it', () => {
for (const type of ['ANY', 'DNSKEY', 'HTTPS', 'PTR', '../etc']) {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { type } }), res, () => { nextCalled = true; });
assert.equal(nextCalled, false, type);
assert.equal(res.statusCode, 400);
assert.deepEqual(res.body, { error: 'Invalid record type' });
}
});
});
describe('requireValidDomain', () => {
const guard = requireValidDomain();
it('calls next() for a valid domain', () => {
let nextCalled = false;
guard(makeReq({ query: { domain: 'example.com' } }), makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
});
it('lowercases the domain in place (canonical cache key)', () => {
const req = makeReq({ query: { domain: 'WWW.Example.COM' } });
let nextCalled = false;
guard(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
assert.equal(req.query.domain, 'www.example.com');
});
it('supports a custom query parameter name', () => {
const hostnameGuard = requireValidDomain('hostname');
const req = makeReq({ query: { hostname: 'WWW.Example.COM' } });
let nextCalled = false;
hostnameGuard(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
assert.equal(req.query.hostname, 'www.example.com');
});
it('returns 400 when the domain is missing', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: {} }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'No domain provided');
assert.equal(nextCalled, false);
});
it('returns 400 for non-domain inputs', () => {
for (const bad of ['not a domain', 'nodot', '1.2.3.4.5.', 'http://example.com', 'exa_mple.com']) {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { domain: bad } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400, `should reject "${bad}"`);
assert.equal(nextCalled, false);
}
});
});
describe('requireValidASN', () => {
const guard = requireValidASN();
it('canonicalizes to the bare number string in place', () => {
for (const [input, expected] of [['13335', '13335'], ['AS13335', '13335'], ['as13335', '13335'],
['AS013335', '13335'], ['0013335', '13335'], [' 13335 ', '13335'], ['1', '1'],
['4294967295', '4294967295'], ['AS4294967295', '4294967295']]) {
const req = makeReq({ query: { asn: input } });
let nextCalled = false;
guard(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true, input);
assert.equal(req.query.asn, expected, input);
}
});
it('returns 400 "No ASN provided" when the param is missing', () => {
const res = makeRes();
let nextCalled = false;
guard(makeReq(), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400);
assert.equal(res.body.error, 'No ASN provided');
assert.equal(nextCalled, false);
});
it('returns 400 for AS0, values past 32 bits, overlong digit strings and non-ASNs', () => {
for (const bad of ['0', 'AS0', '000', '4294967296', 'AS99999999999', '00000000013335', '9'.repeat(400),
'ASN13335', '13335a', '-1', '1.5', 'AS 13335', 'not-an-asn', ['1', '2']]) {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { asn: bad } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400, `should reject "${bad}"`);
assert.equal(res.body.error, 'Invalid ASN');
assert.equal(nextCalled, false);
}
});
it('honours a custom param name', () => {
const req = makeReq({ query: { origin: 'AS64500' } });
let nextCalled = false;
requireValidASN('origin')(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
assert.equal(req.query.origin, '64500');
});
});
describe('normalizeAsnQuery', () => {
const guard = normalizeAsnQuery();
it('canonicalizes ASN-shaped values to AS<n> in place', () => {
for (const [input, expected] of [['AS13335', 'AS13335'], ['as13335', 'AS13335'], ['As13335', 'AS13335'],
['AS013335', 'AS13335'], [' AS13335 ', 'AS13335'], ['AS4294967295', 'AS4294967295']]) {
const req = makeReq({ query: { q: input } });
let nextCalled = false;
guard(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true, input);
assert.equal(req.query.q, expected, input);
}
});
it('returns 400 for AS0 and values past 32 bits', () => {
for (const bad of ['AS0', 'as0', 'AS4294967296', 'AS99999999999']) {
const res = makeRes();
let nextCalled = false;
guard(makeReq({ query: { q: bad } }), res, () => { nextCalled = true; });
assert.equal(res.statusCode, 400, `should reject "${bad}"`);
assert.equal(res.body.error, 'Invalid ASN');
assert.equal(nextCalled, false);
}
});
it('passes IPs, domains, bare numbers, missing and array values through untouched', () => {
for (const query of [{ q: '1.1.1.1' }, { q: 'example.com' }, { q: 'ASN13335' }, {}, { q: ['AS1', 'AS2'] },
{ q: '13335' }, { q: '0' }, { q: '99999999999' }]) {
const req = makeReq({ query: { ...query } });
let nextCalled = false;
guard(req, makeRes(), () => { nextCalled = true; });
assert.equal(nextCalled, true);
assert.deepEqual(req.query, query);
}
});
});