winupdate-neo/lib/clientIp.ts
2026-06-09 18:43:54 +08:00

148 lines
3.4 KiB
TypeScript

import { isIP } from 'net'
import type { NextRequest } from 'next/server'
const DIRECT_IP_HEADERS = [
'cf-connecting-ip',
'true-client-ip',
'fly-client-ip',
'fastly-client-ip'
]
const FALLBACK_IP_HEADERS = [
'x-real-ip',
'x-client-ip'
]
const LIST_IP_HEADERS = [
'x-forwarded-for',
'x-original-forwarded-for'
]
export function getClientIp(req: NextRequest) {
return getClientIpFromHeaders(req.headers)
}
export function getClientIpFromHeaders(headers: Headers) {
const candidates: string[] = []
for (const header of DIRECT_IP_HEADERS) {
const value = headers.get(header)
if (value) {
candidates.push(value)
}
}
const forwarded = headers.get('forwarded')
if (forwarded) {
candidates.push(...parseForwardedHeader(forwarded))
}
for (const header of LIST_IP_HEADERS) {
const value = headers.get(header)
if (value) {
candidates.push(...value.split(','))
}
}
for (const header of FALLBACK_IP_HEADERS) {
const value = headers.get(header)
if (value) {
candidates.push(value)
}
}
for (const candidate of candidates) {
const ip = normalizeIpCandidate(candidate)
if (ip) {
return ip
}
}
return null
}
export function normalizeIpCandidate(value?: string | null) {
if (!value) {
return null
}
let candidate = value.trim()
if (!candidate || candidate.toLowerCase() === 'unknown') {
return null
}
if (candidate.startsWith('"') && candidate.endsWith('"')) {
candidate = candidate.slice(1, -1)
}
if (candidate.startsWith('[')) {
const closingBracketIndex = candidate.indexOf(']')
if (closingBracketIndex > 0) {
candidate = candidate.slice(1, closingBracketIndex)
}
} else {
const colonCount = (candidate.match(/:/g) || []).length
if (colonCount === 1 && candidate.includes('.')) {
candidate = candidate.slice(0, candidate.lastIndexOf(':'))
}
}
const zoneIndex = candidate.indexOf('%')
if (zoneIndex > -1) {
candidate = candidate.slice(0, zoneIndex)
}
candidate = candidate.trim()
if (candidate.toLowerCase().startsWith('::ffff:')) {
const ipv4 = candidate.slice(7)
if (isIP(ipv4) === 4) {
return ipv4
}
}
return isIP(candidate) ? candidate : null
}
export function isPrivateIp(ip: string) {
const normalizedIp = normalizeIpCandidate(ip)
if (!normalizedIp) {
return false
}
if (isIP(normalizedIp) === 4) {
const [first, second] = normalizedIp.split('.').map(Number)
return first === 10 ||
first === 127 ||
first === 0 ||
(first === 172 && second >= 16 && second <= 31) ||
(first === 192 && second === 168) ||
(first === 169 && second === 254) ||
(first === 100 && second >= 64 && second <= 127)
}
const lowerIp = normalizedIp.toLowerCase()
return lowerIp === '::1' ||
lowerIp === '::' ||
lowerIp.startsWith('fc') ||
lowerIp.startsWith('fd') ||
lowerIp.startsWith('fe8') ||
lowerIp.startsWith('fe9') ||
lowerIp.startsWith('fea') ||
lowerIp.startsWith('feb')
}
function parseForwardedHeader(value: string) {
const candidates: string[] = []
for (const entry of value.split(',')) {
for (const segment of entry.split(';')) {
const [key, rawValue] = segment.split('=')
if (key?.trim().toLowerCase() === 'for' && rawValue) {
candidates.push(rawValue.trim())
}
}
}
return candidates
}