UNPKG

@lock-dev/vpn-detection

Version:

VPN and proxy detection module for lock.dev security framework

657 lines (644 loc) 21.5 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, MemoryVPNCacheStore; var init_memory = __esm({ "src/storage/memory.ts"() { "use strict"; import_lru_cache = require("lru-cache"); MemoryVPNCacheStore = 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/redis.ts var RedisVPNCacheStore; var init_redis = __esm({ "src/storage/redis.ts"() { "use strict"; RedisVPNCacheStore = class { constructor(config) { this.keyPrefix = config.redis?.keyPrefix || "vpncache:"; 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(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 UpstashVPNCacheStore; var init_upstash = __esm({ "src/storage/upstash.ts"() { "use strict"; UpstashVPNCacheStore = class { constructor(config) { this.keyPrefix = config.upstash?.keyPrefix || "vpncache:"; 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(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/index.ts var storage_exports = {}; __export(storage_exports, { MemoryVPNCacheStore: () => MemoryVPNCacheStore, RedisVPNCacheStore: () => RedisVPNCacheStore, UpstashVPNCacheStore: () => UpstashVPNCacheStore, 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 RedisVPNCacheStore(config); break; case "upstash": if (!config.upstash) { throw new Error("Upstash configuration is required when using Upstash storage"); } store = new UpstashVPNCacheStore(config); break; case "memory": default: store = new MemoryVPNCacheStore(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, { IPAPIProvider: () => IPAPIProvider, IPQualityScoreProvider: () => IPQualityScoreProvider, MemoryVPNCacheStore: () => MemoryVPNCacheStore, RedisVPNCacheStore: () => RedisVPNCacheStore, UpstashVPNCacheStore: () => UpstashVPNCacheStore, VPNDetectionEventType: () => VPNDetectionEventType, cleanIp: () => cleanIp, createCacheStore: () => createCacheStore, createProvider: () => createProvider, extractIp: () => extractIp, vpnDetector: () => vpnDetector }); module.exports = __toCommonJS(index_exports); // src/types.ts var VPNDetectionEventType = /* @__PURE__ */ ((VPNDetectionEventType2) => { VPNDetectionEventType2["VPN_DETECTED"] = "vpn.detected"; VPNDetectionEventType2["NO_VPN_DETECTED"] = "vpn.not_detected"; VPNDetectionEventType2["VPN_DETECTION_ERROR"] = "vpn.error"; return VPNDetectionEventType2; })(VPNDetectionEventType || {}); // 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/providers/ipapi.ts var import_axios = __toESM(require("axios")); var IPAPIProvider = class { constructor(config) { this.config = config; this.baseUrl = "http://ip-api.com/json"; this.proUrl = "http://pro.ip-api.com/json"; this.useProVersion = !!config.apiKey; this.apiKey = config.apiKey || ""; this.fields = [ "status", "message", "continent", "continentCode", "country", "countryCode", "region", "regionName", "city", "district", "zip", "lat", "lon", "timezone", "offset", "currency", "isp", "org", "as", "asname", "reverse", "mobile", "proxy", "hosting", "query" ]; } /** * Initializes the provider. * * @returns A promise that resolves when the provider has been initialized. */ async init() { } /** * Checks if an IP address is associated with a VPN, proxy, or Tor. * * @param ip - The IP address to check. * @returns A promise that resolves to the VPN detection result. */ async checkIp(ip) { try { const url = this.useProVersion ? `${this.proUrl}/${ip}?key=${this.apiKey}&fields=${this.fields.join(",")}` : `${this.baseUrl}/${ip}?fields=${this.fields.join(",")}`; const response = await import_axios.default.get(url); if (response.data.status !== "success") { throw new Error(response.data.message || "IP-API request failed"); } const isProxyOrVPN = response.data.proxy; const isHosting = response.data.hosting; const proxyScore = isProxyOrVPN ? 1 : 0; const datacenterScore = isHosting ? 1 : 0; const orgLower = (response.data.org || "").toLowerCase(); const isTor = isProxyOrVPN && (orgLower.includes("tor") || orgLower.includes("exit") || orgLower.includes("node")); const torScore = isTor ? 0.8 : 0; return { isVpn: isProxyOrVPN, vpnScore: proxyScore, isProxy: isProxyOrVPN, proxyScore, isTor, torScore, isDatacenter: isHosting, datacenterScore, providerData: { country: response.data.country, countryCode: response.data.countryCode, isp: response.data.isp, organization: response.data.org, asn: response.data.as, asnName: response.data.asname, city: response.data.city, region: response.data.regionName, timezone: response.data.timezone, isMobile: response.data.mobile }, timestamp: Date.now() }; } catch (error) { console.error(`IP-API error: ${error.message}`); throw error; } } }; // src/providers/ipqualityscore.ts var import_axios2 = __toESM(require("axios")); var IPQualityScoreProvider = class { constructor(config) { this.config = config; this.baseUrl = "https://www.ipqualityscore.com/api/json/ip"; this.apiKey = config.apiKey || ""; this.strictMode = config.customProviderOptions?.strictMode || false; this.extraParams = config.customProviderOptions?.extraParams || {}; } /** * Initializes the provider. * * @returns A promise that resolves when the provider has been initialized. */ async init() { if (!this.apiKey) { throw new Error("IPQualityScore API key is required"); } } /** * Checks if an IP address is associated with a VPN, proxy, or Tor. * * @param ip - The IP address to check. * @returns A promise that resolves to the VPN detection result. */ async checkIp(ip) { try { const params = { strictness: this.strictMode ? 1 : 0, allow_public_access_points: true, fast: false, mobile: false, ...this.extraParams }; const url = `${this.baseUrl}/${this.apiKey}/${ip}`; const response = await import_axios2.default.get(url, { params }); if (!response.data.success) { throw new Error(response.data.message || "IPQualityScore API request failed"); } return { isVpn: response.data.vpn || response.data.active_vpn || false, vpnScore: response.data.vpn ? response.data.fraud_score / 100 : 0, isProxy: response.data.proxy || false, proxyScore: response.data.proxy ? response.data.fraud_score / 100 : 0, isTor: response.data.tor || response.data.active_tor || false, torScore: response.data.tor ? 1 : 0, isDatacenter: response.data.hosting || false, datacenterScore: response.data.hosting ? 1 : 0, providerData: { fraudScore: response.data.fraud_score, country: response.data.country_code, isp: response.data.ISP, organization: response.data.organization, asn: response.data.ASN, connectionType: response.data.connection_type, isBot: response.data.bot_status, isMobile: response.data.mobile, isResidential: response.data.residential, isPublicAccessPoint: response.data.public_access_point, recentAbuse: response.data.recent_abuse }, timestamp: Date.now() }; } catch (error) { console.error(`IPQualityScore API error: ${error.message}`); throw error; } } }; // src/providers/index.ts function createProvider(config) { if (config.provider === "custom" && config.customProvider) { return config.customProvider; } switch (config.provider) { case "ipqualityscore": return new IPQualityScoreProvider(config); case "ipapi": return new IPAPIProvider(config); default: return new IPQualityScoreProvider(config); } } // src/index.ts init_storage(); init_storage(); var DEFAULT_CONFIG = { ipHeaders: ["cf-connecting-ip", "x-forwarded-for", "x-real-ip"], useRemoteAddress: true, blockStatusCode: 403, blockMessage: "Access denied: VPN or proxy detected", provider: "ipapi", storage: "memory", cacheTtl: 36e5, cacheSize: 1e4, vpnScoreThreshold: 0.7, proxyScoreThreshold: 0.7, datacenterScoreThreshold: 0.7, torScoreThreshold: 0.7, checkVpn: true, checkProxy: true, checkDatacenter: true, checkTor: true, failBehavior: "open", blockTor: true, blockVpn: true, blockProxy: true, blockDatacenter: false, customProviderOptions: {} }; var vpnCache = null; var provider = null; var vpnDetector = (0, import_core.createModule)({ name: "vpn-detector", defaultConfig: DEFAULT_CONFIG, async check(context, config) { try { if (!vpnCache) { try { vpnCache = await createCacheStore(config); } catch (cacheError) { console.error(`Failed to initialize VPN cache: ${cacheError.message}`); const { MemoryVPNCacheStore: MemoryVPNCacheStore2 } = await Promise.resolve().then(() => (init_storage(), storage_exports)); vpnCache = new MemoryVPNCacheStore2(config); await vpnCache.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" ? "vpn.error" /* VPN_DETECTION_ERROR */ : void 0, data: { error: "Could not determine client IP address" }, severity: "medium" }; } try { let detectionResult = await vpnCache.get(ip); if (!detectionResult) { if (!provider) { provider = createProvider(config); try { await provider.init(); } catch (initError) { console.error( `Failed to initialize VPN detection provider: ${initError.message}` ); if (config.failBehavior === "closed") { return { passed: false, reason: "vpn.error" /* VPN_DETECTION_ERROR */, data: { error: "VPN detection provider failed to initialize" }, severity: "medium" }; } return { passed: true }; } } detectionResult = await provider.checkIp(ip); if (detectionResult && Object.keys(detectionResult).length > 0) { await vpnCache.set(ip, detectionResult); } } if (!detectionResult) { console.warn(`No VPN detection information found for IP: ${ip}`); return { passed: true }; } let isBlocked = false; let blockReason = ""; if (config.checkVpn && config.blockVpn && detectionResult.isVpn && config.vpnScoreThreshold && detectionResult.vpnScore >= config.vpnScoreThreshold) { isBlocked = true; blockReason = "VPN detected"; } if (config.checkProxy && config.blockProxy && config.proxyScoreThreshold && detectionResult.isProxy && detectionResult.proxyScore >= config.proxyScoreThreshold) { isBlocked = true; blockReason = blockReason ? `${blockReason}, proxy detected` : "Proxy detected"; } if (config.checkTor && config.blockTor && config.torScoreThreshold && detectionResult.isTor && detectionResult.torScore >= config.torScoreThreshold) { isBlocked = true; blockReason = blockReason ? `${blockReason}, Tor detected` : "Tor detected"; } if (config.checkDatacenter && config.blockDatacenter && config.datacenterScoreThreshold && detectionResult.isDatacenter && detectionResult.datacenterScore >= config.datacenterScoreThreshold) { isBlocked = true; blockReason = blockReason ? `${blockReason}, datacenter IP detected` : "Datacenter IP detected"; } if (isBlocked) { return { passed: false, reason: "vpn.detected" /* VPN_DETECTED */, data: { ip, detectionResult, reason: blockReason }, severity: "medium" }; } return { passed: true, reason: "vpn.not_detected" /* NO_VPN_DETECTED */, data: { ip, detectionResult }, severity: "low" }; } catch (detectionError) { console.error(`Error during VPN detection for IP ${ip}:`, detectionError); if (config.failBehavior === "closed") { return { passed: false, reason: "vpn.error" /* VPN_DETECTION_ERROR */, data: { error: "VPN detection failed", ip }, severity: "medium" }; } return { passed: true }; } } catch (error) { console.error(`Unexpected error in vpn-detector module:`, error); return { passed: config.failBehavior !== "closed", reason: config.failBehavior === "closed" ? "vpn.error" /* VPN_DETECTION_ERROR */ : void 0, data: config.failBehavior === "closed" ? { error: "VPN detection module failed" } : void 0, severity: "medium" }; } }, async handleFailure(context, reason, data) { const config = context.data.get("vpn-detector:config"); const res = context.response; if (res.headersSent || res.writableEnded) { return; } let message = config.blockMessage ?? "Access denied: VPN or proxy detected"; if (data?.reason) { message = `${message}: ${data.reason}`; } if (typeof res.status === "function") { return res.status(config.blockStatusCode ?? 403).json({ error: message, blocked: true, details: data?.detectionResult || {} }); } else if (typeof res.statusCode === "number") { res.statusCode = config.blockStatusCode ?? 403; res.setHeader("Content-Type", "application/json"); return res.end( JSON.stringify({ error: message, blocked: true, details: data?.detectionResult || {} }) ); } } }); (0, import_core2.registerModule)("vpnDetector", vpnDetector); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { IPAPIProvider, IPQualityScoreProvider, MemoryVPNCacheStore, RedisVPNCacheStore, UpstashVPNCacheStore, VPNDetectionEventType, cleanIp, createCacheStore, createProvider, extractIp, vpnDetector });