Fix IPv4 display and IP detection logic
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.
This commit is contained in:
100
IP-FIX-SUMMARY.md
Normal file
100
IP-FIX-SUMMARY.md
Normal file
@@ -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. **使用調試工具**:利用新的調試頁面進行問題診斷
|
141
IPV4-DISPLAY-FIX.md
Normal file
141
IPV4-DISPLAY-FIX.md
Normal file
@@ -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 格式顯示,提供一致且易於理解的用戶體驗。
|
162
IPV6-DISPLAY-README.md
Normal file
162
IPV6-DISPLAY-README.md
Normal file
@@ -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"
|
||||||
|
|
||||||
|
// 桌面版完整顯示
|
||||||
|
<IpDisplay />
|
||||||
|
|
||||||
|
// 手機版簡化顯示
|
||||||
|
<IpDisplay mobileSimplified />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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歷史記錄功能
|
||||||
|
- [ ] 支援自定義主題
|
||||||
|
- [ ] 添加更多調試工具
|
@@ -10,6 +10,43 @@ export async function GET(request: NextRequest) {
|
|||||||
const detailedInfo = getDetailedIpInfo(request);
|
const detailedInfo = getDetailedIpInfo(request);
|
||||||
let clientIp = detailedInfo.detectedIp;
|
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
|
// 如果檢測到的是127.0.0.1,嘗試從請求頭獲取真實IP
|
||||||
if (clientIp === '127.0.0.1') {
|
if (clientIp === '127.0.0.1') {
|
||||||
// 檢查是否有代理轉發的真實IP
|
// 檢查是否有代理轉發的真實IP
|
||||||
@@ -17,8 +54,13 @@ export async function GET(request: NextRequest) {
|
|||||||
if (forwardedFor) {
|
if (forwardedFor) {
|
||||||
const ips = forwardedFor.split(',').map(ip => ip.trim());
|
const ips = forwardedFor.split(',').map(ip => ip.trim());
|
||||||
for (const ip of ips) {
|
for (const ip of ips) {
|
||||||
if (ip && ip !== '127.0.0.1' && ip !== '::1' && ip !== 'localhost') {
|
// 處理IPv6格式的IPv4地址
|
||||||
clientIp = ip;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,13 +68,25 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// 檢查其他可能的IP來源
|
// 檢查其他可能的IP來源
|
||||||
const realIp = request.headers.get('x-real-ip');
|
const realIp = request.headers.get('x-real-ip');
|
||||||
if (realIp && realIp !== '127.0.0.1') {
|
if (realIp) {
|
||||||
clientIp = 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');
|
const clientIpHeader = request.headers.get('x-client-ip');
|
||||||
if (clientIpHeader && clientIpHeader !== '127.0.0.1') {
|
if (clientIpHeader) {
|
||||||
clientIp = 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({
|
return NextResponse.json({
|
||||||
ip: clientIp,
|
ip: finalIp,
|
||||||
isAllowed,
|
isAllowed,
|
||||||
enableIpWhitelist,
|
enableIpWhitelist,
|
||||||
allowedIps: enableIpWhitelist ? allowedIps.split(',').map(ip => ip.trim()) : [],
|
allowedIps: enableIpWhitelist ? allowedIps.split(',').map(ip => ip.trim()) : [],
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
ipv6Info: {
|
||||||
|
isIPv6Mapped,
|
||||||
|
originalFormat: originalIp,
|
||||||
|
ipv6Format,
|
||||||
|
hasIPv6Support: true
|
||||||
|
},
|
||||||
debug: {
|
debug: {
|
||||||
allIpSources: detailedInfo.ipSources,
|
allIpSources: detailedInfo.ipSources,
|
||||||
allFoundIps: detailedInfo.allFoundIps,
|
allFoundIps: detailedInfo.allFoundIps,
|
||||||
@@ -67,7 +133,9 @@ export async function GET(request: NextRequest) {
|
|||||||
referer: request.headers.get('referer'),
|
referer: request.headers.get('referer'),
|
||||||
userAgent: request.headers.get('user-agent'),
|
userAgent: request.headers.get('user-agent'),
|
||||||
originalDetectedIp: detailedInfo.detectedIp,
|
originalDetectedIp: detailedInfo.detectedIp,
|
||||||
finalDetectedIp: clientIp,
|
finalDetectedIp: finalIp,
|
||||||
|
rawDetectedIp: clientIp, // 保留原始檢測到的IP用於調試
|
||||||
|
ipDetectionMethod: isIPv6Mapped ? 'IPv6-Mapped-IPv4' : 'Standard-IPv4'
|
||||||
},
|
},
|
||||||
location: locationInfo,
|
location: locationInfo,
|
||||||
// 本地開發環境的特殊信息
|
// 本地開發環境的特殊信息
|
||||||
|
126
app/test-ipv6/page.tsx
Normal file
126
app/test-ipv6/page.tsx
Normal file
@@ -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<IpInfo | null>(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 (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-blue-900 to-indigo-900 p-8">
|
||||||
|
<div className="container mx-auto max-w-4xl">
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-8 text-center">
|
||||||
|
IPv6格式IPv4地址測試頁面
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="bg-slate-800/50 backdrop-blur-sm rounded-lg p-6 border border-blue-800/30">
|
||||||
|
<h2 className="text-xl font-semibold text-blue-200 mb-4">
|
||||||
|
IP顯示組件測試
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-lg text-blue-300 mb-2">桌面版顯示:</h3>
|
||||||
|
<IpDisplay />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-lg text-blue-300 mb-2">手機版顯示:</h3>
|
||||||
|
<IpDisplay mobileSimplified />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="bg-slate-800/50 backdrop-blur-sm rounded-lg p-6 border border-blue-800/30 mt-6">
|
||||||
|
<p className="text-blue-200">載入中...</p>
|
||||||
|
</div>
|
||||||
|
) : ipInfo ? (
|
||||||
|
<div className="bg-slate-800/50 backdrop-blur-sm rounded-lg p-6 border border-blue-800/30 mt-6">
|
||||||
|
<h2 className="text-xl font-semibold text-blue-200 mb-4">
|
||||||
|
原始API數據
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg text-blue-300 mb-2">基本信息:</h3>
|
||||||
|
<div className="bg-slate-900/50 p-4 rounded border border-slate-700">
|
||||||
|
<p className="text-white"><span className="text-blue-200">IPv4地址:</span> {ipInfo.ip}</p>
|
||||||
|
{ipInfo.ipv6Info && (
|
||||||
|
<>
|
||||||
|
<p className="text-white"><span className="text-blue-200">是否為IPv6映射:</span> {ipInfo.ipv6Info.isIPv6Mapped ? '是' : '否'}</p>
|
||||||
|
<p className="text-white"><span className="text-blue-200">原始格式:</span> {ipInfo.ipv6Info.originalFormat}</p>
|
||||||
|
<p className="text-white"><span className="text-blue-200">IPv6格式:</span> {ipInfo.ipv6Info.ipv6Format}</p>
|
||||||
|
<p className="text-white"><span className="text-blue-200">IPv6支援:</span> {ipInfo.ipv6Info.hasIPv6Support ? '已啟用' : '未啟用'}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ipInfo.debug && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg text-blue-300 mb-2">調試信息:</h3>
|
||||||
|
<div className="bg-slate-900/50 p-4 rounded border border-slate-700">
|
||||||
|
<p className="text-white"><span className="text-blue-200">檢測方法:</span> {ipInfo.debug.ipDetectionMethod || '未知'}</p>
|
||||||
|
<p className="text-white"><span className="text-blue-200">原始檢測IP:</span> {ipInfo.debug.originalDetectedIp || '無'}</p>
|
||||||
|
<p className="text-white"><span className="text-blue-200">最終檢測IP:</span> {ipInfo.debug.finalDetectedIp || '無'}</p>
|
||||||
|
<p className="text-white"><span className="text-blue-200">原始檢測IP:</span> {ipInfo.debug.rawDetectedIp || '無'}</p>
|
||||||
|
|
||||||
|
{ipInfo.debug.allFoundIps && ipInfo.debug.allFoundIps.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-blue-200">所有檢測到的IP:</p>
|
||||||
|
<ul className="list-disc list-inside text-yellow-300 ml-4">
|
||||||
|
{ipInfo.debug.allFoundIps.map((ip, index) => (
|
||||||
|
<li key={index}>{ip}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-red-900/50 backdrop-blur-sm rounded-lg p-6 border border-red-800/30 mt-6">
|
||||||
|
<p className="text-red-200">無法獲取IP信息</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -3,10 +3,9 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 { 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 {
|
interface IpDebugInfo {
|
||||||
ip: string
|
ip: string
|
||||||
@@ -23,12 +22,11 @@ interface IpDebugInfo {
|
|||||||
host: string | null
|
host: string | null
|
||||||
referer: string | null
|
referer: string | null
|
||||||
userAgent: string | null
|
userAgent: string | null
|
||||||
|
originalDetectedIp: string
|
||||||
|
finalDetectedIp: string
|
||||||
}
|
}
|
||||||
location: any
|
location: any
|
||||||
development: {
|
development: any
|
||||||
message: string
|
|
||||||
suggestions: string[]
|
|
||||||
} | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IpDebugPage() {
|
export default function IpDebugPage() {
|
||||||
@@ -47,8 +45,8 @@ export default function IpDebugPage() {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setIpInfo(data)
|
setIpInfo(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("無法獲取IP信息:", error)
|
console.error('Error fetching IP info:', error)
|
||||||
setError("無法獲取IP信息")
|
setError('無法獲取IP信息')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -60,12 +58,10 @@ export default function IpDebugPage() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6 max-w-4xl">
|
<div className="container mx-auto p-6 max-w-6xl">
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<div className="text-center">
|
||||||
<div className="flex items-center gap-2">
|
<RefreshCw className="w-8 h-8 animate-spin mx-auto mb-4" />
|
||||||
<RefreshCw className="w-5 h-5 animate-spin" />
|
<p>載入中...</p>
|
||||||
<span>載入中...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -73,262 +69,197 @@ export default function IpDebugPage() {
|
|||||||
|
|
||||||
if (error || !ipInfo) {
|
if (error || !ipInfo) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6 max-w-4xl">
|
<div className="container mx-auto p-6 max-w-6xl">
|
||||||
<Card className="border-red-200 bg-red-50">
|
<Alert variant="destructive">
|
||||||
<CardHeader>
|
<AlertCircle className="h-4 w-4" />
|
||||||
<CardTitle className="flex items-center gap-2 text-red-800">
|
<AlertDescription>{error}</AlertDescription>
|
||||||
<AlertCircle className="w-5 h-5" />
|
</Alert>
|
||||||
錯誤
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-red-700">{error || '無法獲取IP信息'}</p>
|
|
||||||
<Button onClick={fetchIpInfo} className="mt-4">
|
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
|
||||||
重試
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6 max-w-4xl space-y-6">
|
<div className="container mx-auto p-6 max-w-6xl space-y-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-3xl font-bold mb-2">IP 檢測調試工具</h1>
|
<h1 className="text-3xl font-bold mb-2">IP 調試信息</h1>
|
||||||
<p className="text-muted-foreground">查看詳細的IP檢測信息和調試數據</p>
|
<p className="text-muted-foreground">詳細的IP檢測和調試信息</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 本地開發環境提示 */}
|
|
||||||
{ipInfo.development && (
|
|
||||||
<Alert className="border-blue-200 bg-blue-50">
|
|
||||||
<Info className="h-4 w-4 text-blue-600" />
|
|
||||||
<AlertDescription className="text-blue-800">
|
|
||||||
<div className="font-medium mb-2">{ipInfo.development.message}</div>
|
|
||||||
<div className="text-sm space-y-1">
|
|
||||||
{ipInfo.development.suggestions.map((suggestion, index) => (
|
|
||||||
<div key={index} className="flex items-start gap-2">
|
|
||||||
<Lightbulb className="w-3 h-3 mt-0.5 text-blue-500 flex-shrink-0" />
|
|
||||||
<span>{suggestion}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 主要IP信息 */}
|
{/* 主要IP信息 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Globe className="w-5 h-5" />
|
<Globe className="w-5 h-5" />
|
||||||
IP 信息
|
檢測到的IP地址
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
系統檢測到的主要IP地址信息
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<span className="font-medium">檢測到的IP:</span>
|
<h4 className="font-medium mb-2">最終檢測到的IP</h4>
|
||||||
<Badge variant={ipInfo.ip === '127.0.0.1' ? 'destructive' : 'default'}>
|
<Badge variant="default" className="text-sm">
|
||||||
{ipInfo.ip}
|
{ipInfo.ip}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<span className="font-medium">狀態:</span>
|
<h4 className="font-medium mb-2">原始檢測到的IP</h4>
|
||||||
{ipInfo.isAllowed ? (
|
<Badge variant="outline" className="text-sm">
|
||||||
<Badge variant="default" className="flex items-center gap-1">
|
{ipInfo.debug.originalDetectedIp}
|
||||||
<CheckCircle className="w-3 h-3" />
|
|
||||||
允許
|
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
|
||||||
<Badge variant="destructive" className="flex items-center gap-1">
|
|
||||||
<AlertCircle className="w-3 h-3" />
|
|
||||||
拒絕
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<Shield className="w-4 h-4" />
|
<h4 className="font-medium mb-2">IP白名單狀態</h4>
|
||||||
<span className="text-sm">IP白名單:</span>
|
<Badge variant={ipInfo.isAllowed ? "default" : "destructive"}>
|
||||||
<Badge variant={ipInfo.enableIpWhitelist ? 'default' : 'secondary'}>
|
{ipInfo.isAllowed ? '允許' : '拒絕'}
|
||||||
{ipInfo.enableIpWhitelist ? '已啟用' : '已停用'}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<span className="text-sm">環境:</span>
|
<h4 className="font-medium mb-2">白名單功能</h4>
|
||||||
<Badge variant="outline">{ipInfo.debug.environment}</Badge>
|
<Badge variant={ipInfo.enableIpWhitelist ? "default" : "secondary"}>
|
||||||
|
{ipInfo.enableIpWhitelist ? '已啟用' : '已禁用'}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{ipInfo.debug.isLocalDevelopment && ipInfo.debug.localIp && (
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<h4 className="font-medium mb-2">環境</h4>
|
||||||
<span className="text-sm">本機IP:</span>
|
<Badge variant="outline">
|
||||||
<Badge variant="outline">{ipInfo.debug.localIp}</Badge>
|
{ipInfo.debug.environment}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ipInfo.location && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MapPin className="w-4 h-4" />
|
|
||||||
<span className="text-sm">位置:</span>
|
|
||||||
<span className="text-sm">
|
|
||||||
{ipInfo.location.city}, {ipInfo.location.country} ({ipInfo.location.isp})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 所有找到的IP */}
|
{/* 所有找到的IP */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>所有檢測到的IP</CardTitle>
|
<CardTitle>所有檢測到的IP地址</CardTitle>
|
||||||
<CardDescription>系統檢測到的所有IP地址</CardDescription>
|
<CardDescription>
|
||||||
|
從各種來源檢測到的所有IP地址
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="space-y-2">
|
||||||
{ipInfo.debug.allFoundIps.length > 0 ? (
|
{ipInfo.debug.allFoundIps.length > 0 ? (
|
||||||
ipInfo.debug.allFoundIps.map((ip, index) => (
|
ipInfo.debug.allFoundIps.map((ip, index) => (
|
||||||
<Badge
|
<div key={index} className="flex items-center gap-2">
|
||||||
key={index}
|
<Badge variant="outline" className="font-mono">
|
||||||
variant={ip === ipInfo.ip ? 'default' : 'outline'}
|
|
||||||
className={ip === '127.0.0.1' ? 'border-red-300 text-red-700' : ''}
|
|
||||||
>
|
|
||||||
{ip} {ip === ipInfo.ip && '(已選擇)'}
|
|
||||||
</Badge>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">未檢測到任何IP</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 允許的IP列表 */}
|
|
||||||
{ipInfo.enableIpWhitelist && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>允許的IP地址</CardTitle>
|
|
||||||
<CardDescription>當前配置的IP白名單</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{ipInfo.allowedIps.length > 0 ? (
|
|
||||||
ipInfo.allowedIps.map((ip, index) => (
|
|
||||||
<Badge key={index} variant="outline">
|
|
||||||
{ip}
|
{ip}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{ip === ipInfo.ip && (
|
||||||
|
<Badge variant="default" className="text-xs">
|
||||||
|
最終選擇
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">未配置IP白名單</span>
|
<p className="text-muted-foreground">沒有檢測到任何IP地址</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 調試信息 */}
|
{/* IP來源詳細信息 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>調試信息</CardTitle>
|
<CardTitle>IP來源詳細信息</CardTitle>
|
||||||
<CardDescription>所有可能的IP來源和請求頭信息</CardDescription>
|
<CardDescription>
|
||||||
|
各種HTTP頭和連接信息中的IP地址
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div>
|
{Object.entries(ipInfo.debug.allIpSources).map(([source, value]) => (
|
||||||
<h4 className="font-medium mb-2">所有IP來源:</h4>
|
<div key={source} className="flex items-center justify-between p-2 bg-muted rounded">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
<span className="font-medium text-sm">{source}:</span>
|
||||||
{Object.entries(ipInfo.debug.allIpSources).map(([key, value]) => (
|
<span className="font-mono text-sm">
|
||||||
<div key={key} className="flex justify-between items-center p-2 bg-muted rounded">
|
{value || '未設置'}
|
||||||
<span className="text-sm font-mono">{key}:</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{value || 'null'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">請求信息:</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">Host:</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{ipInfo.debug.host || 'null'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">Referer:</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{ipInfo.debug.referer || 'null'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">User Agent:</span>
|
|
||||||
<span className="text-sm text-muted-foreground max-w-xs truncate">
|
|
||||||
{ipInfo.debug.userAgent || 'null'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm">時間戳:</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{new Date(ipInfo.timestamp).toLocaleString('zh-TW')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 本地開發環境信息 */}
|
||||||
|
{ipInfo.development && (
|
||||||
|
<Alert>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="font-medium mb-2">{ipInfo.development.message}</div>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
{ipInfo.development.suggestions.map((suggestion, index) => (
|
||||||
|
<div key={index}>• {suggestion}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 地理位置信息 */}
|
{/* 地理位置信息 */}
|
||||||
{ipInfo.location && (
|
{ipInfo.location && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle>地理位置信息</CardTitle>
|
||||||
<MapPin className="w-5 h-5" />
|
<CardDescription>
|
||||||
地理位置信息
|
IP地址的地理位置信息
|
||||||
</CardTitle>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<pre className="bg-muted p-4 rounded text-sm overflow-auto">
|
||||||
<div>
|
{JSON.stringify(ipInfo.location, null, 2)}
|
||||||
<h4 className="font-medium mb-2">位置信息</h4>
|
</pre>
|
||||||
<div className="space-y-1 text-sm">
|
|
||||||
<div>國家: {ipInfo.location.country} ({ipInfo.location.countryCode})</div>
|
|
||||||
<div>地區: {ipInfo.location.regionName}</div>
|
|
||||||
<div>城市: {ipInfo.location.city}</div>
|
|
||||||
<div>郵遞區號: {ipInfo.location.zip}</div>
|
|
||||||
<div>時區: {ipInfo.location.timezone}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">網路信息</h4>
|
|
||||||
<div className="space-y-1 text-sm">
|
|
||||||
<div>ISP: {ipInfo.location.isp}</div>
|
|
||||||
<div>組織: {ipInfo.location.org}</div>
|
|
||||||
<div>AS: {ipInfo.location.as}</div>
|
|
||||||
<div>行動網路: {ipInfo.location.mobile ? '是' : '否'}</div>
|
|
||||||
<div>代理: {ipInfo.location.proxy ? '是' : '否'}</div>
|
|
||||||
<div>主機服務: {ipInfo.location.hosting ? '是' : '否'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 其他調試信息 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>其他調試信息</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
額外的調試和環境信息
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">主機</h4>
|
||||||
|
<p className="text-sm font-mono">{ipInfo.debug.host || '未設置'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">引用來源</h4>
|
||||||
|
<p className="text-sm font-mono">{ipInfo.debug.referer || '未設置'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">用戶代理</h4>
|
||||||
|
<p className="text-sm font-mono break-all">{ipInfo.debug.userAgent || '未設置'}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">時間戳</h4>
|
||||||
|
<p className="text-sm">{ipInfo.timestamp}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 操作按鈕 */}
|
{/* 操作按鈕 */}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center gap-4">
|
||||||
<Button onClick={fetchIpInfo} className="flex items-center gap-2">
|
<Button onClick={fetchIpInfo} className="flex items-center gap-2">
|
||||||
<RefreshCw className="w-4 h-4" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
重新檢測IP
|
重新檢測
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => window.open('/api/ip', '_blank')}>
|
||||||
|
<Globe className="w-4 h-4 mr-2" />
|
||||||
|
查看API響應
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
246
app/test/ip-format-test/page.tsx
Normal file
246
app/test/ip-format-test/page.tsx
Normal file
@@ -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<IpTestResult[]>([])
|
||||||
|
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 (
|
||||||
|
<div className="container mx-auto p-6 max-w-6xl space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl font-bold mb-2">IPv4 格式測試</h1>
|
||||||
|
<p className="text-muted-foreground">測試IP地址清理和IPv4格式轉換功能</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 說明 */}
|
||||||
|
<Alert>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="font-medium mb-2">測試目的</div>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div>• 驗證IPv6格式的IPv4地址能正確轉換為IPv4</div>
|
||||||
|
<div>• 確保所有IP地址都顯示為標準IPv4格式</div>
|
||||||
|
<div>• 測試各種IP地址格式的處理邏輯</div>
|
||||||
|
<div>• 驗證本地回環地址的正確處理</div>
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* 測試結果 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Globe className="w-5 h-5" />
|
||||||
|
IP格式轉換測試結果
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
各種IP地址格式的清理和轉換結果
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{testResults.map((result, index) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">原始IP:</span>
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
{result.originalIp}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">清理後:</span>
|
||||||
|
<Badge variant="default" className="font-mono">
|
||||||
|
{result.cleanedIp}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{result.isIPv4 ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span>IPv4: {result.isIPv4 ? '是' : '否'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{result.isIPv6 ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
|
<span>IPv6: {result.isIPv6 ? '是' : '否'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{result.isLocalhost ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-orange-500" />
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
|
<span>本地: {result.isLocalhost ? '是' : '否'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{result.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button onClick={runTests} disabled={loading} className="flex items-center gap-2">
|
||||||
|
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
{loading ? '測試中...' : '重新測試'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 總結 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>測試總結</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
IPv4格式轉換的關鍵點
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">✅ 正確處理的格式</h4>
|
||||||
|
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||||
|
<li>• <code>::ffff:127.0.0.1</code> → <code>127.0.0.1</code></li>
|
||||||
|
<li>• <code>::1</code> → <code>127.0.0.1</code></li>
|
||||||
|
<li>• <code>127.0.0.1</code> → <code>127.0.0.1</code> (保持不變)</li>
|
||||||
|
<li>• <code>192.168.1.1</code> → <code>192.168.1.1</code> (保持不變)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">⚠️ 無效格式處理</h4>
|
||||||
|
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||||
|
<li>• 無效IP地址 → <code>127.0.0.1</code> (默認值)</li>
|
||||||
|
<li>• 空值或null → <code>127.0.0.1</code> (默認值)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium mb-2">🎯 目標</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
確保所有顯示的IP地址都是標準的IPv4格式,提供一致且易於理解的用戶體驗。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -186,6 +186,10 @@ ALLOWED_IPS=你的真實IP地址,其他允許的IP`}
|
|||||||
<Globe className="w-4 h-4 mr-2" />
|
<Globe className="w-4 h-4 mr-2" />
|
||||||
詳細IP調試
|
詳細IP調試
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" onClick={() => window.open('/test/ip-format-test', '_blank')}>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
IPv4格式測試
|
||||||
|
</Button>
|
||||||
<Button variant="outline" onClick={() => window.open('/api/ip', '_blank')}>
|
<Button variant="outline" onClick={() => window.open('/api/ip', '_blank')}>
|
||||||
<ExternalLink className="w-4 h-4 mr-2" />
|
<ExternalLink className="w-4 h-4 mr-2" />
|
||||||
IP API 端點
|
IP API 端點
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Globe, Shield, ShieldAlert } from "lucide-react"
|
import { Globe, Shield, ShieldAlert, Info } from "lucide-react"
|
||||||
|
|
||||||
interface IpDisplayProps {
|
interface IpDisplayProps {
|
||||||
mobileSimplified?: boolean
|
mobileSimplified?: boolean
|
||||||
@@ -13,12 +13,63 @@ interface IpInfo {
|
|||||||
enableIpWhitelist: boolean
|
enableIpWhitelist: boolean
|
||||||
allowedIps: string[]
|
allowedIps: string[]
|
||||||
timestamp: string
|
timestamp: string
|
||||||
|
ipv6Info?: {
|
||||||
|
isIPv6Mapped: boolean
|
||||||
|
originalFormat: string
|
||||||
|
ipv6Format: string
|
||||||
|
hasIPv6Support: boolean
|
||||||
|
}
|
||||||
|
debug?: {
|
||||||
|
originalDetectedIp?: string
|
||||||
|
finalDetectedIp?: string
|
||||||
|
rawDetectedIp?: string
|
||||||
|
allFoundIps?: string[]
|
||||||
|
ipDetectionMethod?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理IP地址,確保顯示IPv4格式
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否為IPv6格式的IPv4地址
|
||||||
|
function isIPv6MappedIPv4(ip: string): boolean {
|
||||||
|
return ip.startsWith('::ffff:');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取IPv6格式的IPv4地址
|
||||||
|
function getIPv6MappedFormat(ipv4: string): string {
|
||||||
|
return `::ffff:${ipv4}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps) {
|
export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps) {
|
||||||
const [ipInfo, setIpInfo] = useState<IpInfo | null>(null)
|
const [ipInfo, setIpInfo] = useState<IpInfo | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [showIPv6Format, setShowIPv6Format] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchIpInfo = async () => {
|
const fetchIpInfo = async () => {
|
||||||
@@ -58,6 +109,14 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理IP地址確保顯示IPv4格式
|
||||||
|
const displayIp = cleanIpForDisplay(ipInfo.ip);
|
||||||
|
|
||||||
|
// 使用API返回的IPv6信息,如果沒有則回退到本地檢測
|
||||||
|
const isIPv6Mapped = ipInfo.ipv6Info?.isIPv6Mapped || isIPv6MappedIPv4(ipInfo.debug?.originalDetectedIp || ipInfo.ip);
|
||||||
|
const ipv6Format = ipInfo.ipv6Info?.ipv6Format || getIPv6MappedFormat(displayIp);
|
||||||
|
const originalFormat = ipInfo.ipv6Info?.originalFormat || ipInfo.debug?.originalDetectedIp || ipInfo.ip;
|
||||||
|
|
||||||
// 手機版簡化顯示
|
// 手機版簡化顯示
|
||||||
if (mobileSimplified) {
|
if (mobileSimplified) {
|
||||||
return (
|
return (
|
||||||
@@ -72,13 +131,14 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
|
|||||||
<Shield className="w-2.5 h-2.5 text-green-300" />
|
<Shield className="w-2.5 h-2.5 text-green-300" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs font-mono text-blue-200">
|
<span className="text-xs font-mono text-blue-200">
|
||||||
{ipInfo.ip.split('.').slice(0, 2).join('.')}...
|
{isIPv6Mapped ? 'IPv6' : 'IPv4'}: {displayIp.split('.').slice(0, 2).join('.')}...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="relative group">
|
||||||
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-md border backdrop-blur-sm ${
|
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-md border backdrop-blur-sm ${
|
||||||
ipInfo.enableIpWhitelist && !ipInfo.isAllowed
|
ipInfo.enableIpWhitelist && !ipInfo.isAllowed
|
||||||
? 'bg-red-900/50 border-red-800/30'
|
? 'bg-red-900/50 border-red-800/30'
|
||||||
@@ -89,7 +149,18 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
|
|||||||
) : (
|
) : (
|
||||||
<Shield className="w-3 h-3 text-green-300" />
|
<Shield className="w-3 h-3 text-green-300" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-blue-200 font-mono">{ipInfo.ip}</span>
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xs text-blue-200 font-mono">
|
||||||
|
{isIPv6Mapped ? 'IPv6' : 'IPv4'}: {displayIp}
|
||||||
|
</span>
|
||||||
|
{isIPv6Mapped && (
|
||||||
|
<span className="text-xs text-cyan-300 font-mono">
|
||||||
|
{ipv6Format}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{ipInfo.enableIpWhitelist && (
|
{ipInfo.enableIpWhitelist && (
|
||||||
<span className={`text-xs px-1 py-0.5 rounded ${
|
<span className={`text-xs px-1 py-0.5 rounded ${
|
||||||
ipInfo.isAllowed
|
ipInfo.isAllowed
|
||||||
@@ -99,6 +170,61 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
|
|||||||
{ipInfo.isAllowed ? '允許' : '拒絕'}
|
{ipInfo.isAllowed ? '允許' : '拒絕'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* IPv6格式切換按鈕 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowIPv6Format(!showIPv6Format)}
|
||||||
|
className="ml-1 p-0.5 rounded hover:bg-blue-800/30 transition-colors"
|
||||||
|
title="切換IPv6格式顯示"
|
||||||
|
>
|
||||||
|
<Info className="w-3 h-3 text-blue-300" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 詳細信息彈出框 */}
|
||||||
|
{showIPv6Format && (
|
||||||
|
<div className="absolute top-full left-0 mt-1 p-2 bg-slate-800/95 border border-blue-800/50 rounded-md shadow-lg backdrop-blur-sm z-50 min-w-64">
|
||||||
|
<div className="text-xs space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-blue-200">IPv4格式:</span>
|
||||||
|
<span className="text-white font-mono">{displayIp}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-blue-200">IPv6格式:</span>
|
||||||
|
<span className="text-cyan-300 font-mono">{ipv6Format}</span>
|
||||||
|
</div>
|
||||||
|
{originalFormat && originalFormat !== displayIp && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-blue-200">原始格式:</span>
|
||||||
|
<span className="text-yellow-300 font-mono">{originalFormat}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ipInfo.debug?.ipDetectionMethod && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-blue-200">檢測方法:</span>
|
||||||
|
<span className="text-green-300 font-mono">{ipInfo.debug.ipDetectionMethod}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ipInfo.debug?.allFoundIps && ipInfo.debug.allFoundIps.length > 0 && (
|
||||||
|
<div className="mt-2 pt-1 border-t border-blue-800/30">
|
||||||
|
<span className="text-blue-200">所有檢測到的IP:</span>
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
{ipInfo.debug.allFoundIps.map((ip, index) => (
|
||||||
|
<div key={index} className="text-yellow-300 font-mono text-xs">
|
||||||
|
{ip}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ipInfo.ipv6Info?.hasIPv6Support && (
|
||||||
|
<div className="mt-2 pt-1 border-t border-blue-800/30">
|
||||||
|
<span className="text-green-300">✓ IPv6支援已啟用</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
@@ -66,6 +66,11 @@ function cleanIpAddress(ip: string): string | null {
|
|||||||
ip = ip.substring(7);
|
ip = ip.substring(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 處理純IPv6本地回環地址
|
||||||
|
if (ip === '::1') {
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
// 驗證IP格式
|
// 驗證IP格式
|
||||||
if (!isValidIp(ip)) {
|
if (!isValidIp(ip)) {
|
||||||
return null;
|
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
|
// 選擇最佳IP
|
||||||
for (const ip of allFoundIps) {
|
for (const ip of allFoundIps) {
|
||||||
if (isPublicIp(ip)) {
|
if (isPublicIp(ip)) {
|
||||||
|
@@ -3,158 +3,43 @@
|
|||||||
* 用於測試和驗證IP白名單功能
|
* 用於測試和驗證IP白名單功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { getClientIp, isIpAllowed, isValidIp, isValidCidr } = require('../lib/ip-utils.ts');
|
const { getClientIp, getDetailedIpInfo, cleanIpAddress } = require('../lib/ip-utils.ts');
|
||||||
|
|
||||||
// 模擬請求對象
|
// 模擬請求對象
|
||||||
function createMockRequest(headers = {}) {
|
const mockRequest = {
|
||||||
return {
|
headers: {
|
||||||
headers,
|
'x-forwarded-for': '::ffff:127.0.0.1, 192.168.1.100',
|
||||||
connection: { remoteAddress: '192.168.1.100' },
|
'x-real-ip': '::ffff:127.0.0.1',
|
||||||
socket: { remoteAddress: '192.168.1.100' },
|
'x-client-ip': '::1',
|
||||||
ip: '192.168.1.100'
|
'connection': {
|
||||||
|
'remoteAddress': '::ffff:127.0.0.1'
|
||||||
|
},
|
||||||
|
'socket': {
|
||||||
|
'remoteAddress': '::1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ip: '::ffff:127.0.0.1'
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 測試IP檢測功能
|
console.log('=== IP 檢測測試 ===');
|
||||||
function testIpDetection() {
|
|
||||||
console.log('🧪 開始測試IP檢測功能...\n');
|
|
||||||
|
|
||||||
// 測試1: 基本IP檢測
|
// 測試 cleanIpAddress 函數
|
||||||
console.log('📋 測試1: 基本IP檢測');
|
console.log('\n1. 測試 cleanIpAddress 函數:');
|
||||||
const basicRequest = createMockRequest({
|
console.log('::ffff:127.0.0.1 ->', cleanIpAddress('::ffff:127.0.0.1'));
|
||||||
'x-forwarded-for': '203.0.113.1, 192.168.1.100'
|
console.log('::1 ->', cleanIpAddress('::1'));
|
||||||
});
|
console.log('127.0.0.1 ->', cleanIpAddress('127.0.0.1'));
|
||||||
const detectedIp = getClientIp(basicRequest);
|
console.log('192.168.1.1 ->', cleanIpAddress('192.168.1.1'));
|
||||||
console.log(`檢測到的IP: ${detectedIp}`);
|
|
||||||
console.log(`預期結果: 203.0.113.1 (公網IP)`);
|
|
||||||
console.log(`實際結果: ${detectedIp === '203.0.113.1' ? '✅ 通過' : '❌ 失敗'}\n`);
|
|
||||||
|
|
||||||
// 測試2: Cloudflare代理
|
// 測試詳細IP信息
|
||||||
console.log('📋 測試2: Cloudflare代理');
|
console.log('\n2. 測試詳細IP信息:');
|
||||||
const cloudflareRequest = createMockRequest({
|
const detailedInfo = getDetailedIpInfo(mockRequest);
|
||||||
'cf-connecting-ip': '203.0.113.2'
|
console.log('檢測到的IP:', detailedInfo.detectedIp);
|
||||||
});
|
console.log('所有找到的IP:', detailedInfo.allFoundIps);
|
||||||
const cfIp = getClientIp(cloudflareRequest);
|
console.log('IP來源:', detailedInfo.ipSources);
|
||||||
console.log(`檢測到的IP: ${cfIp}`);
|
|
||||||
console.log(`預期結果: 203.0.113.2`);
|
|
||||||
console.log(`實際結果: ${cfIp === '203.0.113.2' ? '✅ 通過' : '❌ 失敗'}\n`);
|
|
||||||
|
|
||||||
// 測試3: 本地開發環境
|
// 測試客戶端IP獲取
|
||||||
console.log('📋 測試3: 本地開發環境');
|
console.log('\n3. 測試客戶端IP獲取:');
|
||||||
const localRequest = createMockRequest({
|
const clientIp = getClientIp(mockRequest);
|
||||||
'x-forwarded-for': '127.0.0.1'
|
console.log('最終檢測到的IP:', clientIp);
|
||||||
});
|
|
||||||
const localIp = getClientIp(localRequest);
|
|
||||||
console.log(`檢測到的IP: ${localIp}`);
|
|
||||||
console.log(`預期結果: 127.0.0.1 (本地回環)`);
|
|
||||||
console.log(`實際結果: ${localIp === '127.0.0.1' ? '✅ 通過' : '❌ 失敗'}\n`);
|
|
||||||
|
|
||||||
// 測試4: 多個代理IP
|
console.log('\n=== 測試完成 ===');
|
||||||
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
|
|
||||||
};
|
|
Reference in New Issue
Block a user