Files
MyIP/common/guards.js
T
jason5ng32andClaude Fable 5 0a0a739edd Style: fix a misplaced guard comment and two quoting nits
The provider-id comment block sat above requireValidRecordType; move it
back over requireValidProviderId. Also drop needless quote escapes in
the resolver-check script and match the ui/ double-quote style in
ButtonGroupSeparator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 18:21:38 +08:00

132 lines
5.4 KiB
JavaScript

// Express middleware that factors out the boilerplate every api/ handler
// used to repeat at the top of its function body: referer check, param
// presence check, param validity check. Mount once in backend-server.js;
// handlers stop carrying the defensive checks and can't accidentally forget
// them.
import { refererCheck } from './referer-check.js';
import { isValidIP, isValidDomain, isUsablePublicIP } from './valid-ip.js';
import { isValidBgpPrefix } from './bgp-prefix.js';
import { STATUS_PROVIDER_IDS } from './service-status-providers.js';
import { DNS_RECORD_TYPE_SET } from './dns-record-types.js';
// Reject requests without an allowed referer. The error message variant
// preserves the existing user-facing wording.
export const requireReferer = (req, res, next) => {
const referer = req.headers.referer;
if (!refererCheck(referer)) {
return res.status(403).json({
error: referer ? 'Access denied' : 'What are you doing?',
});
}
next();
};
// Reject requests whose IP query param isn't a publicly routable address.
// Every `?ip=` route asks an external service about that address — a
// registry, a geolocation source — and reserved space has no answer there,
// so it never reaches an upstream call. Malformed input and reserved space
// get distinct messages; the two failures need different fixes.
// Factory so a route using another param name (none today, but leaving the
// door open) can say `requirePublicIP('target')`.
export const requirePublicIP = (paramName = 'ip') => (req, res, next) => {
const ip = req.query[paramName];
if (!ip) {
return res.status(400).json({ error: 'No IP address provided' });
}
if (!isValidIP(ip)) {
return res.status(400).json({ error: 'Invalid IP address' });
}
if (!isUsablePublicIP(ip)) {
return res.status(400).json({ error: 'Not a public IP address' });
}
next();
};
// Reject requests without a syntactically valid domain in the specified
// query param. Lowercases the value in place so handlers and the edge cache
// see one canonical form (domains are case-insensitive; a mixed-case query
// must not become a distinct CF cache entry).
export const requireValidDomain = (paramName = 'domain') => (req, res, next) => {
const raw = req.query[paramName];
if (!raw) {
return res.status(400).json({ error: 'No domain provided' });
}
const domain = String(raw).toLowerCase();
if (!isValidDomain(domain)) {
return res.status(400).json({ error: 'Invalid domain' });
}
req.query[paramName] = domain;
next();
};
// Reject requests without a valid CIDR prefix in the specified query param.
// Accepts any well-formed CIDR — the quantization policy (e.g. /24 for v4,
// /48 for v6) is the frontend's job, not the guard's.
export const requireValidPrefix = (paramName = 'prefix') => (req, res, next) => {
const prefix = req.query[paramName];
if (!prefix) {
return res.status(400).json({ error: 'No prefix provided' });
}
if (!isValidBgpPrefix(prefix)) {
return res.status(400).json({ error: 'Invalid prefix' });
}
next();
};
// Reject requests without a valid ASN (numeric, with optional 'AS' prefix).
// Used by /api/asn-connectivity; other ASN-taking handlers (cf-radar) still
// validate inline for historical reasons.
export const requireValidASN = (paramName = 'asn') => (req, res, next) => {
const raw = req.query[paramName];
if (!raw) {
return res.status(400).json({ error: 'No ASN provided' });
}
const numeric = String(raw).replace(/^AS/i, '');
if (!/^[0-9]+$/.test(numeric)) {
return res.status(400).json({ error: 'Invalid ASN' });
}
req.query[paramName] = numeric;
next();
};
// Reject requests whose report id doesn't look like one we issued: 16 random
// bytes as base64url, always exactly 22 chars. Reads the ROUTE param (not the
// query) — the share-report read endpoint is /api/report/:id.
export const requireValidReportId = (paramName = 'id') => (req, res, next) => {
const id = req.params?.[paramName];
if (!id || !/^[A-Za-z0-9_-]{22}$/.test(id)) {
return res.status(400).json({ error: 'Invalid report id' });
}
next();
};
// Whitelist ?type= against the record types the resolver actually handles.
// Without it the DoH branch forwards any string verbatim to four third-party
// endpoints, which makes this route a query proxy for types we never support.
export const requireValidRecordType = (paramName = 'type') => (req, res, next) => {
const type = req.query[paramName];
if (!type) {
return res.status(400).json({ error: 'No record type provided' });
}
if (!DNS_RECORD_TYPE_SET.has(String(type).toUpperCase())) {
return res.status(400).json({ error: 'Invalid record type' });
}
req.query[paramName] = String(type).toUpperCase();
next();
};
// Reject requests whose `id` isn't a known service-status provider slug.
// Used by the per-provider components / incidents endpoints, which select a
// row from the in-memory snapshot by id.
export const requireValidProviderId = (paramName = 'id') => (req, res, next) => {
const id = req.query[paramName];
if (!id) {
return res.status(400).json({ error: 'No provider id provided' });
}
if (!STATUS_PROVIDER_IDS.has(id)) {
return res.status(400).json({ error: 'Invalid provider id' });
}
next();
};