UNPKG

@lock-sdk/geo-block

Version:

Geographic blocking module for Lock security framework

537 lines (523 loc) 16.9 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/providers/ip-api.ts var ip_api_exports = {}; __export(ip_api_exports, { IpApiProvider: () => IpApiProvider }); var IpApiProvider; var init_ip_api = __esm({ "src/providers/ip-api.ts"() { "use strict"; IpApiProvider = class { constructor(config) { this.apiKey = config.apiKey; } async init() { } async lookup(ip) { try { const url = this.apiKey ? `https://pro.ip-api.com/json/${ip}?key=${this.apiKey}&fields=status,message,countryCode,region,city,lat,lon` : `http://ip-api.com/json/${ip}?fields=status,message,countryCode,region,city,lat,lon`; const response = await fetch(url); if (!response.ok) { throw new Error(`IP-API request failed with status: ${response.status}`); } const data = await response.json(); if (data.status === "success") { return { country: data.countryCode, region: data.region, city: data.city, latitude: data.lat, longitude: data.lon }; } else { console.warn(`IP-API lookup failed: ${data.message}`); return {}; } } catch (error) { console.error(`Error looking up IP ${ip} with IP-API:`, error); return {}; } } }; } }); // src/storage/redis.ts var RedisGeoCacheStore; var init_redis = __esm({ "src/storage/redis.ts"() { "use strict"; RedisGeoCacheStore = class { constructor(config) { this.keyPrefix = config.redis?.keyPrefix || "geocache:"; this.config = config.redis; this.ttl = config.cacheTtl || 3600; } 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(ip) { try { const data = await this.client.get(this.keyPrefix + ip); return data ? JSON.parse(data) : null; } catch (error) { console.error("Redis get error:", error); return null; } } async set(ip, value) { try { await this.client.set(this.keyPrefix + ip, JSON.stringify(value), { 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 UpstashGeoCacheStore; var init_upstash = __esm({ "src/storage/upstash.ts"() { "use strict"; UpstashGeoCacheStore = class { constructor(config) { this.keyPrefix = config.upstash?.keyPrefix || "geocache:"; this.config = config.upstash; this.ttl = config.cacheTtl || 3600; } 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(ip) { try { const data = await this.client.get(this.keyPrefix + ip); if (!data) return null; if (typeof data === "string") { try { return JSON.parse(data); } catch (parseError) { console.error("Error parsing JSON from Upstash:", parseError); return null; } } else if (typeof data === "object") { return data; } return null; } catch (error) { console.error("Upstash get error:", error); return null; } } async set(ip, value) { try { const stringValue = JSON.stringify(value); await this.client.set(this.keyPrefix + ip, stringValue, { ex: this.ttl }); } catch (error) { console.error("Upstash set error:", error); } } async close() { return Promise.resolve(); } }; } }); // src/storage/memory.ts var import_lru_cache, MemoryGeoCacheStore; var init_memory = __esm({ "src/storage/memory.ts"() { "use strict"; import_lru_cache = require("lru-cache"); MemoryGeoCacheStore = 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(ip) { return this.cache.get(ip) || null; } async set(ip, value) { this.cache.set(ip, value); } async close() { return Promise.resolve(); } }; } }); // src/storage/index.ts var storage_exports = {}; __export(storage_exports, { MemoryGeoCacheStore: () => MemoryGeoCacheStore, RedisGeoCacheStore: () => RedisGeoCacheStore, UpstashGeoCacheStore: () => UpstashGeoCacheStore, 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 RedisGeoCacheStore(config); break; case "upstash": if (!config.upstash) { throw new Error("Upstash configuration is required when using Upstash storage"); } store = new UpstashGeoCacheStore(config); break; case "memory": default: store = new MemoryGeoCacheStore(config); break; } await store.init(); return store; } var init_storage = __esm({ "src/storage/index.ts"() { "use strict"; init_redis(); init_upstash(); init_memory(); init_memory(); init_redis(); init_upstash(); } }); // src/index.ts var index_exports = {}; __export(index_exports, { GeoBlockEventType: () => GeoBlockEventType, MemoryGeoCacheStore: () => MemoryGeoCacheStore, RedisGeoCacheStore: () => RedisGeoCacheStore, UpstashGeoCacheStore: () => UpstashGeoCacheStore, createCacheStore: () => createCacheStore, extractIp: () => extractIp, geoBlock: () => geoBlock }); module.exports = __toCommonJS(index_exports); // src/types.ts var GeoBlockEventType = /* @__PURE__ */ ((GeoBlockEventType2) => { GeoBlockEventType2["GEO_BLOCKED"] = "geo.blocked"; return GeoBlockEventType2; })(GeoBlockEventType || {}); // src/index.ts var import_core = require("@lock-sdk/core"); // src/providers/index.ts var fs2 = __toESM(require("fs")); // src/providers/maxmind.ts var maxmind = __toESM(require("maxmind")); var fs = __toESM(require("fs")); var MaxMindProvider = class { constructor(config) { this.initialized = false; if (!config.maxmindDbPath) { throw new Error("MaxMind database path must be provided"); } this.dbPath = config.maxmindDbPath; } async init() { if (this.initialized) return; try { if (!fs.existsSync(this.dbPath)) { throw new Error(`MaxMind database file not found at: ${this.dbPath}`); } this.reader = await maxmind.open(this.dbPath); this.initialized = true; } catch (error) { throw new Error( `Failed to initialize MaxMind database: ${error instanceof Error ? error.message : String(error)}` ); } } async lookup(ip) { if (!this.initialized) { await this.init(); } try { const result = this.reader.get(ip); if (!result) { return {}; } return { country: result.country?.iso_code, region: result.subdivisions?.[0]?.iso_code, city: result.city?.names?.en, latitude: result.location?.latitude, longitude: result.location?.longitude }; } catch (error) { console.error(`Error looking up IP ${ip}:`, error); return {}; } } }; // src/providers/index.ts init_ip_api(); function createProvider(config) { if (config.provider === "maxmind") { if (!config.maxmindDbPath || !fs2.existsSync(config.maxmindDbPath)) { console.warn( `MaxMind database not found at path: ${config.maxmindDbPath || "undefined"}. Falling back to ip-api.com service.` ); return new IpApiProvider(config); } return new MaxMindProvider(config); } switch (config.provider) { case "ipapi": return new IpApiProvider(config); default: console.warn(`Unknown provider: ${config.provider}. Falling back to ip-api.com service.`); return new IpApiProvider(config); } } // src/utils/extract-ip.ts function extractIp(request, ipHeaders = [], useRemoteAddress = true) { if (request.headers) { for (const header of ipHeaders) { const value = request.headers[header.toLowerCase()]; if (value) { const parts = value.split(","); return parts[0].trim(); } } } if (useRemoteAddress) { if (request.connection && request.connection.remoteAddress) { return request.connection.remoteAddress; } if (request.socket && request.socket.remoteAddress) { return request.socket.remoteAddress; } } return null; } // src/index.ts var import_core2 = require("@lock-sdk/core"); 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 location", provider: "ipapi", storage: "memory", cacheTtl: 36e5, cacheSize: 1e4, failBehavior: "open" }; var geoCache = null; var provider = null; var geoBlock = (0, import_core.createModule)({ name: "geo-block", defaultConfig: DEFAULT_CONFIG, async check(context, config) { try { if (!geoCache) { try { geoCache = await createCacheStore(config); } catch (cacheError) { console.error(`Failed to initialize geo cache: ${cacheError.message}`); const { MemoryGeoCacheStore: MemoryGeoCacheStore2 } = await Promise.resolve().then(() => (init_storage(), storage_exports)); geoCache = new MemoryGeoCacheStore2(config); await geoCache.init(); } } if (!provider) { provider = createProvider(config); try { await provider.init(); } catch (initError) { console.error(`Failed to initialize geo provider: ${initError.message}`); if (config.failBehavior === "closed") { return { passed: false, reason: "geo.blocked" /* GEO_BLOCKED */, data: { error: "Geo provider failed to initialize" }, severity: "medium" }; } if (config.provider !== "ipapi") { try { const IpApiProvider2 = (init_ip_api(), __toCommonJS(ip_api_exports)).IpApiProvider; const fallbackProvider = new IpApiProvider2(config); await fallbackProvider.init(); provider = fallbackProvider; console.warn("Successfully switched to ip-api fallback provider"); } catch (fallbackError) { console.error(`Fallback provider also failed: ${fallbackError.message}`); return { passed: true }; } } else { return { passed: true }; } } } const ip = extractIp(context.request, config.ipHeaders, config.useRemoteAddress); if (!ip) { console.warn("No IP address could be extracted from the request"); return { passed: true }; } try { let geoInfo = await geoCache.get(ip); if (!geoInfo) { geoInfo = await provider.lookup(ip); if (geoInfo && Object.keys(geoInfo).length > 0) { await geoCache.set(ip, geoInfo); } } if (!geoInfo || !geoInfo.country) { console.warn(`No country information found for IP: ${ip}`); return { passed: true }; } const country = geoInfo.country; context.data.set("geo-block:country", country); const isBlocked = config.mode === "blacklist" && config.countries.includes(country) || config.mode === "whitelist" && !config.countries.includes(country); if (isBlocked) { return { passed: false, reason: "geo.blocked" /* GEO_BLOCKED */, data: { ip, country, geoInfo }, severity: "medium" }; } return { passed: true }; } catch (lookupError) { console.error(`Error during geo lookup for IP ${ip}:`, lookupError); if (config.failBehavior === "closed") { return { passed: false, reason: "geo.blocked" /* GEO_BLOCKED */, data: { error: "Geo lookup failed", ip }, severity: "medium" }; } return { passed: true }; } } catch (error) { console.error(`Unexpected error in geo-block module:`, error); return { passed: config.failBehavior !== "closed", reason: config.failBehavior === "closed" ? "geo.blocked" /* GEO_BLOCKED */ : void 0, data: config.failBehavior === "closed" ? { error: "Geo-block module failed" } : void 0, severity: "medium" }; } }, async handleFailure(context, reason, data) { const config = context.data.get("geo-block: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 location" }); } 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 location" }) ); } } }); (0, import_core2.registerModule)("geoBlock", geoBlock); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GeoBlockEventType, MemoryGeoCacheStore, RedisGeoCacheStore, UpstashGeoCacheStore, createCacheStore, extractIp, geoBlock });