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

505 lines
17 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 {
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格式
if (!isValidIp(ip)) {
return null;
}
return ip;
}
// 獲取客戶端真實IP
export function getClientIp(req: any): string {
// 按優先順序檢查各種IP來源
const ipSources = [
// 1Panel 和常見代理伺服器轉發的IP (優先級最高)
req.headers['x-forwarded-for'],
req.headers['x-real-ip'],
req.headers['x-client-ip'],
req.headers['cf-connecting-ip'], // Cloudflare
req.headers['x-original-forwarded-for'], // 某些代理伺服器
req.headers['x-cluster-client-ip'], // 集群環境
// 其他代理頭
req.headers['x-forwarded'], // 舊版代理頭
req.headers['forwarded-for'],
req.headers['forwarded'],
// 1Panel 特殊頭部
req.headers['x-1panel-client-ip'], // 1Panel 可能使用的頭部
req.headers['x-nginx-proxy-real-ip'], // Nginx 代理
// 直接連接的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);
// 檢查是否為公網IP排除內部IP和1Panel代理IP
if (isPublicIp(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));
}
// 驗證IP地址格式
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);
}
// 驗證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
};
}