505 lines
17 KiB
TypeScript
505 lines
17 KiB
TypeScript
/**
|
||
* 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
|
||
};
|
||
}
|