summaryrefslogtreecommitdiff
path: root/lib/network/get-client-ip.ts
blob: 566700e979142566963d436130b4a304c286c24f (plain)
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,
    };
  }