Files
MyIP/common/geo-handler.js
T
jason5ng32andClaude Opus 5 6c679b20a1 Feat(guards): require a publicly routable IP on every ?ip= route
The seven geo routes let a reserved address through to their upstream,
which could only answer "no data" — a visitor querying their own LAN IP
cost a real API call at every source. requireValidIP tightens into
requirePublicIP and rejects that address before the handler runs, so
geo-handler, maxmind and ipcheck-ing need no check of their own; the
guard was already their sole validation layer.

Malformed input and reserved space keep distinct messages — the two
failures point at different fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:30:22 +08:00

47 lines
2.1 KiB
JavaScript

// Factory for IP-geolocation source handlers.
//
// Every geo source (ipinfo.io, ip-api.com, ipapi.is, ip2location.io, ip.sb)
// shares an identical Express shell: read the (already-validated) ?ip,
// build a source-specific URL, fetch it through fetchUpstream, normalize the
// upstream JSON into the canonical response shape, and respond — with a
// uniform try/catch that logs the error and returns a 500.
//
// makeGeoHandler captures that shell. Each source supplies only:
// - name : short id used in the error log message
// - buildUrl : (req) => string URL OR { url, logContext } when the
// handler wants extra fields (e.g. lang) on the error log
// - normalize : (json) => canonical response object
//
// buildUrl runs before the try block so any env-key selection it performs
// keeps its current behavior (e.g. throwing on a missing key surfaces the
// same way the original inline handlers did).
import { fetchUpstream } from './fetch-with-timeout.js';
import logger from './logger.js';
export function makeGeoHandler({ name, buildUrl, normalize }) {
return async (req, res) => {
// Presence, validity and public routability guaranteed by the
// requirePublicIP middleware — a reserved address never reaches here.
const ipAddress = req.query.ip;
const built = buildUrl(req);
const url = typeof built === 'string' ? built : built.url;
const logContext = typeof built === 'string' ? {} : (built.logContext || {});
try {
const apiRes = await fetchUpstream(url);
// Outage / gateway pages come back as HTML — fail on status
// instead of letting JSON.parse throw on "<html>".
if (!apiRes.ok) {
throw new Error(`Upstream responded ${apiRes.status}`);
}
const json = await apiRes.json();
res.json(normalize(json));
} catch (e) {
logger.error({ err: e, ip: ipAddress, ...logContext }, `${name} handler failed`);
res.status(500).json({ error: e.message });
}
};
}