diff --git a/IP-FIX-SUMMARY.md b/IP-FIX-SUMMARY.md new file mode 100644 index 0000000..19f3fd9 --- /dev/null +++ b/IP-FIX-SUMMARY.md @@ -0,0 +1,100 @@ +# IP 檢測問題修復總結 + +## 問題描述 + +用戶報告顯示的 IP 地址 `::ffff:127.0.0.1` 是錯誤的。這個地址實際上是 IPv4 地址 `127.0.0.1`(本地回環地址)的 IPv6 映射格式,這通常表示 IP 檢測或顯示邏輯有問題。 + +## 問題原因 + +1. **IPv6 格式處理不完整**:雖然 `cleanIpAddress` 函數有處理 `::ffff:` 前綴,但在某些情況下沒有被正確調用 +2. **本地回環地址處理**:系統返回 `::ffff:127.0.0.1` 而不是 `127.0.0.1`,但顯示邏輯沒有正確轉換 +3. **IP 來源檢查不完整**:在 API 端點中,當檢測到 `127.0.0.1` 時,沒有正確處理 IPv6 格式的地址 + +## 修復方案 + +### 1. 改進 `cleanIpAddress` 函數 + +在 `lib/ip-utils.ts` 中添加了對純 IPv6 本地回環地址的處理: + +```typescript +// 處理純IPv6本地回環地址 +if (ip === '::1') { + return '127.0.0.1'; +} +``` + +### 2. 增強 `getDetailedIpInfo` 函數 + +添加了額外的 IPv6 格式檢查邏輯: + +```typescript +// 如果沒有找到任何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); + } + } + }); + } + }); +} +``` + +### 3. 修復 API 端點 + +在 `app/api/ip/route.ts` 中改進了 IPv6 格式的處理: + +```typescript +// 處理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; +} +``` + +### 4. 創建調試工具 + +創建了詳細的 IP 調試頁面 (`app/test/ip-debug/page.tsx`),可以: +- 顯示所有檢測到的 IP 地址 +- 顯示 IP 來源詳細信息 +- 顯示原始和最終檢測到的 IP +- 提供調試信息 + +## 測試方法 + +1. **訪問調試頁面**:`/test/ip-debug` +2. **檢查 API 端點**:`/api/ip` +3. **使用外部測試**:`/test/ip-test` + +## 預期結果 + +修復後,系統應該: +1. 正確將 `::ffff:127.0.0.1` 轉換為 `127.0.0.1` +2. 正確將 `::1` 轉換為 `127.0.0.1` +3. 在調試頁面中顯示正確的 IP 地址 +4. 提供詳細的調試信息幫助診斷問題 + +## 本地開發環境說明 + +在本地開發環境中,顯示 `127.0.0.1` 是正常的行為,因為: +- 所有請求都來自本地回環地址 +- 這是正常的開發環境行為 +- 在生產環境中部署後,IP 檢測會更準確 + +## 建議 + +1. **使用 ngrok 進行外部測試**:`ngrok http 3000` +2. **部署到生產環境**:在真實環境中測試 IP 檢測功能 +3. **檢查網路配置**:確保代理伺服器正確設置 IP 轉發頭 +4. **使用調試工具**:利用新的調試頁面進行問題診斷 \ No newline at end of file diff --git a/IPV4-DISPLAY-FIX.md b/IPV4-DISPLAY-FIX.md new file mode 100644 index 0000000..a50e20b --- /dev/null +++ b/IPV4-DISPLAY-FIX.md @@ -0,0 +1,141 @@ +# IPv4 格式顯示修復 + +## 問題描述 + +用戶要求確保所有 IP 地址都顯示為 IPv4 格式,而不是 IPv6 格式(如 `::ffff:127.0.0.1`)。 + +## 修復方案 + +### 1. 改進 IP 顯示組件 (`components/ip-display.tsx`) + +添加了 `cleanIpForDisplay` 函數來確保顯示的 IP 始終是 IPv4 格式: + +```typescript +function cleanIpForDisplay(ip: string): string { + if (!ip) return '127.0.0.1'; + + // 移除空白字符 + ip = ip.trim(); + + // 處理IPv6格式的IPv4地址 (例如: ::ffff:192.168.1.1) + 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'; +} +``` + +### 2. 改進 API 端點 (`app/api/ip/route.ts`) + +添加了 `ensureIPv4Format` 函數來確保 API 返回的 IP 始終是 IPv4 格式: + +```typescript +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'; +} +``` + +### 3. 創建 IPv4 格式測試頁面 (`app/test/ip-format-test/page.tsx`) + +創建了一個專門的測試頁面來驗證 IPv4 格式轉換功能: + +- 測試各種 IP 地址格式的轉換 +- 顯示轉換前後的對比 +- 提供詳細的格式分析 +- 驗證轉換邏輯的正確性 + +## 轉換規則 + +### ✅ 正確處理的格式 + +| 原始格式 | 轉換後 | 說明 | +|---------|--------|------| +| `::ffff:127.0.0.1` | `127.0.0.1` | IPv6格式的IPv4地址 | +| `::1` | `127.0.0.1` | IPv6本地回環地址 | +| `127.0.0.1` | `127.0.0.1` | 標準IPv4地址(保持不變) | +| `192.168.1.1` | `192.168.1.1` | 標準IPv4地址(保持不變) | +| `::ffff:203.0.113.1` | `203.0.113.1` | IPv6格式的公網IPv4地址 | + +### ⚠️ 無效格式處理 + +| 原始格式 | 轉換後 | 說明 | +|---------|--------|------| +| `invalid-ip` | `127.0.0.1` | 無效格式,返回默認值 | +| `localhost` | `127.0.0.1` | 主機名,返回默認值 | +| `null` 或 `undefined` | `127.0.0.1` | 空值,返回默認值 | + +## 測試方法 + +### 1. 訪問測試頁面 +``` +http://localhost:3000/test/ip-format-test +``` + +### 2. 檢查 API 響應 +``` +http://localhost:3000/api/ip +``` + +### 3. 查看實際顯示 +訪問主頁面,檢查 IP 顯示組件是否顯示 IPv4 格式。 + +## 預期結果 + +修復後,系統應該: + +1. **始終顯示 IPv4 格式**:無論原始檢測到的 IP 是什麼格式,都轉換為標準 IPv4 格式 +2. **正確處理 IPv6 格式**:將 `::ffff:127.0.0.1` 轉換為 `127.0.0.1` +3. **處理本地回環地址**:將 `::1` 轉換為 `127.0.0.1` +4. **保持有效 IPv4 不變**:`192.168.1.1` 等標準 IPv4 地址保持原樣 +5. **提供默認值**:無效格式返回 `127.0.0.1` 作為默認值 + +## 實現位置 + +- **前端顯示**:`components/ip-display.tsx` +- **API 端點**:`app/api/ip/route.ts` +- **測試頁面**:`app/test/ip-format-test/page.tsx` +- **調試工具**:`app/test/ip-debug/page.tsx` + +## 驗證 + +1. 啟動開發服務器:`npm run dev` +2. 訪問 `/test/ip-format-test` 查看格式轉換測試 +3. 訪問主頁面檢查 IP 顯示是否為 IPv4 格式 +4. 檢查 API 端點 `/api/ip` 的響應 + +現在所有 IP 地址都會以標準的 IPv4 格式顯示,提供一致且易於理解的用戶體驗。 \ No newline at end of file diff --git a/IPV6-DISPLAY-README.md b/IPV6-DISPLAY-README.md new file mode 100644 index 0000000..cd2ed6c --- /dev/null +++ b/IPV6-DISPLAY-README.md @@ -0,0 +1,162 @@ +# IPv6格式IPv4地址顯示功能 + +## 概述 + +本專案已實現首頁顯示IPv6格式的IPv4地址功能。當系統檢測到IPv6格式的IPv4地址(如 `::ffff:192.168.1.1`)時,會自動轉換並顯示為標準的IPv4格式,同時提供詳細的IPv6信息。 + +## 功能特點 + +### 1. 自動IPv6格式檢測 +- 自動檢測IPv6格式的IPv4地址(`::ffff:` 前綴) +- 將IPv6格式轉換為標準IPv4格式顯示 +- 保留原始IPv6格式信息供參考 + +### 2. 雙格式顯示 +- **主要顯示**: 標準IPv4格式(如 `192.168.1.1`) +- **次要顯示**: IPv6映射格式(如 `::ffff:192.168.1.1`) +- 支持點擊信息圖標查看詳細信息 + +### 3. 響應式設計 +- **桌面版**: 完整顯示IPv4和IPv6格式 +- **手機版**: 簡化顯示,標識IP類型(IPv4/IPv6) + +### 4. 詳細信息彈出框 +點擊信息圖標可查看: +- IPv4格式地址 +- IPv6格式地址 +- 原始檢測格式 +- 檢測方法 +- 所有檢測到的IP列表 +- IPv6支援狀態 + +## 技術實現 + +### API端點 (`/api/ip`) +```typescript +{ + ip: "192.168.1.1", // 標準IPv4格式 + ipv6Info: { + isIPv6Mapped: true, // 是否為IPv6映射 + originalFormat: "::ffff:192.168.1.1", // 原始格式 + ipv6Format: "::ffff:192.168.1.1", // IPv6格式 + hasIPv6Support: true // IPv6支援狀態 + }, + debug: { + ipDetectionMethod: "IPv6-Mapped-IPv4", // 檢測方法 + // ... 其他調試信息 + } +} +``` + +### 組件功能 (`components/ip-display.tsx`) +- 自動處理IPv6格式轉換 +- 提供交互式詳細信息顯示 +- 支持移動端和桌面端不同顯示模式 + +## 使用方式 + +### 1. 基本使用 +```tsx +import IpDisplay from "@/components/ip-display" + +// 桌面版完整顯示 + + +// 手機版簡化顯示 + +``` + +### 2. 測試頁面 +訪問 `/test-ipv6` 頁面查看完整功能演示: +- IP顯示組件測試 +- 原始API數據展示 +- 調試信息查看 + +## 支援的IPv6格式 + +### 1. IPv6映射IPv4地址 +- 格式: `::ffff:192.168.1.1` +- 自動轉換為: `192.168.1.1` +- 顯示類型: IPv6 + +### 2. IPv6本地回環地址 +- 格式: `::1` +- 自動轉換為: `127.0.0.1` +- 顯示類型: IPv4 + +### 3. 標準IPv4地址 +- 格式: `192.168.1.1` +- 保持原格式 +- 顯示類型: IPv4 + +## 配置選項 + +### 環境變數 +```env +# IP白名單功能 +ENABLE_IP_WHITELIST=true +ALLOWED_IPS=192.168.1.0/24,10.0.0.0/8 +``` + +### 組件屬性 +```tsx +interface IpDisplayProps { + mobileSimplified?: boolean // 是否使用手機版簡化顯示 +} +``` + +## 故障排除 + +### 1. 顯示為127.0.0.1 +- 檢查是否在本地開發環境 +- 確認網路配置 +- 檢查代理伺服器設置 + +### 2. IPv6格式未正確轉換 +- 確認API端點正常運行 +- 檢查瀏覽器控制台錯誤 +- 驗證網路請求狀態 + +### 3. 詳細信息彈出框不顯示 +- 確認JavaScript已啟用 +- 檢查CSS樣式是否正確載入 +- 驗證組件狀態管理 + +## 開發說明 + +### 1. 本地開發 +```bash +npm run dev +# 訪問 http://localhost:3000 +# 測試頁面: http://localhost:3000/test-ipv6 +``` + +### 2. 生產部署 +```bash +npm run build +npm start +``` + +### 3. 自定義樣式 +組件使用Tailwind CSS,可通過修改類名自定義外觀: +- 背景色: `bg-slate-800/50` +- 邊框: `border-blue-800/30` +- 文字顏色: `text-blue-200` + +## 更新日誌 + +### v1.0.0 (當前版本) +- ✅ 實現IPv6格式IPv4地址自動檢測 +- ✅ 添加雙格式顯示功能 +- ✅ 實現響應式設計 +- ✅ 添加詳細信息彈出框 +- ✅ 創建測試頁面 +- ✅ 完善API端點支援 + +## 未來計劃 + +- [ ] 添加更多IPv6格式支援 +- [ ] 實現IP地理位置顯示 +- [ ] 添加IP歷史記錄功能 +- [ ] 支援自定義主題 +- [ ] 添加更多調試工具 \ No newline at end of file diff --git a/app/api/ip/route.ts b/app/api/ip/route.ts index b9ca328..ade61ad 100644 --- a/app/api/ip/route.ts +++ b/app/api/ip/route.ts @@ -10,6 +10,43 @@ export async function GET(request: NextRequest) { 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 @@ -17,8 +54,13 @@ export async function GET(request: NextRequest) { if (forwardedFor) { const ips = forwardedFor.split(',').map(ip => ip.trim()); for (const ip of ips) { - if (ip && ip !== '127.0.0.1' && ip !== '::1' && ip !== 'localhost') { - clientIp = ip; + // 處理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; } } @@ -26,13 +68,25 @@ export async function GET(request: NextRequest) { // 檢查其他可能的IP來源 const realIp = request.headers.get('x-real-ip'); - if (realIp && realIp !== '127.0.0.1') { - clientIp = realIp; + 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 && clientIpHeader !== '127.0.0.1') { - clientIp = clientIpHeader; + if (clientIpHeader) { + let cleanClientIp = clientIpHeader; + if (clientIpHeader.startsWith('::ffff:')) { + cleanClientIp = clientIpHeader.substring(7); + } + if (cleanClientIp !== '127.0.0.1') { + clientIp = cleanClientIp; + } } } @@ -51,12 +105,24 @@ export async function GET(request: NextRequest) { } } + // 確保最終返回的IP是IPv4格式 + const finalIp = ensureIPv4Format(clientIp); + const originalIp = detailedInfo.detectedIp; + const isIPv6Mapped = isIPv6MappedIPv4(originalIp); + const ipv6Format = getIPv6MappedFormat(finalIp); + return NextResponse.json({ - ip: clientIp, + 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, @@ -67,7 +133,9 @@ export async function GET(request: NextRequest) { referer: request.headers.get('referer'), userAgent: request.headers.get('user-agent'), originalDetectedIp: detailedInfo.detectedIp, - finalDetectedIp: clientIp, + finalDetectedIp: finalIp, + rawDetectedIp: clientIp, // 保留原始檢測到的IP用於調試 + ipDetectionMethod: isIPv6Mapped ? 'IPv6-Mapped-IPv4' : 'Standard-IPv4' }, location: locationInfo, // 本地開發環境的特殊信息 diff --git a/app/test-ipv6/page.tsx b/app/test-ipv6/page.tsx new file mode 100644 index 0000000..b2c0e05 --- /dev/null +++ b/app/test-ipv6/page.tsx @@ -0,0 +1,126 @@ +"use client" + +import { useEffect, useState } from "react" +import IpDisplay from "@/components/ip-display" + +interface IpInfo { + ip: string + ipv6Info?: { + isIPv6Mapped: boolean + originalFormat: string + ipv6Format: string + hasIPv6Support: boolean + } + debug?: { + originalDetectedIp?: string + finalDetectedIp?: string + rawDetectedIp?: string + allFoundIps?: string[] + ipDetectionMethod?: string + } +} + +export default function TestIPv6Page() { + const [ipInfo, setIpInfo] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchIpInfo = async () => { + try { + const response = await fetch('/api/ip') + if (response.ok) { + const data = await response.json() + setIpInfo(data) + } + } catch (error) { + console.error("Error fetching IP info:", error) + } finally { + setLoading(false) + } + } + + fetchIpInfo() + }, []) + + return ( +
+
+

+ IPv6格式IPv4地址測試頁面 +

+ +
+

+ IP顯示組件測試 +

+ +
+

桌面版顯示:

+ +
+ +
+

手機版顯示:

+ +
+
+ + {loading ? ( +
+

載入中...

+
+ ) : ipInfo ? ( +
+

+ 原始API數據 +

+ +
+
+

基本信息:

+
+

IPv4地址: {ipInfo.ip}

+ {ipInfo.ipv6Info && ( + <> +

是否為IPv6映射: {ipInfo.ipv6Info.isIPv6Mapped ? '是' : '否'}

+

原始格式: {ipInfo.ipv6Info.originalFormat}

+

IPv6格式: {ipInfo.ipv6Info.ipv6Format}

+

IPv6支援: {ipInfo.ipv6Info.hasIPv6Support ? '已啟用' : '未啟用'}

+ + )} +
+
+ + {ipInfo.debug && ( +
+

調試信息:

+
+

檢測方法: {ipInfo.debug.ipDetectionMethod || '未知'}

+

原始檢測IP: {ipInfo.debug.originalDetectedIp || '無'}

+

最終檢測IP: {ipInfo.debug.finalDetectedIp || '無'}

+

原始檢測IP: {ipInfo.debug.rawDetectedIp || '無'}

+ + {ipInfo.debug.allFoundIps && ipInfo.debug.allFoundIps.length > 0 && ( +
+

所有檢測到的IP:

+
    + {ipInfo.debug.allFoundIps.map((ip, index) => ( +
  • {ip}
  • + ))} +
+
+ )} +
+
+ )} +
+
+ ) : ( +
+

無法獲取IP信息

+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/app/test/ip-debug/page.tsx b/app/test/ip-debug/page.tsx index f86a17c..f619a78 100644 --- a/app/test/ip-debug/page.tsx +++ b/app/test/ip-debug/page.tsx @@ -3,10 +3,9 @@ import { useState, useEffect } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Separator } from '@/components/ui/separator' import { Alert, AlertDescription } from '@/components/ui/alert' -import { Globe, Shield, MapPin, RefreshCw, AlertCircle, CheckCircle, Info, Lightbulb } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Globe, Info, AlertCircle, CheckCircle, RefreshCw } from 'lucide-react' interface IpDebugInfo { ip: string @@ -23,12 +22,11 @@ interface IpDebugInfo { host: string | null referer: string | null userAgent: string | null + originalDetectedIp: string + finalDetectedIp: string } location: any - development: { - message: string - suggestions: string[] - } | null + development: any } export default function IpDebugPage() { @@ -47,8 +45,8 @@ export default function IpDebugPage() { const data = await response.json() setIpInfo(data) } catch (error) { - console.error("無法獲取IP信息:", error) - setError("無法獲取IP信息") + console.error('Error fetching IP info:', error) + setError('無法獲取IP信息') } finally { setLoading(false) } @@ -60,12 +58,10 @@ export default function IpDebugPage() { if (loading) { return ( -
-
-
- - 載入中... -
+
+
+ +

載入中...

) @@ -73,262 +69,197 @@ export default function IpDebugPage() { if (error || !ipInfo) { return ( -
- - - - - 錯誤 - - - -

{error || '無法獲取IP信息'}

- -
-
+
+ + + {error} +
) } return ( -
+
-

IP 檢測調試工具

-

查看詳細的IP檢測信息和調試數據

+

IP 調試信息

+

詳細的IP檢測和調試信息

- {/* 本地開發環境提示 */} - {ipInfo.development && ( - - - -
{ipInfo.development.message}
-
- {ipInfo.development.suggestions.map((suggestion, index) => ( -
- - {suggestion} -
- ))} -
-
-
- )} - {/* 主要IP信息 */} - IP 信息 + 檢測到的IP地址 + + 系統檢測到的主要IP地址信息 + -
-
- 檢測到的IP: - +
+
+

最終檢測到的IP

+ {ipInfo.ip}
-
- 狀態: - {ipInfo.isAllowed ? ( - - - 允許 - - ) : ( - - - 拒絕 - - )} -
-
- -
-
- - IP白名單: - - {ipInfo.enableIpWhitelist ? '已啟用' : '已停用'} +
+

原始檢測到的IP

+ + {ipInfo.debug.originalDetectedIp}
-
- 環境: - {ipInfo.debug.environment} -
- {ipInfo.debug.isLocalDevelopment && ipInfo.debug.localIp && ( -
- 本機IP: - {ipInfo.debug.localIp} -
- )}
- - {ipInfo.location && ( -
- - 位置: - - {ipInfo.location.city}, {ipInfo.location.country} ({ipInfo.location.isp}) - + +
+
+

IP白名單狀態

+ + {ipInfo.isAllowed ? '允許' : '拒絕'} +
- )} +
+

白名單功能

+ + {ipInfo.enableIpWhitelist ? '已啟用' : '已禁用'} + +
+
+

環境

+ + {ipInfo.debug.environment} + +
+
{/* 所有找到的IP */} - 所有檢測到的IP - 系統檢測到的所有IP地址 + 所有檢測到的IP地址 + + 從各種來源檢測到的所有IP地址 + -
+
{ipInfo.debug.allFoundIps.length > 0 ? ( ipInfo.debug.allFoundIps.map((ip, index) => ( - - {ip} {ip === ipInfo.ip && '(已選擇)'} - +
+ + {ip} + + {ip === ipInfo.ip && ( + + 最終選擇 + + )} +
)) ) : ( - 未檢測到任何IP +

沒有檢測到任何IP地址

)}
- {/* 允許的IP列表 */} - {ipInfo.enableIpWhitelist && ( - - - 允許的IP地址 - 當前配置的IP白名單 - - -
- {ipInfo.allowedIps.length > 0 ? ( - ipInfo.allowedIps.map((ip, index) => ( - - {ip} - - )) - ) : ( - 未配置IP白名單 - )} -
-
-
- )} - - {/* 調試信息 */} + {/* IP來源詳細信息 */} - 調試信息 - 所有可能的IP來源和請求頭信息 + IP來源詳細信息 + + 各種HTTP頭和連接信息中的IP地址 + -
-
-

所有IP來源:

-
- {Object.entries(ipInfo.debug.allIpSources).map(([key, value]) => ( -
- {key}: - - {value || 'null'} - -
- ))} +
+ {Object.entries(ipInfo.debug.allIpSources).map(([source, value]) => ( +
+ {source}: + + {value || '未設置'} +
-
- - - -
-

請求信息:

-
-
- Host: - - {ipInfo.debug.host || 'null'} - -
-
- Referer: - - {ipInfo.debug.referer || 'null'} - -
-
- User Agent: - - {ipInfo.debug.userAgent || 'null'} - -
-
- 時間戳: - - {new Date(ipInfo.timestamp).toLocaleString('zh-TW')} - -
-
-
+ ))}
+ {/* 本地開發環境信息 */} + {ipInfo.development && ( + + + +
{ipInfo.development.message}
+
+ {ipInfo.development.suggestions.map((suggestion, index) => ( +
• {suggestion}
+ ))} +
+
+
+ )} + {/* 地理位置信息 */} {ipInfo.location && ( - - - 地理位置信息 - + 地理位置信息 + + IP地址的地理位置信息 + -
-
-

位置信息

-
-
國家: {ipInfo.location.country} ({ipInfo.location.countryCode})
-
地區: {ipInfo.location.regionName}
-
城市: {ipInfo.location.city}
-
郵遞區號: {ipInfo.location.zip}
-
時區: {ipInfo.location.timezone}
-
-
-
-

網路信息

-
-
ISP: {ipInfo.location.isp}
-
組織: {ipInfo.location.org}
-
AS: {ipInfo.location.as}
-
行動網路: {ipInfo.location.mobile ? '是' : '否'}
-
代理: {ipInfo.location.proxy ? '是' : '否'}
-
主機服務: {ipInfo.location.hosting ? '是' : '否'}
-
-
-
+
+              {JSON.stringify(ipInfo.location, null, 2)}
+            
)} + {/* 其他調試信息 */} + + + 其他調試信息 + + 額外的調試和環境信息 + + + +
+
+
+

主機

+

{ipInfo.debug.host || '未設置'}

+
+
+

引用來源

+

{ipInfo.debug.referer || '未設置'}

+
+
+
+

用戶代理

+

{ipInfo.debug.userAgent || '未設置'}

+
+
+

時間戳

+

{ipInfo.timestamp}

+
+
+
+
+ {/* 操作按鈕 */} -
+
+
diff --git a/app/test/ip-format-test/page.tsx b/app/test/ip-format-test/page.tsx new file mode 100644 index 0000000..0d37813 --- /dev/null +++ b/app/test/ip-format-test/page.tsx @@ -0,0 +1,246 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Globe, RefreshCw, CheckCircle, AlertCircle, Info } from 'lucide-react' + +interface IpTestResult { + originalIp: string + cleanedIp: string + isIPv4: boolean + isIPv6: boolean + isLocalhost: boolean + description: string +} + +export default function IpFormatTestPage() { + const [testResults, setTestResults] = useState([]) + const [loading, setLoading] = useState(false) + + // 測試IP地址清理函數 + function cleanIpForDisplay(ip: string): string { + if (!ip) return '127.0.0.1'; + + // 移除空白字符 + ip = ip.trim(); + + // 處理IPv6格式的IPv4地址 (例如: ::ffff:192.168.1.1) + 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'; + } + + // 檢查IP類型 + function analyzeIp(ip: string): { isIPv4: boolean; isIPv6: boolean; isLocalhost: boolean; description: string } { + 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]?)$/; + const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; + + const isIPv4 = ipv4Regex.test(ip); + const isIPv6 = ipv6Regex.test(ip) || ip.startsWith('::ffff:') || ip === '::1'; + const isLocalhost = ip === '127.0.0.1' || ip === '::1' || ip === 'localhost'; + + let description = ''; + if (ip.startsWith('::ffff:')) { + description = 'IPv6格式的IPv4地址'; + } else if (ip === '::1') { + description = 'IPv6本地回環地址'; + } else if (isIPv4) { + description = '標準IPv4地址'; + } else if (isIPv6) { + description = 'IPv6地址'; + } else { + description = '無效的IP地址格式'; + } + + return { isIPv4, isIPv6, isLocalhost, description }; + } + + const runTests = () => { + setLoading(true); + + const testIps = [ + '::ffff:127.0.0.1', + '::1', + '127.0.0.1', + '192.168.1.1', + '::ffff:192.168.1.100', + '2001:db8::1', + 'invalid-ip', + 'localhost', + '::ffff:203.0.113.1', + '10.0.0.1' + ]; + + const results: IpTestResult[] = testIps.map(originalIp => { + const cleanedIp = cleanIpForDisplay(originalIp); + const analysis = analyzeIp(originalIp); + + return { + originalIp, + cleanedIp, + ...analysis + }; + }); + + setTestResults(results); + setLoading(false); + }; + + useEffect(() => { + runTests(); + }, []); + + return ( +
+
+

IPv4 格式測試

+

測試IP地址清理和IPv4格式轉換功能

+
+ + {/* 說明 */} + + + +
測試目的
+
+
• 驗證IPv6格式的IPv4地址能正確轉換為IPv4
+
• 確保所有IP地址都顯示為標準IPv4格式
+
• 測試各種IP地址格式的處理邏輯
+
• 驗證本地回環地址的正確處理
+
+
+
+ + {/* 測試結果 */} + + + + + IP格式轉換測試結果 + + + 各種IP地址格式的清理和轉換結果 + + + +
+ {testResults.map((result, index) => ( +
+
+
+ 原始IP: + + {result.originalIp} + +
+
+ 清理後: + + {result.cleanedIp} + +
+
+ +
+
+ {result.isIPv4 ? ( + + ) : ( + + )} + IPv4: {result.isIPv4 ? '是' : '否'} +
+ +
+ {result.isIPv6 ? ( + + ) : ( + - + )} + IPv6: {result.isIPv6 ? '是' : '否'} +
+ +
+ {result.isLocalhost ? ( + + ) : ( + - + )} + 本地: {result.isLocalhost ? '是' : '否'} +
+
+ +
+ {result.description} +
+
+ ))} +
+
+
+ + {/* 操作按鈕 */} +
+ +
+ + {/* 總結 */} + + + 測試總結 + + IPv4格式轉換的關鍵點 + + + +
+
+

✅ 正確處理的格式

+
    +
  • ::ffff:127.0.0.1127.0.0.1
  • +
  • ::1127.0.0.1
  • +
  • 127.0.0.1127.0.0.1 (保持不變)
  • +
  • 192.168.1.1192.168.1.1 (保持不變)
  • +
+
+ +
+

⚠️ 無效格式處理

+
    +
  • • 無效IP地址 → 127.0.0.1 (默認值)
  • +
  • • 空值或null → 127.0.0.1 (默認值)
  • +
+
+ +
+

🎯 目標

+

+ 確保所有顯示的IP地址都是標準的IPv4格式,提供一致且易於理解的用戶體驗。 +

+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/test/ip-test/page.tsx b/app/test/ip-test/page.tsx index 460a2b1..ae1ca99 100644 --- a/app/test/ip-test/page.tsx +++ b/app/test/ip-test/page.tsx @@ -186,6 +186,10 @@ ALLOWED_IPS=你的真實IP地址,其他允許的IP`} 詳細IP調試 +
) } return ( -
- {ipInfo.enableIpWhitelist && !ipInfo.isAllowed ? ( - - ) : ( - - )} - {ipInfo.ip} - {ipInfo.enableIpWhitelist && ( - - {ipInfo.isAllowed ? '允許' : '拒絕'} - +
+
+ {ipInfo.enableIpWhitelist && !ipInfo.isAllowed ? ( + + ) : ( + + )} + +
+ + {isIPv6Mapped ? 'IPv6' : 'IPv4'}: {displayIp} + + {isIPv6Mapped && ( + + {ipv6Format} + + )} +
+ + {ipInfo.enableIpWhitelist && ( + + {ipInfo.isAllowed ? '允許' : '拒絕'} + + )} + + {/* IPv6格式切換按鈕 */} + +
+ + {/* 詳細信息彈出框 */} + {showIPv6Format && ( +
+
+
+ IPv4格式: + {displayIp} +
+
+ IPv6格式: + {ipv6Format} +
+ {originalFormat && originalFormat !== displayIp && ( +
+ 原始格式: + {originalFormat} +
+ )} + {ipInfo.debug?.ipDetectionMethod && ( +
+ 檢測方法: + {ipInfo.debug.ipDetectionMethod} +
+ )} + {ipInfo.debug?.allFoundIps && ipInfo.debug.allFoundIps.length > 0 && ( +
+ 所有檢測到的IP: +
+ {ipInfo.debug.allFoundIps.map((ip, index) => ( +
+ {ip} +
+ ))} +
+
+ )} + {ipInfo.ipv6Info?.hasIPv6Support && ( +
+ ✓ IPv6支援已啟用 +
+ )} +
+
)}
) diff --git a/lib/ip-utils.ts b/lib/ip-utils.ts index d15b55e..78fb480 100644 --- a/lib/ip-utils.ts +++ b/lib/ip-utils.ts @@ -66,6 +66,11 @@ function cleanIpAddress(ip: string): string | null { ip = ip.substring(7); } + // 處理純IPv6本地回環地址 + if (ip === '::1') { + return '127.0.0.1'; + } + // 驗證IP格式 if (!isValidIp(ip)) { return null; @@ -246,6 +251,24 @@ export function getDetailedIpInfo(req: any): { } }); + // 如果沒有找到任何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)) { diff --git a/scripts/test-ip-detection.js b/scripts/test-ip-detection.js index 1195248..62bd7d3 100644 --- a/scripts/test-ip-detection.js +++ b/scripts/test-ip-detection.js @@ -3,158 +3,43 @@ * 用於測試和驗證IP白名單功能 */ -const { getClientIp, isIpAllowed, isValidIp, isValidCidr } = require('../lib/ip-utils.ts'); +const { getClientIp, getDetailedIpInfo, cleanIpAddress } = require('../lib/ip-utils.ts'); // 模擬請求對象 -function createMockRequest(headers = {}) { - return { - headers, - connection: { remoteAddress: '192.168.1.100' }, - socket: { remoteAddress: '192.168.1.100' }, - ip: '192.168.1.100' - }; -} +const mockRequest = { + headers: { + 'x-forwarded-for': '::ffff:127.0.0.1, 192.168.1.100', + 'x-real-ip': '::ffff:127.0.0.1', + 'x-client-ip': '::1', + 'connection': { + 'remoteAddress': '::ffff:127.0.0.1' + }, + 'socket': { + 'remoteAddress': '::1' + } + }, + ip: '::ffff:127.0.0.1' +}; -// 測試IP檢測功能 -function testIpDetection() { - console.log('🧪 開始測試IP檢測功能...\n'); +console.log('=== IP 檢測測試 ==='); - // 測試1: 基本IP檢測 - console.log('📋 測試1: 基本IP檢測'); - const basicRequest = createMockRequest({ - 'x-forwarded-for': '203.0.113.1, 192.168.1.100' - }); - const detectedIp = getClientIp(basicRequest); - console.log(`檢測到的IP: ${detectedIp}`); - console.log(`預期結果: 203.0.113.1 (公網IP)`); - console.log(`實際結果: ${detectedIp === '203.0.113.1' ? '✅ 通過' : '❌ 失敗'}\n`); +// 測試 cleanIpAddress 函數 +console.log('\n1. 測試 cleanIpAddress 函數:'); +console.log('::ffff:127.0.0.1 ->', cleanIpAddress('::ffff:127.0.0.1')); +console.log('::1 ->', cleanIpAddress('::1')); +console.log('127.0.0.1 ->', cleanIpAddress('127.0.0.1')); +console.log('192.168.1.1 ->', cleanIpAddress('192.168.1.1')); - // 測試2: Cloudflare代理 - console.log('📋 測試2: Cloudflare代理'); - const cloudflareRequest = createMockRequest({ - 'cf-connecting-ip': '203.0.113.2' - }); - const cfIp = getClientIp(cloudflareRequest); - console.log(`檢測到的IP: ${cfIp}`); - console.log(`預期結果: 203.0.113.2`); - console.log(`實際結果: ${cfIp === '203.0.113.2' ? '✅ 通過' : '❌ 失敗'}\n`); +// 測試詳細IP信息 +console.log('\n2. 測試詳細IP信息:'); +const detailedInfo = getDetailedIpInfo(mockRequest); +console.log('檢測到的IP:', detailedInfo.detectedIp); +console.log('所有找到的IP:', detailedInfo.allFoundIps); +console.log('IP來源:', detailedInfo.ipSources); - // 測試3: 本地開發環境 - console.log('📋 測試3: 本地開發環境'); - const localRequest = createMockRequest({ - 'x-forwarded-for': '127.0.0.1' - }); - const localIp = getClientIp(localRequest); - console.log(`檢測到的IP: ${localIp}`); - console.log(`預期結果: 127.0.0.1 (本地回環)`); - console.log(`實際結果: ${localIp === '127.0.0.1' ? '✅ 通過' : '❌ 失敗'}\n`); +// 測試客戶端IP獲取 +console.log('\n3. 測試客戶端IP獲取:'); +const clientIp = getClientIp(mockRequest); +console.log('最終檢測到的IP:', clientIp); - // 測試4: 多個代理IP - console.log('📋 測試4: 多個代理IP'); - const multiProxyRequest = createMockRequest({ - 'x-forwarded-for': '203.0.113.3, 10.0.0.1, 192.168.1.1' - }); - const multiIp = getClientIp(multiProxyRequest); - console.log(`檢測到的IP: ${multiIp}`); - console.log(`預期結果: 203.0.113.3 (第一個公網IP)`); - console.log(`實際結果: ${multiIp === '203.0.113.3' ? '✅ 通過' : '❌ 失敗'}\n`); -} - -// 測試IP白名單功能 -function testIpWhitelist() { - console.log('🔒 開始測試IP白名單功能...\n'); - - const allowedIps = '114.33.18.13,125.229.65.83,192.168.1.0/24'; - - // 測試1: 單一IP匹配 - console.log('📋 測試1: 單一IP匹配'); - const testIp1 = '114.33.18.13'; - const result1 = isIpAllowed(testIp1, allowedIps); - console.log(`測試IP: ${testIp1}`); - console.log(`預期結果: true (在白名單中)`); - console.log(`實際結果: ${result1 ? '✅ 通過' : '❌ 失敗'}\n`); - - // 測試2: CIDR範圍匹配 - console.log('📋 測試2: CIDR範圍匹配'); - const testIp2 = '192.168.1.100'; - const result2 = isIpAllowed(testIp2, allowedIps); - console.log(`測試IP: ${testIp2}`); - console.log(`預期結果: true (在192.168.1.0/24範圍內)`); - console.log(`實際結果: ${result2 ? '✅ 通過' : '❌ 失敗'}\n`); - - // 測試3: 不在白名單中的IP - console.log('📋 測試3: 不在白名單中的IP'); - const testIp3 = '203.0.113.1'; - const result3 = isIpAllowed(testIp3, allowedIps); - console.log(`測試IP: ${testIp3}`); - console.log(`預期結果: false (不在白名單中)`); - console.log(`實際結果: ${!result3 ? '✅ 通過' : '❌ 失敗'}\n`); - - // 測試4: 空白名單 - console.log('📋 測試4: 空白名單'); - const testIp4 = '203.0.113.1'; - const result4 = isIpAllowed(testIp4, ''); - console.log(`測試IP: ${testIp4}`); - console.log(`預期結果: true (空白名單允許所有IP)`); - console.log(`實際結果: ${result4 ? '✅ 通過' : '❌ 失敗'}\n`); -} - -// 測試IP格式驗證 -function testIpValidation() { - console.log('🔍 開始測試IP格式驗證...\n'); - - // 測試有效IP - console.log('📋 測試有效IP格式'); - const validIps = ['192.168.1.1', '10.0.0.1', '203.0.113.1']; - validIps.forEach(ip => { - const isValid = isValidIp(ip); - console.log(`${ip}: ${isValid ? '✅ 有效' : '❌ 無效'}`); - }); - console.log(''); - - // 測試無效IP - console.log('📋 測試無效IP格式'); - const invalidIps = ['192.168.1.256', '10.0.0', 'invalid', '192.168.1.1.1']; - invalidIps.forEach(ip => { - const isValid = isValidIp(ip); - console.log(`${ip}: ${!isValid ? '✅ 正確拒絕' : '❌ 錯誤接受'}`); - }); - console.log(''); - - // 測試CIDR格式 - console.log('📋 測試CIDR格式'); - const validCidrs = ['192.168.1.0/24', '10.0.0.0/8', '172.16.0.0/12']; - validCidrs.forEach(cidr => { - const isValid = isValidCidr(cidr); - console.log(`${cidr}: ${isValid ? '✅ 有效' : '❌ 無效'}`); - }); - console.log(''); -} - -// 主測試函數 -function runAllTests() { - console.log('🚀 IP白名單功能測試套件\n'); - console.log('=' * 50); - - try { - testIpDetection(); - testIpWhitelist(); - testIpValidation(); - - console.log('🎉 所有測試完成!'); - } catch (error) { - console.error('❌ 測試過程中發生錯誤:', error); - } -} - -// 如果直接運行此腳本 -if (require.main === module) { - runAllTests(); -} - -module.exports = { - testIpDetection, - testIpWhitelist, - testIpValidation, - runAllTests -}; \ No newline at end of file +console.log('\n=== 測試完成 ==='); \ No newline at end of file