148 lines
3.4 KiB
TypeScript
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
|
|
} |