Improved IP detection and display logic to always show IPv4 format, converting IPv6-mapped IPv4 addresses (e.g., ::ffff:127.0.0.1) and IPv6 loopback (::1) to 127.0.0.1. Updated API endpoint, display components, and added dedicated test/debug pages for IP format and detection. Added documentation summarizing the fixes and new features.
176 lines
5.8 KiB
TypeScript
176 lines
5.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getClientIp, isIpAllowed, getIpLocation, getDetailedIpInfo } from '@/lib/ip-utils'
|
||
|
||
// 強制動態渲染
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
// 使用詳細的IP檢測功能
|
||
const detailedInfo = getDetailedIpInfo(request);
|
||
let clientIp = detailedInfo.detectedIp;
|
||
|
||
// 確保返回IPv4格式的地址
|
||
function ensureIPv4Format(ip: string): string {
|
||
if (!ip) return '127.0.0.1';
|
||
|
||
// 移除空白字符
|
||
ip = ip.trim();
|
||
|
||
// 處理IPv6格式的IPv4地址
|
||
if (ip.startsWith('::ffff:')) {
|
||
return ip.substring(7);
|
||
}
|
||
|
||
// 處理純IPv6本地回環地址
|
||
if (ip === '::1') {
|
||
return '127.0.0.1';
|
||
}
|
||
|
||
// 驗證是否為有效的IPv4地址
|
||
const ipv4Regex = /^(?:(?: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]?)$/;
|
||
if (ipv4Regex.test(ip)) {
|
||
return ip;
|
||
}
|
||
|
||
// 如果不是有效的IPv4,返回默認值
|
||
return '127.0.0.1';
|
||
}
|
||
|
||
// 檢查是否為IPv6格式的IPv4地址
|
||
function isIPv6MappedIPv4(ip: string): boolean {
|
||
return ip.startsWith('::ffff:');
|
||
}
|
||
|
||
// 獲取IPv6格式的IPv4地址
|
||
function getIPv6MappedFormat(ipv4: string): string {
|
||
return `::ffff:${ipv4}`;
|
||
}
|
||
|
||
// 如果檢測到的是127.0.0.1,嘗試從請求頭獲取真實IP
|
||
if (clientIp === '127.0.0.1') {
|
||
// 檢查是否有代理轉發的真實IP
|
||
const forwardedFor = request.headers.get('x-forwarded-for');
|
||
if (forwardedFor) {
|
||
const ips = forwardedFor.split(',').map(ip => ip.trim());
|
||
for (const ip of ips) {
|
||
// 處理IPv6格式的IPv4地址
|
||
let cleanIp = ip;
|
||
if (ip.startsWith('::ffff:')) {
|
||
cleanIp = ip.substring(7);
|
||
}
|
||
if (cleanIp && cleanIp !== '127.0.0.1' && cleanIp !== '::1' && cleanIp !== 'localhost') {
|
||
clientIp = cleanIp;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 檢查其他可能的IP來源
|
||
const realIp = request.headers.get('x-real-ip');
|
||
if (realIp) {
|
||
let cleanRealIp = realIp;
|
||
if (realIp.startsWith('::ffff:')) {
|
||
cleanRealIp = realIp.substring(7);
|
||
}
|
||
if (cleanRealIp !== '127.0.0.1') {
|
||
clientIp = cleanRealIp;
|
||
}
|
||
}
|
||
|
||
const clientIpHeader = request.headers.get('x-client-ip');
|
||
if (clientIpHeader) {
|
||
let cleanClientIp = clientIpHeader;
|
||
if (clientIpHeader.startsWith('::ffff:')) {
|
||
cleanClientIp = clientIpHeader.substring(7);
|
||
}
|
||
if (cleanClientIp !== '127.0.0.1') {
|
||
clientIp = cleanClientIp;
|
||
}
|
||
}
|
||
}
|
||
|
||
const allowedIps = process.env.ALLOWED_IPS || ''
|
||
const enableIpWhitelist = process.env.ENABLE_IP_WHITELIST === 'true'
|
||
|
||
const isAllowed = enableIpWhitelist ? isIpAllowed(clientIp, allowedIps) : true
|
||
|
||
// 嘗試獲取地理位置信息
|
||
let locationInfo = null
|
||
if (clientIp && clientIp !== '127.0.0.1' && isPublicIp(clientIp)) {
|
||
try {
|
||
locationInfo = await getIpLocation(clientIp)
|
||
} catch (error) {
|
||
console.error('Error fetching location info:', error)
|
||
}
|
||
}
|
||
|
||
// 確保最終返回的IP是IPv4格式
|
||
const finalIp = ensureIPv4Format(clientIp);
|
||
const originalIp = detailedInfo.detectedIp;
|
||
const isIPv6Mapped = isIPv6MappedIPv4(originalIp);
|
||
const ipv6Format = getIPv6MappedFormat(finalIp);
|
||
|
||
return NextResponse.json({
|
||
ip: finalIp,
|
||
isAllowed,
|
||
enableIpWhitelist,
|
||
allowedIps: enableIpWhitelist ? allowedIps.split(',').map(ip => ip.trim()) : [],
|
||
timestamp: new Date().toISOString(),
|
||
ipv6Info: {
|
||
isIPv6Mapped,
|
||
originalFormat: originalIp,
|
||
ipv6Format,
|
||
hasIPv6Support: true
|
||
},
|
||
debug: {
|
||
allIpSources: detailedInfo.ipSources,
|
||
allFoundIps: detailedInfo.allFoundIps,
|
||
isLocalDevelopment: detailedInfo.isLocalDevelopment,
|
||
localIp: detailedInfo.localIp,
|
||
environment: process.env.NODE_ENV,
|
||
host: request.headers.get('host'),
|
||
referer: request.headers.get('referer'),
|
||
userAgent: request.headers.get('user-agent'),
|
||
originalDetectedIp: detailedInfo.detectedIp,
|
||
finalDetectedIp: finalIp,
|
||
rawDetectedIp: clientIp, // 保留原始檢測到的IP用於調試
|
||
ipDetectionMethod: isIPv6Mapped ? 'IPv6-Mapped-IPv4' : 'Standard-IPv4'
|
||
},
|
||
location: locationInfo,
|
||
// 本地開發環境的特殊信息
|
||
development: detailedInfo.isLocalDevelopment ? {
|
||
message: '本地開發環境 - IP檢測可能受限',
|
||
suggestions: [
|
||
'在生產環境中部署後,IP檢測會更準確',
|
||
'可以使用 ngrok 或類似工具進行外部測試',
|
||
'檢查防火牆和網路配置',
|
||
'確認代理伺服器設置',
|
||
'如果使用VPN,請檢查VPN設置'
|
||
]
|
||
} : null
|
||
})
|
||
} catch (error) {
|
||
console.error('Error getting IP info:', error)
|
||
return NextResponse.json(
|
||
{ error: '無法獲取IP信息' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
|
||
// 檢查是否為公網IP的輔助函數
|
||
function isPublicIp(ip: string): boolean {
|
||
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));
|
||
}
|