Files
wish-pool/lib/ip-utils.ts

596 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* IP 白名單檢查工具
*/
// 檢查IP是否在指定範圍內
function isIpInRange(ip: string, range: string): boolean {
// 處理單一IP
if (!range.includes('/')) {
return ip === range;
}
// 處理CIDR範圍 (例如: 192.168.1.0/24)
const [rangeIp, prefixLength] = range.split('/');
const mask = parseInt(prefixLength);
if (isNaN(mask) || mask < 0 || mask > 32) {
return false;
}
const ipNum = ipToNumber(ip);
const rangeNum = ipToNumber(rangeIp);
const maskNum = (0xFFFFFFFF << (32 - mask)) >>> 0;
return (ipNum & maskNum) === (rangeNum & maskNum);
}
// 將IP地址轉換為數字
function ipToNumber(ip: string): number {
const parts = ip.split('.').map(part => parseInt(part));
if (parts.length !== 4 || parts.some(part => isNaN(part) || part < 0 || part > 255)) {
throw new Error(`Invalid IP address: ${ip}`);
}
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
}
// 檢查IP是否在白名單內
export function isIpAllowed(clientIp: string, allowedIps: string): boolean {
if (!allowedIps || allowedIps.trim() === '') {
return true; // 如果沒有設置白名單允許所有IP
}
const allowedIpList = allowedIps.split(',').map(ip => ip.trim()).filter(ip => ip);
for (const allowedIp of allowedIpList) {
try {
// 如果是IPv6地址直接比较字符串
if (isValidIPv6(clientIp)) {
if (clientIp === allowedIp) {
return true;
}
} else if (isIpInRange(clientIp, allowedIp)) {
return true;
}
} catch (error) {
console.error(`Invalid IP range: ${allowedIp}`, error);
}
}
return false;
}
// 清理和驗證IP地址
function cleanIpAddress(ip: string): string | null {
if (!ip) return null;
// 移除空白字符
ip = ip.trim();
// 處理IPv6格式的IPv4地址 (例如: ::ffff:192.168.1.1)
if (ip.startsWith('::ffff:')) {
ip = ip.substring(7);
}
// 處理純IPv6本地回環地址
if (ip === '::1') {
return '127.0.0.1';
}
// 驗證IP格式 - 支持IPv4和IPv6
if (!isValidIp(ip) && !isValidIPv6(ip)) {
return null;
}
return ip;
}
// 檢測IP策略 - 根據請求頭判斷環境類型
function detectIpStrategy(req: any): {
isCloudflare: boolean;
isProxy: boolean;
isDirect: boolean;
strategy: string;
} {
const hasCfConnectingIp = !!req.headers['cf-connecting-ip'];
const hasCfRay = !!req.headers['cf-ray'];
const hasCfVisitor = !!req.headers['cf-visitor'];
const hasXForwardedFor = !!req.headers['x-forwarded-for'];
const hasXRealIp = !!req.headers['x-real-ip'];
const hasNginxProxy = !!req.headers['x-nginx-proxy-real-ip'];
const has1Panel = !!req.headers['x-1panel-client-ip'];
const isCloudflare = hasCfConnectingIp || hasCfRay || hasCfVisitor;
const isProxy = hasXForwardedFor || hasXRealIp || hasNginxProxy || has1Panel;
const isDirect = !isCloudflare && !isProxy;
let strategy = 'unknown';
if (isCloudflare) {
strategy = 'cloudflare';
} else if (isProxy) {
strategy = 'proxy';
} else {
strategy = 'direct';
}
return {
isCloudflare,
isProxy,
isDirect,
strategy
};
}
// 獲取客戶端真實IP
export function getClientIp(req: any): string {
// 智能IP檢測 - 根據實際環境動態調整優先級
const ipDetectionStrategy = detectIpStrategy(req);
let ipSources: (string | undefined)[];
if (ipDetectionStrategy.isCloudflare) {
// Cloudflare 環境的優先級 - 根據你的實際環境優化
ipSources = [
req.headers['cf-connecting-ip'], // 你的真實IP: 114.33.18.13
req.headers['x-forwarded-for'], // 備用來源 (但第一個IP是代理IP所以不優先)
req.headers['x-real-ip'], // 這是代理IP: 172.70.214.81,不應該使用
req.headers['x-client-ip'],
req.headers['x-original-forwarded-for'],
req.headers['x-cluster-client-ip'],
req.headers['x-forwarded'],
req.headers['forwarded-for'],
req.headers['forwarded'],
req.headers['x-1panel-client-ip'],
req.headers['x-nginx-proxy-real-ip'],
req.connection?.remoteAddress,
req.socket?.remoteAddress,
req.ip,
];
} else if (ipDetectionStrategy.isProxy) {
// 代理環境的優先級
ipSources = [
req.headers['x-forwarded-for'],
req.headers['x-real-ip'],
req.headers['x-client-ip'],
req.headers['x-original-forwarded-for'],
req.headers['x-cluster-client-ip'],
req.headers['cf-connecting-ip'], // 即使不是Cloudflare也可能有這個頭
req.headers['x-forwarded'],
req.headers['forwarded-for'],
req.headers['forwarded'],
req.headers['x-1panel-client-ip'],
req.headers['x-nginx-proxy-real-ip'],
req.connection?.remoteAddress,
req.socket?.remoteAddress,
req.ip,
];
} else {
// 直接連接的優先級
ipSources = [
req.headers['x-forwarded-for'],
req.headers['x-real-ip'],
req.headers['x-client-ip'],
req.headers['cf-connecting-ip'],
req.headers['x-original-forwarded-for'],
req.headers['x-cluster-client-ip'],
req.headers['x-forwarded'],
req.headers['forwarded-for'],
req.headers['forwarded'],
req.headers['x-1panel-client-ip'],
req.headers['x-nginx-proxy-real-ip'],
req.connection?.remoteAddress,
req.socket?.remoteAddress,
req.ip,
];
}
// 收集所有找到的IP用於調試
const foundIps: string[] = [];
for (const ipSource of ipSources) {
if (ipSource) {
// 處理多個IP的情況 (例如: "192.168.1.1, 10.0.0.1, 203.0.113.1")
const ipList = ipSource.toString().split(',').map(ip => ip.trim());
// 遍歷所有IP找到第一個有效的公網IP
for (const ip of ipList) {
const cleanIp = cleanIpAddress(ip);
if (cleanIp) {
foundIps.push(cleanIp);
// 特殊處理:如果是 cf-connecting-ip 頭部,直接返回(這是最可靠的)
if (ipSource === req.headers['cf-connecting-ip']) {
console.log('使用 cf-connecting-ip:', cleanIp, isValidIPv6(cleanIp) ? '(IPv6)' : '(IPv4)');
return cleanIp;
}
// 檢查是否為公網IP排除內部IP和代理IP
if ((isPublicIp(cleanIp) || isValidIPv6(cleanIp)) && !isInternalProxyIp(cleanIp)) {
return cleanIp;
}
}
}
}
}
// 如果沒有找到有效的公網IP返回第一個有效的IP
for (const ipSource of ipSources) {
if (ipSource) {
const ipList = ipSource.toString().split(',').map(ip => ip.trim());
for (const ip of ipList) {
const cleanIp = cleanIpAddress(ip);
if (cleanIp) {
return cleanIp;
}
}
}
}
// 在本地開發環境中嘗試獲取本機IP
if (process.env.NODE_ENV === 'development') {
const localIp = getLocalIp();
if (localIp && localIp !== '127.0.0.1') {
return localIp;
}
}
// 最後的備用方案
return '127.0.0.1';
}
// 獲取本機IP地址僅用於開發環境
function getLocalIp(): string | null {
try {
const os = require('os');
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const networkInterface of interfaces[name]) {
// 跳過內部非IPv4和回環地址
if (networkInterface.family === 'IPv4' && !networkInterface.internal) {
return networkInterface.address;
}
}
}
} catch (error) {
console.error('Error getting local IP:', error);
}
return null;
}
// 檢查是否為公網IP
function isPublicIp(ip: string): boolean {
// 私有IP範圍
const privateRanges = [
/^10\./, // 10.0.0.0/8
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
/^192\.168\./, // 192.168.0.0/16
/^127\./, // 127.0.0.0/8
/^169\.254\./, // 169.254.0.0/16 (Link-local)
/^0\./, // 0.0.0.0/8
/^224\./, // 224.0.0.0/4 (Multicast)
/^240\./, // 240.0.0.0/4 (Reserved)
];
return !privateRanges.some(range => range.test(ip));
}
// 檢查是否為內部代理IP (如1Panel、Cloudflare等)
function isInternalProxyIp(ip: string): boolean {
// 1Panel 和常見代理服務器的內部IP範圍
const proxyRanges = [
/^172\.70\./, // Cloudflare 內部IP (包含你遇到的 172.70.214.246)
/^172\.71\./, // Cloudflare 內部IP
/^172\.64\./, // Cloudflare 內部IP
/^172\.65\./, // Cloudflare 內部IP
/^172\.66\./, // Cloudflare 內部IP
/^172\.67\./, // Cloudflare 內部IP
/^172\.68\./, // Cloudflare 內部IP
/^172\.69\./, // Cloudflare 內部IP
/^172\.72\./, // Cloudflare 內部IP
/^172\.73\./, // Cloudflare 內部IP
/^172\.74\./, // Cloudflare 內部IP
/^172\.75\./, // Cloudflare 內部IP
/^172\.76\./, // Cloudflare 內部IP
/^172\.77\./, // Cloudflare 內部IP
/^172\.78\./, // Cloudflare 內部IP
/^172\.79\./, // Cloudflare 內部IP
/^172\.80\./, // Cloudflare 內部IP
/^172\.81\./, // Cloudflare 內部IP
/^172\.82\./, // Cloudflare 內部IP
/^172\.83\./, // Cloudflare 內部IP
/^172\.84\./, // Cloudflare 內部IP
/^172\.85\./, // Cloudflare 內部IP
/^172\.86\./, // Cloudflare 內部IP
/^172\.87\./, // Cloudflare 內部IP
/^172\.88\./, // Cloudflare 內部IP
/^172\.89\./, // Cloudflare 內部IP
/^172\.90\./, // Cloudflare 內部IP
/^172\.91\./, // Cloudflare 內部IP
/^172\.92\./, // Cloudflare 內部IP
/^172\.93\./, // Cloudflare 內部IP
/^172\.94\./, // Cloudflare 內部IP
/^172\.95\./, // Cloudflare 內部IP
/^172\.96\./, // Cloudflare 內部IP
/^172\.97\./, // Cloudflare 內部IP
/^172\.98\./, // Cloudflare 內部IP
/^172\.99\./, // Cloudflare 內部IP
/^172\.100\./, // Cloudflare 內部IP
/^172\.101\./, // Cloudflare 內部IP
/^172\.102\./, // Cloudflare 內部IP
/^172\.103\./, // Cloudflare 內部IP
/^172\.104\./, // Cloudflare 內部IP
/^172\.105\./, // Cloudflare 內部IP
/^172\.106\./, // Cloudflare 內部IP
/^172\.107\./, // Cloudflare 內部IP
/^172\.108\./, // Cloudflare 內部IP
/^172\.109\./, // Cloudflare 內部IP
/^172\.110\./, // Cloudflare 內部IP
/^172\.111\./, // Cloudflare 內部IP
/^172\.112\./, // Cloudflare 內部IP
/^172\.113\./, // Cloudflare 內部IP
/^172\.114\./, // Cloudflare 內部IP
/^172\.115\./, // Cloudflare 內部IP
/^172\.116\./, // Cloudflare 內部IP
/^172\.117\./, // Cloudflare 內部IP
/^172\.118\./, // Cloudflare 內部IP
/^172\.119\./, // Cloudflare 內部IP
/^172\.120\./, // Cloudflare 內部IP
/^172\.121\./, // Cloudflare 內部IP
/^172\.122\./, // Cloudflare 內部IP
/^172\.123\./, // Cloudflare 內部IP
/^172\.124\./, // Cloudflare 內部IP
/^172\.125\./, // Cloudflare 內部IP
/^172\.126\./, // Cloudflare 內部IP
/^172\.127\./, // Cloudflare 內部IP
/^172\.128\./, // Cloudflare 內部IP
/^172\.129\./, // Cloudflare 內部IP
/^172\.130\./, // Cloudflare 內部IP
/^172\.131\./, // Cloudflare 內部IP
/^172\.132\./, // Cloudflare 內部IP
/^172\.133\./, // Cloudflare 內部IP
/^172\.134\./, // Cloudflare 內部IP
/^172\.135\./, // Cloudflare 內部IP
/^172\.136\./, // Cloudflare 內部IP
/^172\.137\./, // Cloudflare 內部IP
/^172\.138\./, // Cloudflare 內部IP
/^172\.139\./, // Cloudflare 內部IP
/^172\.140\./, // Cloudflare 內部IP
/^172\.141\./, // Cloudflare 內部IP
/^172\.142\./, // Cloudflare 內部IP
/^172\.143\./, // Cloudflare 內部IP
/^172\.144\./, // Cloudflare 內部IP
/^172\.145\./, // Cloudflare 內部IP
/^172\.146\./, // Cloudflare 內部IP
/^172\.147\./, // Cloudflare 內部IP
/^172\.148\./, // Cloudflare 內部IP
/^172\.149\./, // Cloudflare 內部IP
/^172\.150\./, // Cloudflare 內部IP
/^172\.151\./, // Cloudflare 內部IP
/^172\.152\./, // Cloudflare 內部IP
/^172\.153\./, // Cloudflare 內部IP
/^172\.154\./, // Cloudflare 內部IP
/^172\.155\./, // Cloudflare 內部IP
/^172\.156\./, // Cloudflare 內部IP
/^172\.157\./, // Cloudflare 內部IP
/^172\.158\./, // Cloudflare 內部IP
/^172\.159\./, // Cloudflare 內部IP
/^172\.160\./, // Cloudflare 內部IP
/^172\.161\./, // Cloudflare 內部IP
/^172\.162\./, // Cloudflare 內部IP
/^172\.163\./, // Cloudflare 內部IP
/^172\.164\./, // Cloudflare 內部IP
/^172\.165\./, // Cloudflare 內部IP
/^172\.166\./, // Cloudflare 內部IP
/^172\.167\./, // Cloudflare 內部IP
/^172\.168\./, // Cloudflare 內部IP
/^172\.169\./, // Cloudflare 內部IP
/^172\.170\./, // Cloudflare 內部IP
/^172\.171\./, // Cloudflare 內部IP
/^172\.172\./, // Cloudflare 內部IP
/^172\.173\./, // Cloudflare 內部IP
/^172\.174\./, // Cloudflare 內部IP
/^172\.175\./, // Cloudflare 內部IP
/^172\.176\./, // Cloudflare 內部IP
/^172\.177\./, // Cloudflare 內部IP
/^172\.178\./, // Cloudflare 內部IP
/^172\.179\./, // Cloudflare 內部IP
/^172\.180\./, // Cloudflare 內部IP
/^172\.181\./, // Cloudflare 內部IP
/^172\.182\./, // Cloudflare 內部IP
/^172\.183\./, // Cloudflare 內部IP
/^172\.184\./, // Cloudflare 內部IP
/^172\.185\./, // Cloudflare 內部IP
/^172\.186\./, // Cloudflare 內部IP
/^172\.187\./, // Cloudflare 內部IP
/^172\.188\./, // Cloudflare 內部IP
/^172\.189\./, // Cloudflare 內部IP
/^172\.190\./, // Cloudflare 內部IP
/^172\.191\./, // Cloudflare 內部IP
/^172\.192\./, // Cloudflare 內部IP
/^172\.193\./, // Cloudflare 內部IP
/^172\.194\./, // Cloudflare 內部IP
/^172\.195\./, // Cloudflare 內部IP
/^172\.196\./, // Cloudflare 內部IP
/^172\.197\./, // Cloudflare 內部IP
/^172\.198\./, // Cloudflare 內部IP
/^172\.199\./, // Cloudflare 內部IP
/^172\.200\./, // Cloudflare 內部IP
/^172\.201\./, // Cloudflare 內部IP
/^172\.202\./, // Cloudflare 內部IP
/^172\.203\./, // Cloudflare 內部IP
/^172\.204\./, // Cloudflare 內部IP
/^172\.205\./, // Cloudflare 內部IP
/^172\.206\./, // Cloudflare 內部IP
/^172\.207\./, // Cloudflare 內部IP
/^172\.208\./, // Cloudflare 內部IP
/^172\.209\./, // Cloudflare 內部IP
/^172\.210\./, // Cloudflare 內部IP
/^172\.211\./, // Cloudflare 內部IP
/^172\.212\./, // Cloudflare 內部IP
/^172\.213\./, // Cloudflare 內部IP
/^172\.214\./, // Cloudflare 內部IP
/^172\.215\./, // Cloudflare 內部IP
/^172\.216\./, // Cloudflare 內部IP
/^172\.217\./, // Cloudflare 內部IP
/^172\.218\./, // Cloudflare 內部IP
/^172\.219\./, // Cloudflare 內部IP
/^172\.220\./, // Cloudflare 內部IP
/^172\.221\./, // Cloudflare 內部IP
/^172\.222\./, // Cloudflare 內部IP
/^172\.223\./, // Cloudflare 內部IP
/^172\.224\./, // Cloudflare 內部IP
/^172\.225\./, // Cloudflare 內部IP
/^172\.226\./, // Cloudflare 內部IP
/^172\.227\./, // Cloudflare 內部IP
/^172\.228\./, // Cloudflare 內部IP
/^172\.229\./, // Cloudflare 內部IP
/^172\.230\./, // Cloudflare 內部IP
/^172\.231\./, // Cloudflare 內部IP
/^172\.232\./, // Cloudflare 內部IP
/^172\.233\./, // Cloudflare 內部IP
/^172\.234\./, // Cloudflare 內部IP
/^172\.235\./, // Cloudflare 內部IP
/^172\.236\./, // Cloudflare 內部IP
/^172\.237\./, // Cloudflare 內部IP
/^172\.238\./, // Cloudflare 內部IP
/^172\.239\./, // Cloudflare 內部IP
/^172\.240\./, // Cloudflare 內部IP
/^172\.241\./, // Cloudflare 內部IP
/^172\.242\./, // Cloudflare 內部IP
/^172\.243\./, // Cloudflare 內部IP
/^172\.244\./, // Cloudflare 內部IP
/^172\.245\./, // Cloudflare 內部IP
/^172\.246\./, // Cloudflare 內部IP
/^172\.247\./, // Cloudflare 內部IP
/^172\.248\./, // Cloudflare 內部IP
/^172\.249\./, // Cloudflare 內部IP
/^172\.250\./, // Cloudflare 內部IP
/^172\.251\./, // Cloudflare 內部IP
/^172\.252\./, // Cloudflare 內部IP
/^172\.253\./, // Cloudflare 內部IP
/^172\.254\./, // Cloudflare 內部IP
/^172\.255\./, // Cloudflare 內部IP
];
return proxyRanges.some(range => range.test(ip));
}
// 驗證IPv4地址格式
export function isValidIp(ip: string): boolean {
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
return ipRegex.test(ip);
}
// 驗證IPv6地址格式
export function isValidIPv6(ip: string): boolean {
// 簡化的IPv6驗證正則表達式
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)+::$|^::$/;
return ipv6Regex.test(ip);
}
// 驗證CIDR格式
export function isValidCidr(cidr: string): boolean {
const cidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/;
return cidrRegex.test(cidr);
}
// 獲取IP地理位置信息可選功能
export async function getIpLocation(ip: string): Promise<any> {
try {
const response = await fetch(`http://ip-api.com/json/${ip}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,org,as,mobile,proxy,hosting,query`);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error('Error fetching IP location:', error);
}
return null;
}
// 獲取詳細的IP檢測信息用於調試
export function getDetailedIpInfo(req: any): {
detectedIp: string;
allFoundIps: string[];
ipSources: Record<string, string | null>;
isLocalDevelopment: boolean;
localIp: string | null;
} {
const ipSources = {
'x-forwarded-for': req.headers['x-forwarded-for'],
'x-real-ip': req.headers['x-real-ip'],
'x-client-ip': req.headers['x-client-ip'],
'cf-connecting-ip': req.headers['cf-connecting-ip'],
'x-forwarded': req.headers['x-forwarded'],
'forwarded-for': req.headers['forwarded-for'],
'forwarded': req.headers['forwarded'],
'x-original-forwarded-for': req.headers['x-original-forwarded-for'],
'x-cluster-client-ip': req.headers['x-cluster-client-ip'],
'connection.remoteAddress': req.connection?.remoteAddress,
'socket.remoteAddress': req.socket?.remoteAddress,
'req.ip': req.ip,
};
const allFoundIps: string[] = [];
let detectedIp = '127.0.0.1';
// 收集所有IP
Object.values(ipSources).forEach(ipSource => {
if (ipSource) {
const ipList = ipSource.toString().split(',').map(ip => ip.trim());
ipList.forEach(ip => {
const cleanIp = cleanIpAddress(ip);
if (cleanIp && !allFoundIps.includes(cleanIp)) {
allFoundIps.push(cleanIp);
}
});
}
});
// 如果沒有找到任何IP檢查是否有IPv6格式的地址
if (allFoundIps.length === 0) {
Object.values(ipSources).forEach(ipSource => {
if (ipSource) {
const ipList = ipSource.toString().split(',').map(ip => ip.trim());
ipList.forEach(ip => {
// 直接處理IPv6格式的IPv4地址
if (ip.startsWith('::ffff:')) {
const cleanIp = ip.substring(7);
if (isValidIp(cleanIp) && !allFoundIps.includes(cleanIp)) {
allFoundIps.push(cleanIp);
}
}
});
}
});
}
// 選擇最佳IP
for (const ip of allFoundIps) {
if (isPublicIp(ip)) {
detectedIp = ip;
break;
}
}
// 如果沒有公網IP選擇第一個非回環IP
if (detectedIp === '127.0.0.1') {
for (const ip of allFoundIps) {
if (ip !== '127.0.0.1' && ip !== '::1') {
detectedIp = ip;
break;
}
}
}
const isLocalDevelopment = process.env.NODE_ENV === 'development';
const localIp = isLocalDevelopment ? getLocalIp() : null;
return {
detectedIp,
allFoundIps,
ipSources,
isLocalDevelopment,
localIp
};
}