// 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 }