gongxiangp2/lib/network-detection.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

364 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// LEGACY_READ_ONLY: 旧域名仅用于识别历史网络资源。
/**
* 网络检测工具
* 用于在后端 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',
'sc.jiansun.vip',
's3.yydd.ai',
's3.jiansun.vip',
'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 是公网域名(白名单或匹配公网域名模式),继续检查客户端IP
* - 如果 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,判断为内网用户(内网用户通过公网域名访问)
* - 如果 hostname 是内网IP,且客户端IP也是内网IP,判断为内网用户
* - 如果 hostname 是公网域名,且客户端IP是公网IP,判断为公网用户
* - 如果无法确定,默认判断为公网用户(保守策略)
*
* 注意:VPN用户可能显示内网IP但实际无法访问内网资源,这种情况下:
* - 如果用户通过公网域名访问,且IP显示为内网,我们假设用户在内网
* - 如果实际无法访问内网MinIO,前端会自动降级到代理访问
*
* @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(用于判断用户实际网络位置)
// 收集所有可能的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')
// 尝试从 socket 获取远程地址(Next.js 可能不支持,但尝试一下)
const remoteAddr = (request as any).socket?.remoteAddress ||
(request as any).connection?.remoteAddress ||
request.headers.get('x-client-ip') ||
request.headers.get('x-cluster-client-ip')
// 收集所有IP来源用于判断
const allIpSources: { source: string; ip: string | null }[] = [
{ source: 'x-forwarded-for', ip: forwarded },
{ source: 'x-real-ip', ip: realIp },
{ source: 'cf-connecting-ip', ip: cfConnectingIp },
{ source: 'true-client-ip', ip: trueClientIp },
{ source: 'remote-addr', ip: remoteAddr },
]
let clientIp: string | null = null
let clientIpSource: string = 'unknown'
// 优先使用 X-Forwarded-For(最常见的代理头)
if (forwarded) {
clientIp = extractClientIpFromForwarded(forwarded)
clientIpSource = 'x-forwarded-for'
}
// 其次使用 X-Real-IP(Nginx 常用)
if (!clientIp && realIp) {
clientIp = realIp
clientIpSource = 'x-real-ip'
}
// Cloudflare 的头
if (!clientIp && cfConnectingIp) {
clientIp = cfConnectingIp
clientIpSource = 'cf-connecting-ip'
}
// Akamai 等 CDN 的头
if (!clientIp && trueClientIp) {
clientIp = trueClientIp
clientIpSource = 'true-client-ip'
}
// 最后尝试远程地址
if (!clientIp && remoteAddr) {
clientIp = remoteAddr
clientIpSource = 'remote-addr'
}
// 检查 X-Forwarded-For 中是否有任何内网IP(更宽松的判断)
// 如果 X-Forwarded-For 中包含内网IP,说明请求经过了内网代理,用户很可能在内网
let hasInternalIpInForwarded = false
if (forwarded) {
const ipList = forwarded.split(',').map(ip => ip.trim()).filter(ip => ip)
hasInternalIpInForwarded = ipList.some(ip => isInternalIP(ip))
}
// 调试日志(仅在开发环境或需要诊断时启用)
const debugNetworkDetection = process.env.DEBUG_NETWORK_DETECTION === 'true'
if (debugNetworkDetection) {
console.log('[Network Detection] ========== 开始检测 ==========')
console.log('[Network Detection] hostname:', hostname)
console.log('[Network Detection] IP sources:', allIpSources.filter(s => s.ip).map(s => `${s.source}=${s.ip}`).join(', '))
console.log('[Network Detection] clientIp:', clientIp, 'from:', clientIpSource)
console.log('[Network Detection] hasInternalIpInForwarded:', hasInternalIpInForwarded)
}
// 1.2 如果 hostname 是公网域名,检查客户端IP
if (isPublicDomain(hostname)) {
// 判断用户是否在内网的条件(严格版本):
// 只检查真正的客户端IP是否为内网IP
// 注意:不能用 hasInternalIpInForwarded,因为反向代理/负载均衡器的内网IP会导致误判
// 例如:公网用户 -> Nginx代理(192.168.x.x) -> 应用,X-Forwarded-For 中会包含代理的内网IP
const isIntranetUser = !!(clientIp && isInternalIP(clientIp))
if (debugNetworkDetection) {
console.log('[Network Detection] isPublicDomain:', true)
console.log('[Network Detection] clientIp:', clientIp)
console.log('[Network Detection] clientIp isInternal:', clientIp ? isInternalIP(clientIp) : 'N/A')
console.log('[Network Detection] hasInternalIpInForwarded (仅供参考,不用于判断):', hasInternalIpInForwarded)
console.log('[Network Detection] 判断结果:', isIntranetUser ? '内网用户' : '外网用户')
console.log('[Network Detection] ====================================')
}
return isIntranetUser
}
// 1.3 检查 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,判断为公网用户
if (debugNetworkDetection) {
console.log('[Network Detection] hostname是公网IP,判断为外网用户')
console.log('[Network Detection] ====================================')
}
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)) {
if (debugNetworkDetection) {
console.log('[Network Detection] hostname是内网IP,无法获取clientIp,判断为内网用户')
console.log('[Network Detection] ====================================')
}
return true
}
// 如果 hostname 不是内网IP(可能是内网主机名),但无法获取客户端IP,保守判断为公网用户
if (debugNetworkDetection) {
console.log('[Network Detection] 无法确定,保守判断为外网用户')
console.log('[Network Detection] ====================================')
}
return false
}
// 判断客户端IP是否为内网IP
const clientIsIntranet = isInternalIP(clientIp)
// 三重验证:只有当 hostname 是内网IP 且 客户端IP也是内网IP时,才判断为内网用户
if (clientIsIntranet) {
if (debugNetworkDetection) {
console.log('[Network Detection] hostname和clientIp都是内网,判断为内网用户')
console.log('[Network Detection] ====================================')
}
return true
}
// 如果 hostname 是内网IP,但客户端IP是公网IP,可能是:
// 1. 用户通过VPN访问内网
// 2. 用户通过代理访问内网
// 3. 检测错误
// 保守策略:如果 hostname 是内网IP,即使客户端IP是公网,也判断为内网用户(因为用户明确访问内网地址)
if (isInternalIP(hostname)) {
if (debugNetworkDetection) {
console.log('[Network Detection] hostname是内网IP,即使clientIp是公网,也判断为内网用户')
console.log('[Network Detection] ====================================')
}
return true
}
// hostname 不是内网IP(可能是内网主机名),但客户端IP是公网IP,判断为公网用户
if (debugNetworkDetection) {
console.log('[Network Detection] hostname是内网主机名但clientIp是公网,判断为外网用户')
console.log('[Network Detection] ====================================')
}
return false
}
// hostname 既不是公网域名,也不是内网IP,也不是内网主机名
// 默认判断为公网用户(保守策略,避免内网用户误判为公网导致无法访问)
if (debugNetworkDetection) {
console.log('[Network Detection] 无法确定hostname类型,保守判断为外网用户')
console.log('[Network Detection] ====================================')
}
return false
}