Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
364 lines
14 KiB
TypeScript
364 lines
14 KiB
TypeScript
// 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
|
||
}
|