正確顯示 ip 結果 ( cloudflare )
This commit is contained in:
193
app/api/ip-diagnostic/route.ts
Normal file
193
app/api/ip-diagnostic/route.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// 收集所有可能的IP相关头部
|
||||||
|
const allHeaders = Object.fromEntries(request.headers.entries())
|
||||||
|
|
||||||
|
// 专门收集IP相关的头部
|
||||||
|
const ipHeaders = {
|
||||||
|
'x-forwarded-for': request.headers.get('x-forwarded-for'),
|
||||||
|
'x-real-ip': request.headers.get('x-real-ip'),
|
||||||
|
'x-client-ip': request.headers.get('x-client-ip'),
|
||||||
|
'cf-connecting-ip': request.headers.get('cf-connecting-ip'), // Cloudflare
|
||||||
|
'cf-ray': request.headers.get('cf-ray'), // Cloudflare Ray ID
|
||||||
|
'cf-visitor': request.headers.get('cf-visitor'), // Cloudflare visitor info
|
||||||
|
'cf-ipcountry': request.headers.get('cf-ipcountry'), // Cloudflare country
|
||||||
|
'x-forwarded': request.headers.get('x-forwarded'),
|
||||||
|
'forwarded-for': request.headers.get('forwarded-for'),
|
||||||
|
'forwarded': request.headers.get('forwarded'),
|
||||||
|
'x-original-forwarded-for': request.headers.get('x-original-forwarded-for'),
|
||||||
|
'x-cluster-client-ip': request.headers.get('x-cluster-client-ip'),
|
||||||
|
'x-1panel-client-ip': request.headers.get('x-1panel-client-ip'),
|
||||||
|
'x-nginx-proxy-real-ip': request.headers.get('x-nginx-proxy-real-ip'),
|
||||||
|
'x-original-remote-addr': request.headers.get('x-original-remote-addr'),
|
||||||
|
'x-remote-addr': request.headers.get('x-remote-addr'),
|
||||||
|
'x-client-real-ip': request.headers.get('x-client-real-ip'),
|
||||||
|
'true-client-ip': request.headers.get('true-client-ip'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取其他有用的头部信息
|
||||||
|
const otherHeaders = {
|
||||||
|
'user-agent': request.headers.get('user-agent'),
|
||||||
|
'host': request.headers.get('host'),
|
||||||
|
'referer': request.headers.get('referer'),
|
||||||
|
'origin': request.headers.get('origin'),
|
||||||
|
'x-forwarded-proto': request.headers.get('x-forwarded-proto'),
|
||||||
|
'x-forwarded-host': request.headers.get('x-forwarded-host'),
|
||||||
|
'x-forwarded-port': request.headers.get('x-forwarded-port'),
|
||||||
|
'via': request.headers.get('via'),
|
||||||
|
'connection': request.headers.get('connection'),
|
||||||
|
'upgrade-insecure-requests': request.headers.get('upgrade-insecure-requests'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从不同来源获取IP
|
||||||
|
const ipAnalysis = {
|
||||||
|
// 标准代理头部
|
||||||
|
xForwardedFor: request.headers.get('x-forwarded-for'),
|
||||||
|
xRealIp: request.headers.get('x-real-ip'),
|
||||||
|
xClientIp: request.headers.get('x-client-ip'),
|
||||||
|
|
||||||
|
// Cloudflare 特定头部
|
||||||
|
cfConnectingIp: request.headers.get('cf-connecting-ip'),
|
||||||
|
|
||||||
|
// 其他可能的头部
|
||||||
|
forwarded: request.headers.get('forwarded'),
|
||||||
|
xOriginalForwardedFor: request.headers.get('x-original-forwarded-for'),
|
||||||
|
|
||||||
|
// 分析结果
|
||||||
|
analysis: {
|
||||||
|
hasCloudflare: !!request.headers.get('cf-connecting-ip') || !!request.headers.get('cf-ray'),
|
||||||
|
hasXForwardedFor: !!request.headers.get('x-forwarded-for'),
|
||||||
|
hasXRealIp: !!request.headers.get('x-real-ip'),
|
||||||
|
hasForwarded: !!request.headers.get('forwarded'),
|
||||||
|
recommendedIpSource: '',
|
||||||
|
recommendedIp: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析并推荐最佳IP来源
|
||||||
|
if (ipAnalysis.cfConnectingIp) {
|
||||||
|
ipAnalysis.analysis.recommendedIpSource = 'cf-connecting-ip (Cloudflare)'
|
||||||
|
ipAnalysis.analysis.recommendedIp = ipAnalysis.cfConnectingIp
|
||||||
|
} else if (ipAnalysis.xForwardedFor) {
|
||||||
|
// 解析 x-forwarded-for,通常格式为 "client-ip, proxy1-ip, proxy2-ip"
|
||||||
|
const forwardedIps = ipAnalysis.xForwardedFor.split(',').map(ip => ip.trim())
|
||||||
|
const clientIp = forwardedIps[0] // 第一个IP通常是客户端IP
|
||||||
|
ipAnalysis.analysis.recommendedIpSource = 'x-forwarded-for (first IP)'
|
||||||
|
ipAnalysis.analysis.recommendedIp = clientIp
|
||||||
|
} else if (ipAnalysis.xRealIp) {
|
||||||
|
ipAnalysis.analysis.recommendedIpSource = 'x-real-ip'
|
||||||
|
ipAnalysis.analysis.recommendedIp = ipAnalysis.xRealIp
|
||||||
|
} else if (ipAnalysis.forwarded) {
|
||||||
|
ipAnalysis.analysis.recommendedIpSource = 'forwarded header'
|
||||||
|
ipAnalysis.analysis.recommendedIp = ipAnalysis.forwarded
|
||||||
|
} else {
|
||||||
|
ipAnalysis.analysis.recommendedIpSource = 'no reliable IP source found'
|
||||||
|
ipAnalysis.analysis.recommendedIp = 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是Cloudflare
|
||||||
|
const isCloudflare = {
|
||||||
|
hasCfConnectingIp: !!request.headers.get('cf-connecting-ip'),
|
||||||
|
hasCfRay: !!request.headers.get('cf-ray'),
|
||||||
|
hasCfVisitor: !!request.headers.get('cf-visitor'),
|
||||||
|
hasCfIpCountry: !!request.headers.get('cf-ipcountry'),
|
||||||
|
cfRay: request.headers.get('cf-ray'),
|
||||||
|
cfCountry: request.headers.get('cf-ipcountry'),
|
||||||
|
cfVisitor: request.headers.get('cf-visitor'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是1Panel或Nginx代理
|
||||||
|
const proxyInfo = {
|
||||||
|
hasNginxProxy: !!request.headers.get('x-nginx-proxy-real-ip'),
|
||||||
|
has1Panel: !!request.headers.get('x-1panel-client-ip'),
|
||||||
|
nginxProxyIp: request.headers.get('x-nginx-proxy-real-ip'),
|
||||||
|
panelClientIp: request.headers.get('x-1panel-client-ip'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 x-forwarded-for 中的所有IP
|
||||||
|
const parseXForwardedFor = (xff: string | null) => {
|
||||||
|
if (!xff) return []
|
||||||
|
return xff.split(',').map(ip => ip.trim()).filter(ip => ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析IP来源链
|
||||||
|
const ipChain = {
|
||||||
|
xForwardedForChain: parseXForwardedFor(ipAnalysis.xForwardedFor),
|
||||||
|
recommendedClientIp: ipAnalysis.analysis.recommendedIp,
|
||||||
|
isPublicIp: (ip: string) => {
|
||||||
|
// 简单的公网IP检查
|
||||||
|
return !ip.match(/^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|169\.254\.)/)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
environment: process.env.NODE_ENV,
|
||||||
|
url: request.url,
|
||||||
|
|
||||||
|
// 主要分析结果
|
||||||
|
ipAnalysis,
|
||||||
|
isCloudflare,
|
||||||
|
proxyInfo,
|
||||||
|
ipChain,
|
||||||
|
|
||||||
|
// 所有IP相关头部
|
||||||
|
ipHeaders,
|
||||||
|
|
||||||
|
// 其他有用头部
|
||||||
|
otherHeaders,
|
||||||
|
|
||||||
|
// 完整头部列表(用于调试)
|
||||||
|
allHeaders,
|
||||||
|
|
||||||
|
// 建议
|
||||||
|
recommendations: {
|
||||||
|
primaryIpSource: ipAnalysis.analysis.recommendedIpSource,
|
||||||
|
primaryIp: ipAnalysis.analysis.recommendedIp,
|
||||||
|
isCloudflareSetup: isCloudflare.hasCfConnectingIp,
|
||||||
|
isProxySetup: proxyInfo.hasNginxProxy || proxyInfo.has1Panel,
|
||||||
|
suggestedConfig: generateSuggestedConfig(ipAnalysis, isCloudflare, proxyInfo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('IP诊断错误:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'IP诊断失败', details: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSuggestedConfig(ipAnalysis: any, isCloudflare: any, proxyInfo: any): string[] {
|
||||||
|
const suggestions = []
|
||||||
|
|
||||||
|
if (isCloudflare.hasCfConnectingIp) {
|
||||||
|
suggestions.push('检测到Cloudflare代理,建议优先使用 cf-connecting-ip 头部')
|
||||||
|
suggestions.push('确保1Panel/反向代理正确转发 cf-connecting-ip 头部')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ipAnalysis.xForwardedFor) {
|
||||||
|
suggestions.push('检测到 x-forwarded-for 头部,建议解析第一个IP作为客户端IP')
|
||||||
|
const ips = ipAnalysis.xForwardedFor.split(',').map((ip: string) => ip.trim())
|
||||||
|
if (ips.length > 1) {
|
||||||
|
suggestions.push(`IP链: ${ips.join(' -> ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyInfo.hasNginxProxy) {
|
||||||
|
suggestions.push('检测到Nginx代理,建议配置 nginx.conf 正确转发客户端IP')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyInfo.has1Panel) {
|
||||||
|
suggestions.push('检测到1Panel,建议检查1Panel的反向代理配置')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCloudflare.hasCfConnectingIp && !ipAnalysis.xForwardedFor && !ipAnalysis.xRealIp) {
|
||||||
|
suggestions.push('警告: 未检测到可靠的IP来源头部,请检查代理配置')
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getClientIp, isIpAllowed, getIpLocation, getDetailedIpInfo } from '@/lib/ip-utils'
|
import { getClientIp, isIpAllowed, getIpLocation, getDetailedIpInfo, isValidIp, isValidIPv6 } from '@/lib/ip-utils'
|
||||||
|
|
||||||
// 強制動態渲染
|
// 強制動態渲染
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -10,6 +10,17 @@ export async function GET(request: NextRequest) {
|
|||||||
const detailedInfo = getDetailedIpInfo(request);
|
const detailedInfo = getDetailedIpInfo(request);
|
||||||
let clientIp = detailedInfo.detectedIp;
|
let clientIp = detailedInfo.detectedIp;
|
||||||
|
|
||||||
|
// 根據你的環境,優先使用 cf-connecting-ip (支持IPv4和IPv6)
|
||||||
|
const cfConnectingIp = request.headers.get('cf-connecting-ip');
|
||||||
|
if (cfConnectingIp && cfConnectingIp.trim() !== '') {
|
||||||
|
const cleanCfIp = cfConnectingIp.trim();
|
||||||
|
// 检查是否是有效的IPv4或IPv6地址
|
||||||
|
if (isValidIp(cleanCfIp) || isValidIPv6(cleanCfIp)) {
|
||||||
|
clientIp = cleanCfIp;
|
||||||
|
console.log('使用 cf-connecting-ip:', clientIp, isValidIPv6(clientIp) ? '(IPv6)' : '(IPv4)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 確保返回IPv4格式的地址
|
// 確保返回IPv4格式的地址
|
||||||
function ensureIPv4Format(ip: string): string {
|
function ensureIPv4Format(ip: string): string {
|
||||||
if (!ip) return '127.0.0.1';
|
if (!ip) return '127.0.0.1';
|
||||||
|
|||||||
224
app/test-ip-blocking/page.tsx
Normal file
224
app/test-ip-blocking/page.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { RefreshCw, Shield, AlertTriangle, CheckCircle, Lock } from "lucide-react"
|
||||||
|
|
||||||
|
interface IpInfo {
|
||||||
|
ip: string
|
||||||
|
isAllowed: boolean
|
||||||
|
enableIpWhitelist: boolean
|
||||||
|
allowedIps: string[]
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IpBlockingTestPage() {
|
||||||
|
const [ipInfo, setIpInfo] = useState<IpInfo | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const fetchIpInfo = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ip')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('無法獲取IP信息')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setIpInfo(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("無法獲取IP信息:", error)
|
||||||
|
setError("無法獲取IP信息")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchIpInfo()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
|
<span>載入中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !ipInfo) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<Card className="border-red-200 bg-red-50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-red-600">
|
||||||
|
<AlertTriangle className="w-5 h-5" />
|
||||||
|
錯誤
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-red-600">{error}</p>
|
||||||
|
<Button onClick={fetchIpInfo} className="mt-4">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
重試
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold">IP 阻擋測試</h1>
|
||||||
|
<Button onClick={fetchIpInfo} variant="outline">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* IP狀態卡片 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
{ipInfo.enableIpWhitelist && !ipInfo.isAllowed ? (
|
||||||
|
<>
|
||||||
|
<Lock className="w-5 h-5 text-red-500" />
|
||||||
|
IP 被阻擋
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||||
|
IP 允許訪問
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">檢測到的IP</label>
|
||||||
|
<p className="text-lg font-mono bg-gray-100 p-2 rounded">
|
||||||
|
{ipInfo.ip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">訪問狀態</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<Badge variant={ipInfo.isAllowed ? "default" : "destructive"}>
|
||||||
|
{ipInfo.isAllowed ? "允許" : "阻擋"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">白名單狀態</label>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Shield className="w-4 h-4 text-blue-500" />
|
||||||
|
<span className="text-sm">
|
||||||
|
{ipInfo.enableIpWhitelist ? "已啟用" : "未啟用"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 允許的IP列表 */}
|
||||||
|
{ipInfo.enableIpWhitelist && ipInfo.allowedIps.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>允許的IP列表</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||||
|
{ipInfo.allowedIps.map((ip, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<Badge variant={ip === ipInfo.ip ? "default" : "outline"}>
|
||||||
|
{ip === ipInfo.ip ? "當前" : ""}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm font-mono">{ip}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 阻擋測試說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>IP 阻擋測試說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<h3 className="font-medium text-blue-900 mb-2">✅ 如果你能看到這個頁面</h3>
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
說明你的IP ({ipInfo.ip}) 在白名單中,可以正常訪問網站。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<h3 className="font-medium text-yellow-900 mb-2">⚠️ 測試阻擋功能</h3>
|
||||||
|
<p className="text-sm text-yellow-800">
|
||||||
|
要測試IP阻擋功能,可以:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-yellow-800 mt-2 ml-4 list-disc">
|
||||||
|
<li>暫時從 .env.local 的 ALLOWED_IPS 中移除你的IP</li>
|
||||||
|
<li>重啟應用後訪問網站</li>
|
||||||
|
<li>應該會看到403禁止訪問頁面</li>
|
||||||
|
<li>記得測試完成後將IP加回白名單</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<h3 className="font-medium text-red-900 mb-2">🚫 被阻擋的訪問</h3>
|
||||||
|
<p className="text-sm text-red-800">
|
||||||
|
如果IP不在白名單中,訪問者會看到:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-red-800 mt-2 ml-4 list-disc">
|
||||||
|
<li>403 Forbidden 錯誤頁面</li>
|
||||||
|
<li>顯示被阻擋的IP地址</li>
|
||||||
|
<li>無法訪問任何網頁內容</li>
|
||||||
|
<li>只有IP檢測API可以訪問</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 當前配置 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>當前配置</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium text-gray-500">白名單狀態:</span>
|
||||||
|
<Badge variant={ipInfo.enableIpWhitelist ? "default" : "secondary"}>
|
||||||
|
{ipInfo.enableIpWhitelist ? "已啟用" : "未啟用"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium text-gray-500">當前IP:</span>
|
||||||
|
<span className="text-sm font-mono">{ipInfo.ip}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium text-gray-500">訪問權限:</span>
|
||||||
|
<Badge variant={ipInfo.isAllowed ? "default" : "destructive"}>
|
||||||
|
{ipInfo.isAllowed ? "允許" : "阻擋"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -100,10 +100,18 @@ export default function IpDebugPage() {
|
|||||||
<div className="container mx-auto p-6 space-y-6">
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-3xl font-bold">IP 檢測調試</h1>
|
<h1 className="text-3xl font-bold">IP 檢測調試</h1>
|
||||||
<Button onClick={fetchIpInfo} variant="outline">
|
<div className="flex gap-2">
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<Button onClick={fetchIpInfo} variant="outline">
|
||||||
刷新
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
</Button>
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.open('/api/ip-diagnostic', '_blank')}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
完整診斷
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主要IP信息 */}
|
{/* 主要IP信息 */}
|
||||||
|
|||||||
339
app/test/ip-diagnostic/page.tsx
Normal file
339
app/test/ip-diagnostic/page.tsx
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { RefreshCw, Shield, AlertTriangle, CheckCircle, Info, Cloud, Server } from "lucide-react"
|
||||||
|
|
||||||
|
interface DiagnosticData {
|
||||||
|
timestamp: string
|
||||||
|
environment: string
|
||||||
|
url: string
|
||||||
|
ipAnalysis: {
|
||||||
|
xForwardedFor: string | null
|
||||||
|
xRealIp: string | null
|
||||||
|
xClientIp: string | null
|
||||||
|
cfConnectingIp: string | null
|
||||||
|
forwarded: string | null
|
||||||
|
xOriginalForwardedFor: string | null
|
||||||
|
analysis: {
|
||||||
|
hasCloudflare: boolean
|
||||||
|
hasXForwardedFor: boolean
|
||||||
|
hasXRealIp: boolean
|
||||||
|
hasForwarded: boolean
|
||||||
|
recommendedIpSource: string
|
||||||
|
recommendedIp: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isCloudflare: {
|
||||||
|
hasCfConnectingIp: boolean
|
||||||
|
hasCfRay: boolean
|
||||||
|
hasCfVisitor: boolean
|
||||||
|
hasCfIpCountry: boolean
|
||||||
|
cfRay: string | null
|
||||||
|
cfCountry: string | null
|
||||||
|
cfVisitor: string | null
|
||||||
|
}
|
||||||
|
proxyInfo: {
|
||||||
|
hasNginxProxy: boolean
|
||||||
|
has1Panel: boolean
|
||||||
|
nginxProxyIp: string | null
|
||||||
|
panelClientIp: string | null
|
||||||
|
}
|
||||||
|
ipChain: {
|
||||||
|
xForwardedForChain: string[]
|
||||||
|
recommendedClientIp: string
|
||||||
|
}
|
||||||
|
ipHeaders: Record<string, string | null>
|
||||||
|
otherHeaders: Record<string, string | null>
|
||||||
|
allHeaders: Record<string, string>
|
||||||
|
recommendations: {
|
||||||
|
primaryIpSource: string
|
||||||
|
primaryIp: string
|
||||||
|
isCloudflareSetup: boolean
|
||||||
|
isProxySetup: boolean
|
||||||
|
suggestedConfig: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IpDiagnosticPage() {
|
||||||
|
const [diagnosticData, setDiagnosticData] = useState<DiagnosticData | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const fetchDiagnosticData = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ip-diagnostic')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('無法獲取診斷數據')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setDiagnosticData(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("無法獲取診斷數據:", error)
|
||||||
|
setError("無法獲取診斷數據")
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDiagnosticData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
|
<span>載入診斷數據中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !diagnosticData) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<Card className="border-red-200 bg-red-50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-red-600">
|
||||||
|
<AlertTriangle className="w-5 h-5" />
|
||||||
|
診斷失敗
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-red-600">{error}</p>
|
||||||
|
<Button onClick={fetchDiagnosticData} className="mt-4">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
重試
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold">IP 診斷工具</h1>
|
||||||
|
<Button onClick={fetchDiagnosticData} variant="outline">
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
刷新診斷
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 環境檢測結果 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Info className="w-5 h-5" />
|
||||||
|
環境檢測結果
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Cloud className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">Cloudflare:</span>
|
||||||
|
<Badge variant={diagnosticData.isCloudflare.hasCfConnectingIp ? "default" : "secondary"}>
|
||||||
|
{diagnosticData.isCloudflare.hasCfConnectingIp ? "檢測到" : "未檢測到"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">代理服務器:</span>
|
||||||
|
<Badge variant={diagnosticData.proxyInfo.hasNginxProxy || diagnosticData.proxyInfo.has1Panel ? "default" : "secondary"}>
|
||||||
|
{diagnosticData.proxyInfo.hasNginxProxy || diagnosticData.proxyInfo.has1Panel ? "檢測到" : "未檢測到"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shield className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">IP來源:</span>
|
||||||
|
<Badge variant="default">
|
||||||
|
{diagnosticData.recommendations.primaryIpSource}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 推薦的IP */}
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<h3 className="font-medium text-blue-900 mb-2">推薦使用的客戶端IP</h3>
|
||||||
|
<p className="text-lg font-mono text-blue-800">
|
||||||
|
{diagnosticData.recommendations.primaryIp}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-blue-600 mt-1">
|
||||||
|
來源: {diagnosticData.recommendations.primaryIpSource}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Cloudflare 信息 */}
|
||||||
|
{diagnosticData.isCloudflare.hasCfConnectingIp && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Cloud className="w-5 h-5 text-blue-500" />
|
||||||
|
Cloudflare 信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">CF-Connecting-IP</label>
|
||||||
|
<p className="text-sm bg-gray-100 p-2 rounded font-mono">
|
||||||
|
{diagnosticData.ipAnalysis.cfConnectingIp || 'null'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">CF-Ray</label>
|
||||||
|
<p className="text-sm bg-gray-100 p-2 rounded">
|
||||||
|
{diagnosticData.isCloudflare.cfRay || 'null'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">CF-Country</label>
|
||||||
|
<p className="text-sm bg-gray-100 p-2 rounded">
|
||||||
|
{diagnosticData.isCloudflare.cfCountry || 'null'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">CF-Visitor</label>
|
||||||
|
<p className="text-sm bg-gray-100 p-2 rounded">
|
||||||
|
{diagnosticData.isCloudflare.cfVisitor || 'null'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 代理信息 */}
|
||||||
|
{(diagnosticData.proxyInfo.hasNginxProxy || diagnosticData.proxyInfo.has1Panel) && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Server className="w-5 h-5 text-green-500" />
|
||||||
|
代理服務器信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{diagnosticData.proxyInfo.hasNginxProxy && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Nginx Proxy Real IP</label>
|
||||||
|
<p className="text-sm bg-gray-100 p-2 rounded font-mono">
|
||||||
|
{diagnosticData.proxyInfo.nginxProxyIp || 'null'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{diagnosticData.proxyInfo.has1Panel && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">1Panel Client IP</label>
|
||||||
|
<p className="text-sm bg-gray-100 p-2 rounded font-mono">
|
||||||
|
{diagnosticData.proxyInfo.panelClientIp || 'null'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* IP鏈分析 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>IP 鏈分析</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">X-Forwarded-For 鏈</label>
|
||||||
|
{diagnosticData.ipChain.xForwardedForChain.length > 0 ? (
|
||||||
|
<div className="mt-1 space-y-1">
|
||||||
|
{diagnosticData.ipChain.xForwardedForChain.map((ip, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2 text-sm bg-gray-100 p-2 rounded">
|
||||||
|
<Badge variant={index === 0 ? "default" : "outline"}>
|
||||||
|
{index === 0 ? "客戶端" : `代理${index}`}
|
||||||
|
</Badge>
|
||||||
|
<span className="font-mono">{ip}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 mt-1">無 X-Forwarded-For 頭部</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 所有IP相關頭部 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>所有 IP 相關頭部</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-80 overflow-y-auto">
|
||||||
|
{Object.entries(diagnosticData.ipHeaders).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-start gap-2 text-sm bg-gray-100 p-2 rounded">
|
||||||
|
<span className="font-medium text-blue-600 min-w-32">{key}:</span>
|
||||||
|
<span className="font-mono text-gray-800 flex-1">{value || 'null'}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 配置建議 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>配置建議</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{diagnosticData.recommendations.suggestedConfig.map((suggestion, index) => (
|
||||||
|
<div key={index} className="flex items-start gap-2 p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||||||
|
<Info className="w-4 h-4 text-yellow-600 mt-0.5 flex-shrink-0" />
|
||||||
|
<p className="text-sm text-yellow-800">{suggestion}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 完整頭部列表 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>完整 HTTP 頭部列表</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{Object.entries(diagnosticData.allHeaders).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-start gap-2 text-sm bg-gray-100 p-2 rounded">
|
||||||
|
<span className="font-medium text-blue-600 min-w-40">{key}:</span>
|
||||||
|
<span className="text-gray-800 flex-1 break-all">{value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 使用說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>使用說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-gray-600">
|
||||||
|
<p>1. 查看上面的診斷結果,確認你的環境類型(Cloudflare、代理、直接連接)</p>
|
||||||
|
<p>2. 檢查「推薦使用的客戶端IP」是否為你的真實IP (114.33.18.13)</p>
|
||||||
|
<p>3. 如果不是,請將診斷結果截圖發給我,我會根據實際的頭部信息調整IP檢測邏輯</p>
|
||||||
|
<p>4. 特別注意 X-Forwarded-For 鏈和 Cloudflare 相關頭部</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ interface IpInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理IP地址,確保顯示IPv4格式
|
// 清理IP地址,支持IPv4和IPv6格式
|
||||||
function cleanIpForDisplay(ip: string): string {
|
function cleanIpForDisplay(ip: string): string {
|
||||||
if (!ip) return '127.0.0.1';
|
if (!ip) return '127.0.0.1';
|
||||||
|
|
||||||
@@ -51,7 +51,13 @@ function cleanIpForDisplay(ip: string): string {
|
|||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是有效的IPv4,返回默認值
|
// 驗證是否為有效的IPv6地址
|
||||||
|
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)+::$|^::$/;
|
||||||
|
if (ipv6Regex.test(ip)) {
|
||||||
|
return ip; // 直接返回IPv6地址
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是有效的IP,返回默認值
|
||||||
return '127.0.0.1';
|
return '127.0.0.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +80,18 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchIpInfo = async () => {
|
const fetchIpInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/ip')
|
// 添加时间戳防止缓存,确保获取最新的IP检测结果
|
||||||
|
const response = await fetch(`/api/ip?t=${Date.now()}`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
}
|
||||||
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('無法獲取IP信息')
|
throw new Error('無法獲取IP信息')
|
||||||
}
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
console.log('IP显示组件获取到数据:', data)
|
||||||
setIpInfo(data)
|
setIpInfo(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("無法獲取IP信息:", error)
|
console.error("無法獲取IP信息:", error)
|
||||||
@@ -109,9 +122,12 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理IP地址確保顯示IPv4格式
|
// 清理IP地址,支持IPv4和IPv6格式
|
||||||
const displayIp = cleanIpForDisplay(ipInfo.ip);
|
const displayIp = cleanIpForDisplay(ipInfo.ip);
|
||||||
|
|
||||||
|
// 检测是否是真正的IPv6地址
|
||||||
|
const isRealIPv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)+::$|^::$/.test(displayIp);
|
||||||
|
|
||||||
// 使用API返回的IPv6信息,如果沒有則回退到本地檢測
|
// 使用API返回的IPv6信息,如果沒有則回退到本地檢測
|
||||||
const isIPv6Mapped = ipInfo.ipv6Info?.isIPv6Mapped || isIPv6MappedIPv4(ipInfo.debug?.originalDetectedIp || ipInfo.ip);
|
const isIPv6Mapped = ipInfo.ipv6Info?.isIPv6Mapped || isIPv6MappedIPv4(ipInfo.debug?.originalDetectedIp || ipInfo.ip);
|
||||||
const ipv6Format = ipInfo.ipv6Info?.ipv6Format || getIPv6MappedFormat(displayIp);
|
const ipv6Format = ipInfo.ipv6Info?.ipv6Format || getIPv6MappedFormat(displayIp);
|
||||||
@@ -131,7 +147,7 @@ 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">
|
||||||
{isIPv6Mapped ? 'IPv6' : 'IPv4'}: {displayIp.split('.').slice(0, 2).join('.')}...
|
{isRealIPv6 ? 'IPv6' : 'IPv4'}: {isRealIPv6 ? displayIp.substring(0, 10) + '...' : displayIp.split('.').slice(0, 2).join('.') + '...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -152,9 +168,9 @@ export default function IpDisplay({ mobileSimplified = false }: IpDisplayProps)
|
|||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs text-blue-200 font-mono">
|
<span className="text-xs text-blue-200 font-mono">
|
||||||
{isIPv6Mapped ? 'IPv6' : 'IPv4'}: {displayIp}
|
{isRealIPv6 ? 'IPv6' : 'IPv4'}: {displayIp}
|
||||||
</span>
|
</span>
|
||||||
{isIPv6Mapped && (
|
{isIPv6Mapped && !isRealIPv6 && (
|
||||||
<span className="text-xs text-cyan-300 font-mono">
|
<span className="text-xs text-cyan-300 font-mono">
|
||||||
{ipv6Format}
|
{ipv6Format}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
151
lib/ip-utils.ts
151
lib/ip-utils.ts
@@ -43,7 +43,12 @@ export function isIpAllowed(clientIp: string, allowedIps: string): boolean {
|
|||||||
|
|
||||||
for (const allowedIp of allowedIpList) {
|
for (const allowedIp of allowedIpList) {
|
||||||
try {
|
try {
|
||||||
if (isIpInRange(clientIp, allowedIp)) {
|
// 如果是IPv6地址,直接比较字符串
|
||||||
|
if (isValidIPv6(clientIp)) {
|
||||||
|
if (clientIp === allowedIp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (isIpInRange(clientIp, allowedIp)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -71,40 +76,112 @@ function cleanIpAddress(ip: string): string | null {
|
|||||||
return '127.0.0.1';
|
return '127.0.0.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 驗證IP格式
|
// 驗證IP格式 - 支持IPv4和IPv6
|
||||||
if (!isValidIp(ip)) {
|
if (!isValidIp(ip) && !isValidIPv6(ip)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 檢測IP策略 - 根據請求頭判斷環境類型
|
||||||
|
function detectIpStrategy(req: any): {
|
||||||
|
isCloudflare: boolean;
|
||||||
|
isProxy: boolean;
|
||||||
|
isDirect: boolean;
|
||||||
|
strategy: string;
|
||||||
|
} {
|
||||||
|
const hasCfConnectingIp = !!req.headers['cf-connecting-ip'];
|
||||||
|
const hasCfRay = !!req.headers['cf-ray'];
|
||||||
|
const hasCfVisitor = !!req.headers['cf-visitor'];
|
||||||
|
const hasXForwardedFor = !!req.headers['x-forwarded-for'];
|
||||||
|
const hasXRealIp = !!req.headers['x-real-ip'];
|
||||||
|
const hasNginxProxy = !!req.headers['x-nginx-proxy-real-ip'];
|
||||||
|
const has1Panel = !!req.headers['x-1panel-client-ip'];
|
||||||
|
|
||||||
|
const isCloudflare = hasCfConnectingIp || hasCfRay || hasCfVisitor;
|
||||||
|
const isProxy = hasXForwardedFor || hasXRealIp || hasNginxProxy || has1Panel;
|
||||||
|
const isDirect = !isCloudflare && !isProxy;
|
||||||
|
|
||||||
|
let strategy = 'unknown';
|
||||||
|
if (isCloudflare) {
|
||||||
|
strategy = 'cloudflare';
|
||||||
|
} else if (isProxy) {
|
||||||
|
strategy = 'proxy';
|
||||||
|
} else {
|
||||||
|
strategy = 'direct';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCloudflare,
|
||||||
|
isProxy,
|
||||||
|
isDirect,
|
||||||
|
strategy
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 獲取客戶端真實IP
|
// 獲取客戶端真實IP
|
||||||
export function getClientIp(req: any): string {
|
export function getClientIp(req: any): string {
|
||||||
// 按優先順序檢查各種IP來源
|
// 智能IP檢測 - 根據實際環境動態調整優先級
|
||||||
const ipSources = [
|
const ipDetectionStrategy = detectIpStrategy(req);
|
||||||
// 1Panel 和常見代理伺服器轉發的IP (優先級最高)
|
|
||||||
req.headers['x-forwarded-for'],
|
let ipSources: (string | undefined)[];
|
||||||
req.headers['x-real-ip'],
|
|
||||||
req.headers['x-client-ip'],
|
if (ipDetectionStrategy.isCloudflare) {
|
||||||
req.headers['cf-connecting-ip'], // Cloudflare
|
// Cloudflare 環境的優先級 - 根據你的實際環境優化
|
||||||
req.headers['x-original-forwarded-for'], // 某些代理伺服器
|
ipSources = [
|
||||||
req.headers['x-cluster-client-ip'], // 集群環境
|
req.headers['cf-connecting-ip'], // 你的真實IP: 114.33.18.13
|
||||||
|
req.headers['x-forwarded-for'], // 備用來源 (但第一個IP是代理IP,所以不優先)
|
||||||
// 其他代理頭
|
req.headers['x-real-ip'], // 這是代理IP: 172.70.214.81,不應該使用
|
||||||
req.headers['x-forwarded'], // 舊版代理頭
|
req.headers['x-client-ip'],
|
||||||
req.headers['forwarded-for'],
|
req.headers['x-original-forwarded-for'],
|
||||||
req.headers['forwarded'],
|
req.headers['x-cluster-client-ip'],
|
||||||
|
req.headers['x-forwarded'],
|
||||||
// 1Panel 特殊頭部
|
req.headers['forwarded-for'],
|
||||||
req.headers['x-1panel-client-ip'], // 1Panel 可能使用的頭部
|
req.headers['forwarded'],
|
||||||
req.headers['x-nginx-proxy-real-ip'], // Nginx 代理
|
req.headers['x-1panel-client-ip'],
|
||||||
|
req.headers['x-nginx-proxy-real-ip'],
|
||||||
// 直接連接的IP
|
req.connection?.remoteAddress,
|
||||||
req.connection?.remoteAddress,
|
req.socket?.remoteAddress,
|
||||||
req.socket?.remoteAddress,
|
req.ip,
|
||||||
req.ip,
|
];
|
||||||
];
|
} else if (ipDetectionStrategy.isProxy) {
|
||||||
|
// 代理環境的優先級
|
||||||
|
ipSources = [
|
||||||
|
req.headers['x-forwarded-for'],
|
||||||
|
req.headers['x-real-ip'],
|
||||||
|
req.headers['x-client-ip'],
|
||||||
|
req.headers['x-original-forwarded-for'],
|
||||||
|
req.headers['x-cluster-client-ip'],
|
||||||
|
req.headers['cf-connecting-ip'], // 即使不是Cloudflare也可能有這個頭
|
||||||
|
req.headers['x-forwarded'],
|
||||||
|
req.headers['forwarded-for'],
|
||||||
|
req.headers['forwarded'],
|
||||||
|
req.headers['x-1panel-client-ip'],
|
||||||
|
req.headers['x-nginx-proxy-real-ip'],
|
||||||
|
req.connection?.remoteAddress,
|
||||||
|
req.socket?.remoteAddress,
|
||||||
|
req.ip,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// 直接連接的優先級
|
||||||
|
ipSources = [
|
||||||
|
req.headers['x-forwarded-for'],
|
||||||
|
req.headers['x-real-ip'],
|
||||||
|
req.headers['x-client-ip'],
|
||||||
|
req.headers['cf-connecting-ip'],
|
||||||
|
req.headers['x-original-forwarded-for'],
|
||||||
|
req.headers['x-cluster-client-ip'],
|
||||||
|
req.headers['x-forwarded'],
|
||||||
|
req.headers['forwarded-for'],
|
||||||
|
req.headers['forwarded'],
|
||||||
|
req.headers['x-1panel-client-ip'],
|
||||||
|
req.headers['x-nginx-proxy-real-ip'],
|
||||||
|
req.connection?.remoteAddress,
|
||||||
|
req.socket?.remoteAddress,
|
||||||
|
req.ip,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// 收集所有找到的IP用於調試
|
// 收集所有找到的IP用於調試
|
||||||
const foundIps: string[] = [];
|
const foundIps: string[] = [];
|
||||||
@@ -119,8 +196,15 @@ export function getClientIp(req: any): string {
|
|||||||
const cleanIp = cleanIpAddress(ip);
|
const cleanIp = cleanIpAddress(ip);
|
||||||
if (cleanIp) {
|
if (cleanIp) {
|
||||||
foundIps.push(cleanIp);
|
foundIps.push(cleanIp);
|
||||||
// 檢查是否為公網IP,排除內部IP和1Panel代理IP
|
|
||||||
if (isPublicIp(cleanIp) && !isInternalProxyIp(cleanIp)) {
|
// 特殊處理:如果是 cf-connecting-ip 頭部,直接返回(這是最可靠的)
|
||||||
|
if (ipSource === req.headers['cf-connecting-ip']) {
|
||||||
|
console.log('使用 cf-connecting-ip:', cleanIp, isValidIPv6(cleanIp) ? '(IPv6)' : '(IPv4)');
|
||||||
|
return cleanIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否為公網IP,排除內部IP和代理IP
|
||||||
|
if ((isPublicIp(cleanIp) || isValidIPv6(cleanIp)) && !isInternalProxyIp(cleanIp)) {
|
||||||
return cleanIp;
|
return cleanIp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,12 +476,19 @@ function isInternalProxyIp(ip: string): boolean {
|
|||||||
return proxyRanges.some(range => range.test(ip));
|
return proxyRanges.some(range => range.test(ip));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 驗證IP地址格式
|
// 驗證IPv4地址格式
|
||||||
export function isValidIp(ip: string): boolean {
|
export function isValidIp(ip: string): boolean {
|
||||||
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
return ipRegex.test(ip);
|
return ipRegex.test(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 驗證IPv6地址格式
|
||||||
|
export function isValidIPv6(ip: string): boolean {
|
||||||
|
// 簡化的IPv6驗證正則表達式
|
||||||
|
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)+::$|^::$/;
|
||||||
|
return ipv6Regex.test(ip);
|
||||||
|
}
|
||||||
|
|
||||||
// 驗證CIDR格式
|
// 驗證CIDR格式
|
||||||
export function isValidCidr(cidr: string): boolean {
|
export function isValidCidr(cidr: string): boolean {
|
||||||
const cidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
const cidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
||||||
|
|||||||
@@ -10,16 +10,22 @@ export function middleware(request: NextRequest) {
|
|||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取客戶端IP
|
// 獲取客戶端IP - 使用與API相同的邏輯
|
||||||
const clientIp = getClientIp(request)
|
const clientIp = getClientIp(request)
|
||||||
|
|
||||||
// 獲取允許的IP列表
|
// 獲取允許的IP列表
|
||||||
const allowedIps = process.env.ALLOWED_IPS || ''
|
const allowedIps = process.env.ALLOWED_IPS || ''
|
||||||
|
|
||||||
|
// 調試信息
|
||||||
|
console.log(`[Middleware] IP檢測: ${clientIp}, 路徑: ${request.nextUrl.pathname}`)
|
||||||
|
console.log(`[Middleware] 白名單狀態: ${enableIpWhitelist}`)
|
||||||
|
console.log(`[Middleware] 允許的IP: ${allowedIps}`)
|
||||||
|
|
||||||
// 檢查IP是否被允許
|
// 檢查IP是否被允許
|
||||||
if (!isIpAllowed(clientIp, allowedIps)) {
|
if (!isIpAllowed(clientIp, allowedIps)) {
|
||||||
// 記錄被拒絕的訪問
|
// 記錄被拒絕的訪問
|
||||||
console.warn(`Access denied for IP: ${clientIp} - Path: ${request.nextUrl.pathname}`)
|
console.warn(`[Middleware] Access denied for IP: ${clientIp} - Path: ${request.nextUrl.pathname}`)
|
||||||
|
console.warn(`[Middleware] 允許的IP列表: ${allowedIps}`)
|
||||||
|
|
||||||
// 返回403禁止訪問頁面
|
// 返回403禁止訪問頁面
|
||||||
return new NextResponse(
|
return new NextResponse(
|
||||||
@@ -112,12 +118,13 @@ export const config = {
|
|||||||
matcher: [
|
matcher: [
|
||||||
/*
|
/*
|
||||||
* 匹配所有路徑,除了:
|
* 匹配所有路徑,除了:
|
||||||
* - api (API routes)
|
* - api/ip (IP檢測API,允許訪問)
|
||||||
|
* - api/ip-diagnostic (IP診斷API,允許訪問)
|
||||||
* - _next/static (static files)
|
* - _next/static (static files)
|
||||||
* - _next/image (image optimization files)
|
* - _next/image (image optimization files)
|
||||||
* - favicon.ico (favicon file)
|
* - favicon.ico (favicon file)
|
||||||
* - icon.png (icon file)
|
* - icon.png (icon file)
|
||||||
*/
|
*/
|
||||||
'/((?!api|_next/static|_next/image|favicon.ico|icon.png).*)',
|
'/((?!api/ip|api/ip-diagnostic|_next/static|_next/image|favicon.ico|icon.png).*)',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user