1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
import { NextRequest } from 'next/server';
export interface GetClientIpOptions {
/**
* Which index to take from a comma-separated forwarded-for list.
* 0 = leftmost (original client; default).
* -1 = rightmost (closest hop).
*/
forwardedIndex?: number;
/**
* Custom header priority override. Default uses common reverse-proxy headers.
*/
headerPriority?: readonly string[];
/**
* Value to return when nothing found.
* Default: 'unknown'
*/
fallback?: string;
}
const DEFAULT_HEADER_PRIORITY = [
'x-forwarded-for',
'x-real-ip',
'cf-connecting-ip',
'vercel-forwarded-for',
] as const;
export function getClientIp(
req: NextRequest,
opts: GetClientIpOptions = {}
): string {
const {
forwardedIndex = 0,
headerPriority = DEFAULT_HEADER_PRIORITY,
fallback = 'unknown',
} = opts;
for (const h of headerPriority) {
const raw = req.headers.get(h);
if (!raw) continue;
// headers like x-forwarded-for can be CSV
const parts = raw.split(',').map(p => p.trim()).filter(Boolean);
if (parts.length === 0) continue;
const idx =
forwardedIndex >= 0
? forwardedIndex
: parts.length + forwardedIndex; // support -1 end indexing
const sel = parts[idx] ?? parts[0]; // safe fallback to 0
const norm = normalizeIp(sel);
if (norm) return norm;
}
return fallback;
}
/**
* Normalize IPv4/IPv6/port-suffixed forms to a canonical-ish string.
*/
export function normalizeIp(raw: string | null | undefined): string {
if (!raw) return '';
let ip = raw.trim();
// Strip brackets: [2001:db8::1]
if (ip.startsWith('[') && ip.endsWith(']')) {
ip = ip.slice(1, -1);
}
// IPv4:port (because some proxies send ip:port)
// Heuristic: if '.' present (IPv4-ish) and ':' present, drop port after last colon.
if (ip.includes('.') && ip.includes(':')) {
ip = ip.slice(0, ip.lastIndexOf(':'));
}
// IPv4-mapped IPv6 ::ffff:203.0.113.5
if (ip.startsWith('::ffff:')) {
ip = ip.substring(7);
}
return ip;
}
export interface RequestInfo {
ip: string;
userAgent: string;
referer?: string;
requestId: string;
method: string;
url: string;
}
export function getRequestInfo(req: NextRequest, ipOpts?: GetClientIpOptions): RequestInfo {
const ip = getClientIp(req, ipOpts);
const userAgent = req.headers.get('user-agent')?.trim() || 'unknown';
const refererRaw = req.headers.get('referer')?.trim();
const referer = refererRaw && refererRaw.length > 0 ? refererRaw : undefined;
const { href, pathname } = req.nextUrl;
return {
ip,
userAgent,
referer,
requestId: (globalThis.crypto?.randomUUID?.() ?? crypto.randomUUID()),
method: req.method,
url: href || pathname,
};
}
|