@lock-dev/vpn-detection
Version:
VPN and proxy detection module for lock.dev security framework
657 lines (644 loc) • 21.5 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, 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
});