273 lines
8.3 KiB
TypeScript
273 lines
8.3 KiB
TypeScript
// =====================================================
|
||
// 智能 IP 偵測器
|
||
// =====================================================
|
||
|
||
import { NextRequest } from 'next/server';
|
||
|
||
export interface IPDetectionResult {
|
||
detectedIP: string;
|
||
confidence: 'high' | 'medium' | 'low';
|
||
source: string;
|
||
allCandidates: string[];
|
||
isPublicIP: boolean;
|
||
}
|
||
|
||
export class SmartIPDetector {
|
||
private static instance: SmartIPDetector;
|
||
|
||
public static getInstance(): SmartIPDetector {
|
||
if (!SmartIPDetector.instance) {
|
||
SmartIPDetector.instance = new SmartIPDetector();
|
||
}
|
||
return SmartIPDetector.instance;
|
||
}
|
||
|
||
// 檢查是否為公網 IP
|
||
private isPublicIP(ip: string): boolean {
|
||
if (!ip || ip === 'unknown') return false;
|
||
|
||
// 本地地址
|
||
if (ip === '127.0.0.1' || ip === '::1' || ip === 'localhost') return false;
|
||
|
||
// 私有地址範圍
|
||
if (ip.startsWith('192.168.') ||
|
||
ip.startsWith('10.') ||
|
||
ip.startsWith('172.') ||
|
||
ip.startsWith('169.254.')) return false;
|
||
|
||
// IPv6 本地地址
|
||
if (ip.startsWith('fe80:') ||
|
||
ip.startsWith('::1') ||
|
||
ip.startsWith('::ffff:127.0.0.1')) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
// 檢查是否為基礎設施 IP(Vercel、AWS、Cloudflare 等)
|
||
private isInfrastructureIP(ip: string): boolean {
|
||
if (!ip) return false;
|
||
|
||
// Vercel/AWS EC2 實例
|
||
if (ip.includes('ec2-') && ip.includes('.compute-1.amazonaws.com')) return true;
|
||
if (ip.includes('ec2-') && ip.includes('.amazonaws.com')) return true;
|
||
|
||
// AWS 其他服務
|
||
if (ip.includes('.amazonaws.com')) return true;
|
||
|
||
// Vercel 相關
|
||
if (ip.includes('vercel')) return true;
|
||
|
||
// Cloudflare 相關(但 cf-connecting-ip 是我們想要的)
|
||
if (ip.includes('cloudflare') && !ip.includes('cf-connecting-ip')) return true;
|
||
|
||
// 其他常見的 CDN/代理服務
|
||
const infrastructurePatterns = [
|
||
'fastly.com',
|
||
'cloudfront.net',
|
||
'akamai.net',
|
||
'maxcdn.com',
|
||
'keycdn.com'
|
||
];
|
||
|
||
return infrastructurePatterns.some(pattern => ip.includes(pattern));
|
||
}
|
||
|
||
// 檢查是否為用戶真實 IP
|
||
private isUserRealIP(ip: string): boolean {
|
||
if (!ip || ip === 'unknown') return false;
|
||
|
||
// 不是基礎設施 IP
|
||
if (this.isInfrastructureIP(ip)) return false;
|
||
|
||
// 是公網 IP
|
||
if (!this.isPublicIP(ip)) return false;
|
||
|
||
// 是有效的 IP 格式
|
||
if (!this.isValidIP(ip)) return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
// 從 x-forwarded-for 解析 IP
|
||
private parseForwardedFor(forwarded: string): string[] {
|
||
if (!forwarded) return [];
|
||
|
||
return forwarded
|
||
.split(',')
|
||
.map(ip => ip.trim())
|
||
.filter(ip => ip && ip !== 'unknown');
|
||
}
|
||
|
||
// 智能偵測客戶端 IP
|
||
public detectClientIP(request: NextRequest): IPDetectionResult {
|
||
const candidates: string[] = [];
|
||
const sources: { [key: string]: string } = {};
|
||
|
||
// 1. 收集所有可能的 IP 來源
|
||
const headers = {
|
||
'x-forwarded-for': request.headers.get('x-forwarded-for'),
|
||
'x-real-ip': request.headers.get('x-real-ip'),
|
||
'cf-connecting-ip': request.headers.get('cf-connecting-ip'),
|
||
'x-client-ip': request.headers.get('x-client-ip'),
|
||
'x-forwarded': request.headers.get('x-forwarded'),
|
||
'x-cluster-client-ip': request.headers.get('x-cluster-client-ip'),
|
||
'x-original-forwarded-for': request.headers.get('x-original-forwarded-for'),
|
||
'x-remote-addr': request.headers.get('x-remote-addr'),
|
||
'remote-addr': request.headers.get('remote-addr'),
|
||
'client-ip': request.headers.get('client-ip'),
|
||
};
|
||
|
||
// 2. 從各個標頭提取 IP
|
||
Object.entries(headers).forEach(([header, value]) => {
|
||
if (value) {
|
||
if (header === 'x-forwarded-for') {
|
||
const ips = this.parseForwardedFor(value);
|
||
ips.forEach(ip => {
|
||
candidates.push(ip);
|
||
sources[ip] = header;
|
||
});
|
||
} else {
|
||
candidates.push(value);
|
||
sources[value] = header;
|
||
}
|
||
}
|
||
});
|
||
|
||
// 3. 添加 NextRequest 的 IP (如果可用)
|
||
const nextIP = (request as any).ip;
|
||
if (nextIP) {
|
||
candidates.push(nextIP);
|
||
sources[nextIP] = 'next-request';
|
||
}
|
||
|
||
// 4. 去重並過濾
|
||
const uniqueCandidates = [...new Set(candidates)].filter(ip =>
|
||
ip && ip !== 'unknown' && ip !== '::1' && ip !== '127.0.0.1'
|
||
);
|
||
|
||
console.log('🔍 IP 偵測候選:', {
|
||
candidates: uniqueCandidates,
|
||
sources: sources,
|
||
allHeaders: Object.fromEntries(request.headers.entries())
|
||
});
|
||
|
||
// 5. 智能選擇最佳 IP
|
||
let selectedIP = 'unknown';
|
||
let confidence: 'high' | 'medium' | 'low' = 'low';
|
||
let source = 'unknown';
|
||
|
||
// 優先級 1: Cloudflare IP (最高可信度) - 但要是用戶真實 IP
|
||
const cfIP = uniqueCandidates.find(ip => sources[ip] === 'cf-connecting-ip');
|
||
if (cfIP && this.isUserRealIP(cfIP)) {
|
||
selectedIP = cfIP;
|
||
confidence = 'high';
|
||
source = 'cf-connecting-ip';
|
||
}
|
||
|
||
// 優先級 2: 其他代理標頭中的用戶真實 IP
|
||
if (selectedIP === 'unknown') {
|
||
const proxyHeaders = ['x-real-ip', 'x-client-ip', 'x-cluster-client-ip'];
|
||
for (const header of proxyHeaders) {
|
||
const ip = uniqueCandidates.find(candidate => sources[candidate] === header);
|
||
if (ip && this.isUserRealIP(ip)) {
|
||
selectedIP = ip;
|
||
confidence = 'high';
|
||
source = header;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 優先級 3: x-forwarded-for 中的第一個用戶真實 IP
|
||
if (selectedIP === 'unknown') {
|
||
const forwardedIPs = uniqueCandidates.filter(ip => sources[ip] === 'x-forwarded-for');
|
||
const userRealIP = forwardedIPs.find(ip => this.isUserRealIP(ip));
|
||
if (userRealIP) {
|
||
selectedIP = userRealIP;
|
||
confidence = 'medium';
|
||
source = 'x-forwarded-for';
|
||
}
|
||
}
|
||
|
||
// 優先級 4: 任何用戶真實 IP
|
||
if (selectedIP === 'unknown') {
|
||
const userRealIP = uniqueCandidates.find(ip => this.isUserRealIP(ip));
|
||
if (userRealIP) {
|
||
selectedIP = userRealIP;
|
||
confidence = 'medium';
|
||
source = sources[userRealIP] || 'unknown';
|
||
}
|
||
}
|
||
|
||
// 優先級 5: 任何公網 IP(非基礎設施)
|
||
if (selectedIP === 'unknown') {
|
||
const publicIP = uniqueCandidates.find(ip =>
|
||
this.isPublicIP(ip) && !this.isInfrastructureIP(ip)
|
||
);
|
||
if (publicIP) {
|
||
selectedIP = publicIP;
|
||
confidence = 'low';
|
||
source = sources[publicIP] || 'unknown';
|
||
}
|
||
}
|
||
|
||
// 優先級 6: 任何非本地 IP(非基礎設施)
|
||
if (selectedIP === 'unknown') {
|
||
const nonLocalIP = uniqueCandidates.find(ip =>
|
||
ip !== '::1' && ip !== '127.0.0.1' &&
|
||
!ip.startsWith('192.168.') && !ip.startsWith('10.') && !ip.startsWith('172.') &&
|
||
!this.isInfrastructureIP(ip)
|
||
);
|
||
if (nonLocalIP) {
|
||
selectedIP = nonLocalIP;
|
||
confidence = 'low';
|
||
source = sources[nonLocalIP] || 'unknown';
|
||
}
|
||
}
|
||
|
||
// 優先級 7: 第一個候選 IP(最後選擇)
|
||
if (selectedIP === 'unknown' && uniqueCandidates.length > 0) {
|
||
selectedIP = uniqueCandidates[0];
|
||
confidence = 'low';
|
||
source = sources[selectedIP] || 'unknown';
|
||
}
|
||
|
||
const result: IPDetectionResult = {
|
||
detectedIP: selectedIP,
|
||
confidence,
|
||
source,
|
||
allCandidates: uniqueCandidates,
|
||
isPublicIP: this.isPublicIP(selectedIP)
|
||
};
|
||
|
||
console.log('🎯 IP 偵測結果:', {
|
||
...result,
|
||
isInfrastructureIP: this.isInfrastructureIP(selectedIP),
|
||
isUserRealIP: this.isUserRealIP(selectedIP),
|
||
infrastructureIPs: uniqueCandidates.filter(ip => this.isInfrastructureIP(ip)),
|
||
userRealIPs: uniqueCandidates.filter(ip => this.isUserRealIP(ip))
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
// 驗證 IP 格式
|
||
public isValidIP(ip: string): boolean {
|
||
if (!ip || ip === 'unknown') return false;
|
||
|
||
// IPv4 格式檢查
|
||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||
if (ipv4Regex.test(ip)) {
|
||
const parts = ip.split('.').map(Number);
|
||
return parts.every(part => part >= 0 && part <= 255);
|
||
}
|
||
|
||
// IPv6 格式檢查(簡化)
|
||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||
return ipv6Regex.test(ip);
|
||
}
|
||
}
|
||
|
||
// 導出單例實例
|
||
export const smartIPDetector = SmartIPDetector.getInstance();
|