UNPKG

@lock-dev/vpn-detection

Version:

VPN and proxy detection module for lock.dev security framework

414 lines (406 loc) 13.6 kB
import { MemoryVPNCacheStore, RedisVPNCacheStore, UpstashVPNCacheStore, createCacheStore } from "./chunk-EEC3436U.mjs"; // 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 import { createModule } from "@lock-dev/core"; import { registerModule } from "@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 import axios from "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 axios.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 import axios2 from "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 axios2.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 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 = 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 import("./storage-JFLGPMFK.mjs"); 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 || {} }) ); } } }); registerModule("vpnDetector", vpnDetector); export { IPAPIProvider, IPQualityScoreProvider, MemoryVPNCacheStore, RedisVPNCacheStore, UpstashVPNCacheStore, VPNDetectionEventType, cleanIp, createCacheStore, createProvider, extractIp, vpnDetector };