feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { getHeaders } from 'h3'
|
||||
import { isIP } from 'node:net'
|
||||
|
||||
const SUPPORTED_CLIENT_IP_HEADERS = new Set([
|
||||
'eo-connecting-ip',
|
||||
'eo-client-ip',
|
||||
'x-edgeone-client-ip',
|
||||
'edgeone-client-ip',
|
||||
'cf-connecting-ip',
|
||||
'true-client-ip',
|
||||
'fastly-client-ip',
|
||||
'fly-client-ip',
|
||||
'x-nf-client-connection-ip',
|
||||
'x-vercel-forwarded-for',
|
||||
'x-azure-clientip',
|
||||
'x-appengine-user-ip',
|
||||
'x-cluster-client-ip',
|
||||
'x-real-ip',
|
||||
'x-client-ip',
|
||||
'x-real-client-ip',
|
||||
'x-forwarded-client-ip',
|
||||
'x-original-forwarded-for',
|
||||
'x-forwarded-for',
|
||||
'x-forwarded',
|
||||
'forwarded-for',
|
||||
'forwarded'
|
||||
])
|
||||
|
||||
// 未配置 TRUSTED_CLIENT_IP_HEADERS 时的回退头部列表,覆盖主流平台
|
||||
const FALLBACK_CLIENT_IP_HEADERS = Object.freeze([
|
||||
'x-forwarded-for',
|
||||
'x-real-ip',
|
||||
'x-vercel-forwarded-for',
|
||||
'cf-connecting-ip',
|
||||
'true-client-ip'
|
||||
])
|
||||
|
||||
function getTrustedClientIPHeaders() {
|
||||
const configured = String(process.env.TRUSTED_CLIENT_IP_HEADERS || '')
|
||||
.split(',')
|
||||
.map((header) => header.trim().toLowerCase())
|
||||
.filter((header) => SUPPORTED_CLIENT_IP_HEADERS.has(header))
|
||||
|
||||
return configured.length > 0
|
||||
? [...new Set(configured)]
|
||||
: FALLBACK_CLIENT_IP_HEADERS
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
* @param event H3Event对象
|
||||
* @returns 客户端IP地址
|
||||
*/
|
||||
export function getClientIP(event: H3Event): string {
|
||||
const headers = getHeaders(event)
|
||||
|
||||
// 只有部署者明确声明受信头部时才读取转发头,避免客户端直连源站时伪造 IP。
|
||||
const ipHeaders = getTrustedClientIPHeaders()
|
||||
|
||||
for (const header of ipHeaders) {
|
||||
const value = headers[header]
|
||||
if (value) {
|
||||
const values = Array.isArray(value) ? value : [value]
|
||||
for (const item of values) {
|
||||
for (const candidate of String(item).split(',')) {
|
||||
const ip = normalizeIPCandidate(candidate)
|
||||
if (ip) return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到,直接从连接信息获取
|
||||
|
||||
// 从连接信息获取IP(备用方法)
|
||||
const remoteAddress = event.node.req.socket?.remoteAddress
|
||||
if (remoteAddress) {
|
||||
// 移除IPv6映射的IPv4前缀
|
||||
const cleanIP = normalizeIPCandidate(remoteAddress)
|
||||
if (cleanIP) {
|
||||
return cleanIP
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function normalizeIPCandidate(value: string): string | null {
|
||||
let candidate = value.trim().replace(/^"|"$/g, '')
|
||||
const forwardedMatch = candidate.match(/^for\s*=\s*(.+)$/i)
|
||||
if (forwardedMatch) {
|
||||
const forwardedValue = forwardedMatch[1] || ''
|
||||
candidate = (forwardedValue.split(';').shift() || '').trim().replace(/^"|"$/g, '')
|
||||
}
|
||||
|
||||
if (candidate.startsWith('[')) {
|
||||
const closingBracket = candidate.indexOf(']')
|
||||
if (closingBracket > 0) candidate = candidate.slice(1, closingBracket)
|
||||
} else if (isIP(candidate) === 0 && /^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(candidate)) {
|
||||
candidate = candidate.slice(0, candidate.lastIndexOf(':'))
|
||||
}
|
||||
|
||||
const cleanIP = candidate.replace(/^::ffff:/i, '')
|
||||
return isValidIP(cleanIP) ? cleanIP : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证IP地址格式是否有效
|
||||
* @param ip IP地址字符串
|
||||
* @returns 是否为有效IP
|
||||
*/
|
||||
function isValidIP(ip: string): boolean {
|
||||
// Node 原生解析器支持压缩 IPv6,避免大量合法地址被归入 unknown 共享限流桶。
|
||||
if (isIP(ip) === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 排除明显无效的IP
|
||||
if (ip === '0.0.0.0' || ip === '::') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化IP地址用于邮件显示
|
||||
* @param ip IP地址
|
||||
* @returns 格式化后的IP地址字符串
|
||||
*/
|
||||
export function formatIPForEmail(ip: string): string {
|
||||
if (!ip || ip === 'unknown') {
|
||||
return '未知'
|
||||
}
|
||||
|
||||
// 如果是本地IP,显示为本地访问
|
||||
if (
|
||||
ip === '127.0.0.1' ||
|
||||
ip === '::1' ||
|
||||
ip.startsWith('192.168.') ||
|
||||
ip.startsWith('10.') ||
|
||||
ip.startsWith('172.')
|
||||
) {
|
||||
return `${ip} (本地网络)`
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
Reference in New Issue
Block a user