272 lines
9.8 KiB
TypeScript
272 lines
9.8 KiB
TypeScript
/**
|
||
* 网络检测工具
|
||
* 用于在后端 API 中检测请求是否来自内网
|
||
*
|
||
* 检测逻辑(严谨方案):
|
||
* 1. 优先检查 hostname:如果访问的是公网域名,优先判断为公网用户
|
||
* 2. 正确解析 X-Forwarded-For:从右到左遍历,跳过内网IP(可信代理),找到真正的客户端IP
|
||
* 3. 双重验证:只有当 hostname 是内网IP 且 客户端IP也是内网IP时,才判断为内网用户
|
||
*/
|
||
|
||
import { NextRequest } from 'next/server'
|
||
|
||
// 公网域名白名单(明确标识为公网域名)
|
||
const PUBLIC_DOMAINS = [
|
||
'3d.yydd.ai',
|
||
's3.yydd.ai',
|
||
's3.1080p.hk',
|
||
]
|
||
|
||
// 公网域名模式(用于更严格的判断)
|
||
const PUBLIC_DOMAIN_PATTERNS = [
|
||
/\.yydd\.ai$/i, // 所有 .yydd.ai 子域名
|
||
/\.1080p\.hk$/i, // 所有 .1080p.hk 子域名
|
||
/^[a-z0-9-]+\.(com|net|org|cn|io|ai|hk|cc|top|xyz|site|online|tech|app|dev)$/i, // 常见公网顶级域名
|
||
]
|
||
|
||
// 判断是否为内网IP地址
|
||
export function isInternalIP(ip: string): boolean {
|
||
if (!ip || ip === 'unknown' || ip === '::1') {
|
||
return false
|
||
}
|
||
|
||
if (ip === 'localhost' || ip === '127.0.0.1') {
|
||
return true
|
||
}
|
||
|
||
// 判断是否为内网IP段
|
||
// 10.0.0.0 - 10.255.255.255
|
||
// 172.16.0.0 - 172.31.255.255
|
||
// 192.168.0.0 - 192.168.255.255
|
||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
|
||
const match = ip.match(ipv4Regex)
|
||
|
||
if (match) {
|
||
const [, a, b] = match.map(Number)
|
||
|
||
// 10.x.x.x
|
||
if (a === 10) return true
|
||
|
||
// 172.16.x.x - 172.31.x.x
|
||
if (a === 172 && b >= 16 && b <= 31) return true
|
||
|
||
// 192.168.x.x
|
||
if (a === 192 && b === 168) return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 判断是否为公网域名(增强版,更严格的判断)
|
||
* @param hostname 主机名
|
||
* @returns 是否为公网域名
|
||
*/
|
||
function isPublicDomain(hostname: string): boolean {
|
||
if (!hostname) return false
|
||
|
||
// 1. 检查是否在白名单中(最高优先级)
|
||
if (PUBLIC_DOMAINS.some(domain => hostname === domain || hostname.endsWith(`.${domain}`))) {
|
||
return true
|
||
}
|
||
|
||
// 2. 检查是否为IP地址格式(IP地址不是域名)
|
||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
|
||
if (ipv4Regex.test(hostname)) {
|
||
return false
|
||
}
|
||
|
||
// 3. 检查是否为localhost或内网IP(明确的内网标识)
|
||
if (hostname === 'localhost' || hostname === '127.0.0.1' || isInternalIP(hostname)) {
|
||
return false
|
||
}
|
||
|
||
// 4. 检查是否匹配公网域名模式(更严格的判断)
|
||
// 如果hostname包含点号(说明是域名),且匹配公网域名模式,判断为公网域名
|
||
if (hostname.includes('.')) {
|
||
// 检查是否匹配公网域名模式
|
||
const matchesPublicPattern = PUBLIC_DOMAIN_PATTERNS.some(pattern => pattern.test(hostname))
|
||
if (matchesPublicPattern) {
|
||
return true
|
||
}
|
||
|
||
// 如果hostname包含多个点号(如 subdomain.example.com),且不是内网IP,更可能是公网域名
|
||
const parts = hostname.split('.')
|
||
if (parts.length >= 2) {
|
||
// 有顶级域名(如 .com, .cn),且不是内网IP,判断为公网域名
|
||
const topLevelDomain = parts[parts.length - 1]
|
||
// 如果顶级域名不是数字(不是IP),且hostname不是内网IP,判断为公网域名
|
||
if (!/^\d+$/.test(topLevelDomain) && !isInternalIP(hostname)) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. 如果hostname不包含点号,且不是localhost/127.0.0.1,可能是内网主机名,不判断为公网
|
||
if (!hostname.includes('.')) {
|
||
return false
|
||
}
|
||
|
||
// 6. 默认情况:如果hostname包含点号且不是内网IP,判断为公网域名(保守策略)
|
||
// 但这里我们已经通过上面的检查了,所以这里应该不会执行到
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 从 X-Forwarded-For 头中提取真正的客户端IP
|
||
*
|
||
* X-Forwarded-For 格式:client_ip, proxy1_ip, proxy2_ip, ...
|
||
* - 最左边的IP是原始客户端IP
|
||
* - 但如果请求经过反向代理,第一个IP可能是反向代理的内网IP
|
||
*
|
||
* 策略:从右到左遍历,跳过内网IP(可信代理),找到第一个公网IP或最后一个IP
|
||
*
|
||
* @param forwarded X-Forwarded-For 头的值
|
||
* @returns 客户端IP地址
|
||
*/
|
||
export function extractClientIpFromForwarded(forwarded: string | null): string | null {
|
||
if (!forwarded) return null
|
||
|
||
// 分割IP列表
|
||
const ipList = forwarded.split(',').map(ip => ip.trim()).filter(ip => ip)
|
||
|
||
if (ipList.length === 0) return null
|
||
|
||
// 从右到左遍历,跳过内网IP(可信代理),找到第一个公网IP
|
||
for (let i = ipList.length - 1; i >= 0; i--) {
|
||
const ip = ipList[i]
|
||
if (!isInternalIP(ip)) {
|
||
// 找到第一个非内网IP,这应该是真正的客户端IP
|
||
return ip
|
||
}
|
||
}
|
||
|
||
// 如果所有IP都是内网IP,返回最后一个(最接近客户端的)
|
||
return ipList[ipList.length - 1]
|
||
}
|
||
|
||
/**
|
||
* 从请求中检测用户是否在内网(增强版,更严格的判断)
|
||
*
|
||
* 严谨的检测逻辑(优先级从高到低):
|
||
* 1. 优先检查 hostname(最高优先级):
|
||
* - 如果 hostname 是公网域名(白名单或匹配公网域名模式),立即判断为公网用户
|
||
* - 如果 hostname 是内网IP,继续检查客户端IP
|
||
* - 如果 hostname 是IP地址格式但不是内网IP,判断为公网用户
|
||
*
|
||
* 2. 正确解析客户端IP:
|
||
* - 从 X-Forwarded-For 中提取真正的客户端IP(跳过可信代理的内网IP)
|
||
* - 如果 X-Forwarded-For 不可用,使用 X-Real-IP
|
||
* - 如果 X-Real-IP 也不可用,检查其他可能的IP头(如 CF-Connecting-IP)
|
||
*
|
||
* 3. 三重验证(最严格):
|
||
* - 只有当 hostname 是内网IP 且 客户端IP也是内网IP时,才判断为内网用户
|
||
* - 如果 hostname 是公网域名,即使客户端IP是内网,也判断为公网用户(用户明确访问公网)
|
||
* - 如果 hostname 是IP地址但不是内网IP,判断为公网用户
|
||
* - 如果无法确定,默认判断为公网用户(保守策略,避免内网用户误判为公网)
|
||
*
|
||
* @param request NextRequest 对象
|
||
* @returns 是否在内网
|
||
*/
|
||
export function detectIsIntranet(request: NextRequest): boolean {
|
||
// 获取当前访问的域名
|
||
const host = request.headers.get('host') || ''
|
||
const hostname = host.split(':')[0].toLowerCase() // 移除端口号并转为小写
|
||
|
||
// ========== 第一步:检查 hostname 和客户端IP ==========
|
||
|
||
// 1.1 先获取客户端IP(用于判断用户实际网络位置)
|
||
const forwarded = request.headers.get('x-forwarded-for')
|
||
const realIp = request.headers.get('x-real-ip')
|
||
const cfConnectingIp = request.headers.get('cf-connecting-ip')
|
||
const trueClientIp = request.headers.get('true-client-ip')
|
||
|
||
let clientIp: string | null = null
|
||
if (forwarded) {
|
||
clientIp = extractClientIpFromForwarded(forwarded)
|
||
}
|
||
if (!clientIp && realIp) {
|
||
clientIp = realIp
|
||
}
|
||
if (!clientIp && cfConnectingIp) {
|
||
clientIp = cfConnectingIp
|
||
}
|
||
if (!clientIp && trueClientIp) {
|
||
clientIp = trueClientIp
|
||
}
|
||
|
||
// 1.2 如果 hostname 是公网域名,但客户端IP是内网IP,判断为内网用户
|
||
// 这是因为用户在内网访问公网域名(通过DNS解析到内网),应该使用内网资源
|
||
if (isPublicDomain(hostname)) {
|
||
if (clientIp && isInternalIP(clientIp)) {
|
||
// 用户在内网访问公网域名,判断为内网用户(使用内网资源)
|
||
return true
|
||
}
|
||
// 用户在外网访问公网域名,判断为公网用户
|
||
return false
|
||
}
|
||
|
||
// 1.2 检查 hostname 是否为IP地址格式
|
||
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
|
||
const isIpAddress = ipv4Regex.test(hostname)
|
||
|
||
if (isIpAddress) {
|
||
// hostname 是IP地址格式
|
||
const hostnameIsIntranet = isInternalIP(hostname)
|
||
|
||
if (!hostnameIsIntranet) {
|
||
// hostname 是IP地址但不是内网IP,判断为公网用户
|
||
return false
|
||
}
|
||
// hostname 是内网IP,继续检查客户端IP
|
||
} else {
|
||
// hostname 不是IP地址格式,也不是公网域名(已检查过)
|
||
// 可能是内网主机名(如 server.local),需要检查客户端IP
|
||
}
|
||
|
||
// ========== 第二步:检查客户端IP(仅当 hostname 是内网IP或内网主机名时) ==========
|
||
|
||
// 客户端IP已在第一步获取,这里不需要重复获取
|
||
|
||
// ========== 第三步:三重验证 ==========
|
||
|
||
// 如果 hostname 是内网IP或内网主机名
|
||
const hostnameIsIntranet = isInternalIP(hostname) || (!isIpAddress && !hostname.includes('.'))
|
||
|
||
if (hostnameIsIntranet) {
|
||
// 如果无法获取客户端IP,且 hostname 是内网IP,默认判断为内网用户
|
||
if (!clientIp || clientIp === 'unknown') {
|
||
// 但需要确保 hostname 确实是内网IP(不是公网域名误判)
|
||
if (isInternalIP(hostname)) {
|
||
return true
|
||
}
|
||
// 如果 hostname 不是内网IP(可能是内网主机名),但无法获取客户端IP,保守判断为公网用户
|
||
return false
|
||
}
|
||
|
||
// 判断客户端IP是否为内网IP
|
||
const clientIsIntranet = isInternalIP(clientIp)
|
||
|
||
// 三重验证:只有当 hostname 是内网IP 且 客户端IP也是内网IP时,才判断为内网用户
|
||
if (clientIsIntranet) {
|
||
return true
|
||
}
|
||
|
||
// 如果 hostname 是内网IP,但客户端IP是公网IP,可能是:
|
||
// 1. 用户通过VPN访问内网
|
||
// 2. 用户通过代理访问内网
|
||
// 3. 检测错误
|
||
// 保守策略:如果 hostname 是内网IP,即使客户端IP是公网,也判断为内网用户(因为用户明确访问内网地址)
|
||
if (isInternalIP(hostname)) {
|
||
return true
|
||
}
|
||
|
||
// hostname 不是内网IP(可能是内网主机名),但客户端IP是公网IP,判断为公网用户
|
||
return false
|
||
}
|
||
|
||
// hostname 既不是公网域名,也不是内网IP,也不是内网主机名
|
||
// 默认判断为公网用户(保守策略,避免内网用户误判为公网导致无法访问)
|
||
return false
|
||
}
|