@lock-dev/ip-filter
Version:
IP filtering module for lock.dev security framework
480 lines (469 loc) • 14.8 kB
JavaScript
;
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
});