mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
OmniRoute is an intelligent API gateway that unifies 20+ AI providers behind a single OpenAI-compatible endpoint. Features include intelligent routing with 6 strategies, multi-format translation (OpenAI/Claude/Gemini/Responses API), circuit breakers, semantic caching, combo fallback chains, real-time health monitoring, and a full dashboard with provider management, analytics, and CLI tool integration. Key highlights: - 20+ providers (Claude Code, Codex, Gemini CLI, GitHub Copilot, iFlow, Qwen, Kiro, etc.) - 6 routing strategies (Fill First, Round Robin, P2C, Random, Least Used, Cost Optimized) - Export/Import database backup with full archive support - Translator Playground with 4 modes (Playground, Chat Tester, Test Bench, Live Monitor) - 100% TypeScript across src/ and open-sse/ - Docker support with multi-stage builds - Comprehensive documentation and 9 dashboard screenshots
250 lines
6.3 KiB
TypeScript
250 lines
6.3 KiB
TypeScript
/**
|
|
* IP Filter Middleware — Phase 6
|
|
*
|
|
* IP-based access control with blacklist, whitelist, priority modes, and temporary bans.
|
|
*/
|
|
|
|
// In-memory IP lists
|
|
let _config = {
|
|
enabled: false,
|
|
mode: "blacklist", // "blacklist" | "whitelist" | "whitelist-priority"
|
|
blacklist: new Set(),
|
|
whitelist: new Set(),
|
|
tempBans: new Map(), // IP → { until: timestamp, reason: string }
|
|
};
|
|
|
|
/**
|
|
* Configure the IP filter
|
|
*/
|
|
export function configureIPFilter(config) {
|
|
if (config.enabled !== undefined) _config.enabled = config.enabled;
|
|
if (config.mode) _config.mode = config.mode;
|
|
if (config.blacklist) _config.blacklist = new Set(config.blacklist);
|
|
if (config.whitelist) _config.whitelist = new Set(config.whitelist);
|
|
}
|
|
|
|
/**
|
|
* Get current IP filter config (for API)
|
|
*/
|
|
export function getIPFilterConfig() {
|
|
return {
|
|
enabled: _config.enabled,
|
|
mode: _config.mode,
|
|
blacklist: Array.from(_config.blacklist),
|
|
whitelist: Array.from(_config.whitelist),
|
|
tempBans: Array.from(_config.tempBans.entries()).map(([ip, info]) => ({
|
|
ip,
|
|
until: new Date(info.until).toISOString(),
|
|
reason: info.reason,
|
|
remainingMs: Math.max(0, info.until - Date.now()),
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if an IP is allowed
|
|
* @param {string} ip
|
|
* @returns {{ allowed: boolean, reason?: string }}
|
|
*/
|
|
export function checkIP(ip) {
|
|
if (!_config.enabled) return { allowed: true };
|
|
if (!ip) return { allowed: true };
|
|
|
|
const normalizedIP = normalizeIP(ip);
|
|
|
|
// Check temp bans first (highest priority)
|
|
const ban = _config.tempBans.get(normalizedIP);
|
|
if (ban) {
|
|
if (Date.now() < ban.until) {
|
|
return { allowed: false, reason: `Temporarily banned: ${ban.reason}` };
|
|
}
|
|
_config.tempBans.delete(normalizedIP); // Expired
|
|
}
|
|
|
|
switch (_config.mode) {
|
|
case "whitelist":
|
|
// Only whitelisted IPs allowed
|
|
if (!matchesAny(normalizedIP, _config.whitelist)) {
|
|
return { allowed: false, reason: "IP not in whitelist" };
|
|
}
|
|
return { allowed: true };
|
|
|
|
case "whitelist-priority":
|
|
// Whitelist overrides blacklist
|
|
if (matchesAny(normalizedIP, _config.whitelist)) {
|
|
return { allowed: true };
|
|
}
|
|
if (matchesAny(normalizedIP, _config.blacklist)) {
|
|
return { allowed: false, reason: "IP blacklisted" };
|
|
}
|
|
return { allowed: true };
|
|
|
|
case "blacklist":
|
|
default:
|
|
// Blacklisted IPs blocked
|
|
if (matchesAny(normalizedIP, _config.blacklist)) {
|
|
return { allowed: false, reason: "IP blacklisted" };
|
|
}
|
|
return { allowed: true };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Temporarily ban an IP
|
|
*/
|
|
export function tempBanIP(ip, durationMs, reason = "Automated ban") {
|
|
const normalizedIP = normalizeIP(ip);
|
|
_config.tempBans.set(normalizedIP, {
|
|
until: Date.now() + durationMs,
|
|
reason,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove a temporary ban
|
|
*/
|
|
export function removeTempBan(ip) {
|
|
_config.tempBans.delete(normalizeIP(ip));
|
|
}
|
|
|
|
/**
|
|
* Add IP to blacklist
|
|
*/
|
|
export function addToBlacklist(ip) {
|
|
_config.blacklist.add(normalizeIP(ip));
|
|
}
|
|
|
|
/**
|
|
* Remove IP from blacklist
|
|
*/
|
|
export function removeFromBlacklist(ip) {
|
|
_config.blacklist.delete(normalizeIP(ip));
|
|
}
|
|
|
|
/**
|
|
* Add IP to whitelist
|
|
*/
|
|
export function addToWhitelist(ip) {
|
|
_config.whitelist.add(normalizeIP(ip));
|
|
}
|
|
|
|
/**
|
|
* Remove IP from whitelist
|
|
*/
|
|
export function removeFromWhitelist(ip) {
|
|
_config.whitelist.delete(normalizeIP(ip));
|
|
}
|
|
|
|
/**
|
|
* Express/Next.js middleware factory
|
|
*/
|
|
export function createIPFilterMiddleware() {
|
|
return (req, res, next) => {
|
|
const ip = extractClientIP(req);
|
|
const { allowed, reason } = checkIP(ip);
|
|
if (!allowed) {
|
|
const statusCode = 403;
|
|
if (res.status) {
|
|
// Express
|
|
return res.status(statusCode).json({ error: reason || "Access denied" });
|
|
}
|
|
// Raw response
|
|
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: reason || "Access denied" }));
|
|
}
|
|
if (next) next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* For Next.js App Router — check IP from request object
|
|
*/
|
|
export function checkRequestIP(request) {
|
|
const ip =
|
|
request.headers?.get?.("x-forwarded-for")?.split(",")[0].trim() ||
|
|
request.headers?.get?.("x-real-ip") ||
|
|
request.headers?.get?.("cf-connecting-ip") ||
|
|
request.ip ||
|
|
"unknown";
|
|
return checkIP(ip);
|
|
}
|
|
|
|
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
|
|
function normalizeIP(ip) {
|
|
if (!ip) return "";
|
|
// Remove IPv6 prefix from IPv4-mapped addresses
|
|
return ip.replace(/^::ffff:/, "").trim();
|
|
}
|
|
|
|
function matchesAny(ip, ipSet) {
|
|
// Direct match
|
|
if (ipSet.has(ip)) return true;
|
|
|
|
// CIDR match
|
|
for (const entry of ipSet) {
|
|
if (entry.includes("/") && matchesCIDR(ip, entry)) return true;
|
|
// Wildcard match (e.g., "192.168.*.*")
|
|
if (entry.includes("*") && matchesWildcard(ip, entry)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function matchesCIDR(ip, cidr) {
|
|
try {
|
|
const [range, bits] = cidr.split("/");
|
|
const mask = parseInt(bits, 10);
|
|
if (isNaN(mask) || mask < 0 || mask > 32) return false;
|
|
const ipNum = ipToNum(ip);
|
|
const rangeNum = ipToNum(range);
|
|
if (ipNum === null || rangeNum === null) return false;
|
|
const maskBits = (-1 << (32 - mask)) >>> 0;
|
|
return (ipNum & maskBits) === (rangeNum & maskBits);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function ipToNum(ip) {
|
|
const parts = ip.split(".");
|
|
if (parts.length !== 4) return null;
|
|
let num = 0;
|
|
for (const p of parts) {
|
|
const n = parseInt(p, 10);
|
|
if (isNaN(n) || n < 0 || n > 255) return null;
|
|
num = (num << 8) | n;
|
|
}
|
|
return num >>> 0;
|
|
}
|
|
|
|
function matchesWildcard(ip, pattern) {
|
|
const ipParts = ip.split(".");
|
|
const patParts = pattern.split(".");
|
|
if (ipParts.length !== 4 || patParts.length !== 4) return false;
|
|
return ipParts.every((part, i) => patParts[i] === "*" || part === patParts[i]);
|
|
}
|
|
|
|
function extractClientIP(req) {
|
|
return (
|
|
req.headers?.["x-forwarded-for"]?.split(",")[0].trim() ||
|
|
req.headers?.["x-real-ip"] ||
|
|
req.headers?.["cf-connecting-ip"] ||
|
|
req.socket?.remoteAddress ||
|
|
req.ip ||
|
|
"unknown"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reset config (for testing)
|
|
*/
|
|
export function resetIPFilter() {
|
|
_config = {
|
|
enabled: false,
|
|
mode: "blacklist",
|
|
blacklist: new Set(),
|
|
whitelist: new Set(),
|
|
tempBans: new Map(),
|
|
};
|
|
}
|