UNPKG

@lock-dev/ip-filter

Version:

IP filtering module for lock.dev security framework

480 lines (469 loc) 14.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/storage/memory.ts var import_lru_cache, MemoryIPCacheStore; var init_memory = __esm({ "src/storage/memory.ts"() { "use strict"; import_lru_cache = require("lru-cache"); MemoryIPCacheStore = class { constructor(config) { this.cache = new import_lru_cache.LRUCache({ max: config.cacheSize || 1e4, ttl: config.cacheTtl || 36e5, ttlAutopurge: true }); } async init() { return Promise.resolve(); } async get(key) { const value = this.cache.get(key); return value === void 0 ? null : value; } async set(key, value) { this.cache.set(key, value); } async close() { return Promise.resolve(); } }; } }); // src/storage/redis.ts var RedisIPCacheStore; var init_redis = __esm({ "src/storage/redis.ts"() { "use strict"; RedisIPCacheStore = class { constructor(config) { this.keyPrefix = config.redis?.keyPrefix || "ipfilter:"; this.config = config.redis; this.ttl = Math.floor((config.cacheTtl || 36e5) / 1e3); } async init() { try { const { createClient } = await import("redis"); if (this.config.url) { this.client = createClient({ url: this.config.url }); } else { this.client = createClient({ socket: { host: this.config.host || "localhost", port: this.config.port || 6379 }, username: this.config.username, password: this.config.password, database: this.config.database || 0 }); } await this.client.connect(); this.client.on("error", (err) => { console.error("Redis client error:", err); }); } catch (error) { console.error("Failed to initialize Redis client:", error); throw new Error("Redis initialization failed"); } } async get(key) { try { const data = await this.client.get(this.keyPrefix + key); if (data === null) return null; return data === "true"; } catch (error) { console.error("Redis get error:", error); return null; } } async set(key, value) { try { await this.client.set(this.keyPrefix + key, value.toString(), { EX: this.ttl }); } catch (error) { console.error("Redis set error:", error); } } async close() { try { if (this.client) { await this.client.quit(); } } catch (error) { console.error("Redis close error:", error); } } }; } }); // src/storage/upstash.ts var UpstashIPCacheStore; var init_upstash = __esm({ "src/storage/upstash.ts"() { "use strict"; UpstashIPCacheStore = class { constructor(config) { this.keyPrefix = config.upstash?.keyPrefix || "ipfilter:"; this.config = config.upstash; this.ttl = Math.floor((config.cacheTtl || 36e5) / 1e3); } async init() { try { const { Redis } = await import("@upstash/redis"); this.client = new Redis({ url: this.config.url, token: this.config.token }); await this.client.ping(); } catch (error) { console.error("Failed to initialize Upstash client:", error); throw new Error("Upstash initialization failed"); } } async get(key) { try { const data = await this.client.get(this.keyPrefix + key); if (data === null) return null; if (typeof data === "boolean") { return data; } if (typeof data === "string") { return data === "true"; } return null; } catch (error) { console.error("Upstash get error:", error); return null; } } async set(key, value) { try { await this.client.set(this.keyPrefix + key, value.toString(), { ex: this.ttl }); } catch (error) { console.error("Upstash set error:", error); } } async close() { return Promise.resolve(); } }; } }); // src/storage/index.ts var storage_exports = {}; __export(storage_exports, { MemoryIPCacheStore: () => MemoryIPCacheStore, RedisIPCacheStore: () => RedisIPCacheStore, UpstashIPCacheStore: () => UpstashIPCacheStore, createCacheStore: () => createCacheStore }); async function createCacheStore(config) { const storageType = config.storage || "memory"; let store; switch (storageType) { case "redis": if (!config.redis) { throw new Error("Redis configuration is required when using Redis storage"); } store = new RedisIPCacheStore(config); break; case "upstash": if (!config.upstash) { throw new Error("Upstash configuration is required when using Upstash storage"); } store = new UpstashIPCacheStore(config); break; case "memory": default: store = new MemoryIPCacheStore(config); break; } await store.init(); return store; } var init_storage = __esm({ "src/storage/index.ts"() { "use strict"; init_memory(); init_redis(); init_upstash(); init_memory(); init_redis(); init_upstash(); } }); // src/index.ts var index_exports = {}; __export(index_exports, { IPFilterEventType: () => IPFilterEventType, MemoryIPCacheStore: () => MemoryIPCacheStore, RedisIPCacheStore: () => RedisIPCacheStore, UpstashIPCacheStore: () => UpstashIPCacheStore, cleanIp: () => cleanIp, createCacheStore: () => createCacheStore, extractIp: () => extractIp, ipEquals: () => ipEquals, ipFilter: () => ipFilter, ipInCidr: () => ipInCidr, isIpInList: () => isIpInList, isValidIpOrCidr: () => isValidIpOrCidr, normalizeIp: () => normalizeIp }); module.exports = __toCommonJS(index_exports); // src/types.ts var IPFilterEventType = /* @__PURE__ */ ((IPFilterEventType2) => { IPFilterEventType2["IP_BLOCKED"] = "ip.blocked"; IPFilterEventType2["IP_ALLOWED"] = "ip.allowed"; IPFilterEventType2["IP_FILTER_ERROR"] = "ip.error"; return IPFilterEventType2; })(IPFilterEventType || {}); // src/index.ts var import_core = require("@lock-dev/core"); var import_core2 = require("@lock-dev/core"); // src/utils/extract-ip.ts function extractIp(req, ipHeaders = ["cf-connecting-ip", "x-forwarded-for", "x-real-ip"], useRemoteAddress = true) { for (const header of ipHeaders) { const headerValue = req.headers?.[header] || req.headers?.[header.toLowerCase()]; if (headerValue) { if (typeof headerValue === "string" && headerValue.includes(",")) { const firstIp = headerValue.split(",")[0].trim(); if (firstIp) return firstIp; } else { return headerValue; } } } if (useRemoteAddress) { const connection = req.connection || req.socket || req.info; if (connection?.remoteAddress) { return connection.remoteAddress; } if (req.ip) { return req.ip; } if (req.socket?.remoteAddress) { return req.socket.remoteAddress; } } return null; } function cleanIp(ip) { if (ip.startsWith("::ffff:")) { return ip.substring(7); } return ip; } // src/utils/ip-matcher.ts var ipaddr = __toESM(require("ipaddr.js")); function ipInCidr(ip, cidr) { try { const addr = ipaddr.parse(ip); const range = ipaddr.parseCIDR(cidr); return addr.kind() === range[0].kind() && addr.match(range); } catch (error) { console.error(`Error checking IP ${ip} against CIDR ${cidr}:`, error); return false; } } function ipEquals(ip1, ip2) { try { const addr1 = ipaddr.parse(ip1); const addr2 = ipaddr.parse(ip2); return addr1.kind() === addr2.kind() && addr1.toString() === addr2.toString(); } catch (error) { console.error(`Error comparing IPs ${ip1} and ${ip2}:`, error); return false; } } function isIpInList(ip, list) { try { const parsedIp = ipaddr.parse(ip); const normalizedIp = parsedIp.toString(); return list.some((entry) => { if (entry.includes("/")) { return ipInCidr(normalizedIp, entry); } return ipEquals(normalizedIp, entry); }); } catch (error) { console.error(`Error checking IP ${ip} against list:`, error); return false; } } function normalizeIp(ip) { try { return ipaddr.parse(ip).toString(); } catch (error) { return null; } } function isValidIpOrCidr(input) { try { if (input.includes("/")) { ipaddr.parseCIDR(input); } else { ipaddr.parse(input); } return true; } catch (error) { return false; } } // src/index.ts init_storage(); init_storage(); var DEFAULT_CONFIG = { mode: "blacklist", ipHeaders: ["cf-connecting-ip", "x-forwarded-for", "x-real-ip"], useRemoteAddress: true, blockStatusCode: 403, blockMessage: "Access denied based on your IP address", storage: "memory", cacheTtl: 36e5, cacheSize: 1e4, failBehavior: "open" }; var ipCache = null; var ipFilter = (0, import_core.createModule)({ name: "ip-filter", defaultConfig: DEFAULT_CONFIG, async check(context, config) { try { if (!ipCache) { try { ipCache = await createCacheStore(config); } catch (cacheError) { console.error(`Failed to initialize IP filter cache: ${cacheError.message}`); const { MemoryIPCacheStore: MemoryIPCacheStore2 } = await Promise.resolve().then(() => (init_storage(), storage_exports)); ipCache = new MemoryIPCacheStore2(config); await ipCache.init(); } } const ip = extractIp(context.request, config.ipHeaders, config.useRemoteAddress); if (!ip) { console.warn("No IP address could be extracted from the request"); return { passed: config.failBehavior === "open", reason: config.failBehavior === "closed" ? "ip.blocked" /* IP_BLOCKED */ : void 0, data: { error: "Could not determine client IP address" }, severity: "medium" }; } try { let cacheKey = `${ip}:${config.mode}:${config.ipAddresses.join(",")}`; let ipMatched = await ipCache.get(cacheKey); if (ipMatched === null) { ipMatched = isIpInList(ip, config.ipAddresses); await ipCache.set(cacheKey, ipMatched); } const isBlocked = config.mode === "blacklist" && ipMatched || config.mode === "whitelist" && !ipMatched; if (isBlocked) { if (config.logBlocked) { const logFn = config.logFunction || console.log; logFn(`IP blocked: ${ip}`, { matched: ipMatched, mode: config.mode }); } return { passed: false, reason: "ip.blocked" /* IP_BLOCKED */, data: { ip, matched: ipMatched }, severity: "medium" }; } if (config.logAllowed) { const logFn = config.logFunction || console.log; logFn(`IP allowed: ${ip}`, { matched: ipMatched, mode: config.mode }); } return { passed: true, reason: "ip.allowed" /* IP_ALLOWED */, data: { ip, matched: ipMatched }, severity: "low" }; } catch (matchError) { console.error(`Error during IP matching for ${ip}:`, matchError); if (config.failBehavior === "closed") { return { passed: false, reason: "ip.error" /* IP_FILTER_ERROR */, data: { error: "IP matching failed", ip }, severity: "medium" }; } return { passed: true }; } } catch (error) { console.error(`Unexpected error in ip-filter module:`, error); return { passed: config.failBehavior !== "closed", reason: config.failBehavior === "closed" ? "ip.error" /* IP_FILTER_ERROR */ : void 0, data: config.failBehavior === "closed" ? { error: "IP-filter module failed" } : void 0, severity: "medium" }; } }, async handleFailure(context, reason, data) { const config = context.data.get("ip-filter:config"); const res = context.response; if (res.headersSent || res.writableEnded) { return; } if (typeof res.status === "function") { return res.status(config.blockStatusCode ?? 403).json({ error: config.blockMessage ?? "Access denied based on your IP address" }); } else if (typeof res.statusCode === "number") { res.statusCode = config.blockStatusCode ?? 403; res.setHeader("Content-Type", "application/json"); return res.end( JSON.stringify({ error: config.blockMessage ?? "Access denied based on your IP address" }) ); } } }); (0, import_core2.registerModule)("ipFilter", ipFilter); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { IPFilterEventType, MemoryIPCacheStore, RedisIPCacheStore, UpstashIPCacheStore, cleanIp, createCacheStore, extractIp, ipEquals, ipFilter, ipInCidr, isIpInList, isValidIpOrCidr, normalizeIp });