Files
MyIP/api/ip-blocklist.js
jason5ng32andClaude Opus 5.5 deb7f11468 Feat(tools): add the IP Blocklist Check
A new In-depth Checks tool shows which blocklists an IP is on: the
real-time DNSBLs and the threat intelligence lists the private API's
/iphitlist checks. Listings are split by kind, so a VPN or Tor exit
reads as an anonymity mark rather than a bad record; hits stay open with
their reasons and removal links, the remaining lists fold away, and a
legend explains every mark.

/api/ipblocklist proxies the upstream with the caller's headers and a
30s timeout (a cold DNSBL pass outlasts the default 8s), passing sign-in
and quota rejections through. Signed-in users only, original site only,
150 checks a month (ip_hitlist in the usage dialog and the pre-flight
quota gate). Shift+B opens it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:15:21 +08:00

54 lines
2.2 KiB
JavaScript

// GET /api/ipblocklist?ip= — which blocklists an IP is on, from the private
// IPCheck.ing API (`/iphitlist`: live DNSBL lookups plus static threat lists).
// `?ip` is validated by requirePublicIP(). Sign-in is enforced upstream, so
// the caller's headers (auth token) are forwarded as-is.
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../server/logger.js';
// A cold DNSBL pass queries dozens of zones under per-zone deadlines; well
// past the default 8s, and the frontend waits a little longer than this.
export const IP_BLOCKLIST_TIMEOUT_MS = 30000;
export default async (req, res) => {
const apiKey = process.env.IPCHECKING_API_KEY;
const apiEndpoint = process.env.IPCHECKING_API_ENDPOINT;
if (!apiKey || !apiEndpoint) {
return res.status(500).json({ error: 'API key is missing' });
}
const url = new URL(`${apiEndpoint}/iphitlist`);
url.searchParams.set('ip', req.query.ip);
url.searchParams.set('key', apiKey);
try {
const apiResponse = await fetchUpstream(url, {
headers: { ...req.headers },
timeoutMs: IP_BLOCKLIST_TIMEOUT_MS,
});
// Monthly quota exhausted: not a failure — pass status + code through
// so the frontend shows the quota hint.
if (apiResponse.status === 429) {
const errorData = await apiResponse.json().catch(() => ({}));
return res.status(429).json({ error: errorData.error || 'Monthly quota exceeded', code: 'quota_exceeded' });
}
// Sign-in problems are the visitor's state, not a failure: pass the
// status through so the frontend prompts sign-in.
if (apiResponse.status === 401 || apiResponse.status === 403) {
const errorData = await apiResponse.json().catch(() => ({}));
return res.status(apiResponse.status).json({ error: errorData.message || 'Sign in required' });
}
if (!apiResponse.ok) {
throw new Error(`API responded with status: ${apiResponse.status}`);
}
res.json(await apiResponse.json());
} catch (error) {
logger.error({ err: error, ip: req.query.ip }, 'ip-blocklist upstream request failed');
res.status(500).json({ error: error.message });
}
};