Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/get-client-ip-address.ts
5818 views
1
import { isIP } from "net";
2
import { getClientIp } from "request-ip";
3
4
export function getClientIpAddress(req: {
5
headers: Record<string, string | string[] | undefined>;
6
}): string | undefined {
7
// Try manual extraction for headers not supported by request-ip
8
const headersToCheck = [
9
"cf-connecting-ip", // prioritize cloudflare
10
"x-client-ip",
11
"x-forwarded-for",
12
"fastly-client-ip",
13
"true-client-ip",
14
"x-real-ip",
15
"x-cluster-client-ip",
16
"appengine-user-ip",
17
];
18
19
// Check each header (case-insensitive)
20
for (const headerName of headersToCheck) {
21
const headerValue = getHeaderValue(req.headers, headerName);
22
if (headerValue) {
23
// Handle comma-separated values (like X-Forwarded-For)
24
const ips = headerValue.split(",").map((ip) => ip.trim());
25
for (const ip of ips) {
26
const processedIp = normalizeIPAddress(ip);
27
if (isIP(processedIp)) {
28
return processedIp;
29
}
30
}
31
}
32
}
33
34
// Try request-ip package as fallback
35
const ip = getClientIp(req);
36
if (ip && isIP(ip)) {
37
return ip;
38
}
39
40
// Fallback "Forwarded" header parsing, because this is not merged:
41
// https://github.com/pbojinov/request-ip/pull/71
42
const forwardedHeader = getHeaderValue(req.headers, "forwarded");
43
if (forwardedHeader) {
44
// Split by comma for multiple forwarded entries, trimming each entry
45
const forwardedEntries = forwardedHeader.split(",").map(entry => entry.trim());
46
47
for (const entry of forwardedEntries) {
48
// Split by semicolon for parameters, trimming each parameter
49
const params = entry.split(";").map(param => param.trim());
50
51
for (const param of params) {
52
if (param.toLowerCase().startsWith("for=")) {
53
let ipVal = param.substring(4).trim();
54
55
// Remove quotes if present
56
if (ipVal.startsWith('"') && ipVal.endsWith('"')) {
57
ipVal = ipVal.slice(1, -1);
58
}
59
60
// Normalize IP address (remove brackets and ports)
61
ipVal = normalizeIPAddress(ipVal);
62
63
if (isIP(ipVal)) {
64
return ipVal;
65
}
66
}
67
}
68
}
69
}
70
71
return undefined;
72
}
73
74
// Helper function to normalize IP address by removing brackets and ports
75
function normalizeIPAddress(ip: string): string {
76
let processedIp = ip.trim();
77
78
// Remove IPv6 brackets if present (do this first!)
79
const bracketStart = processedIp.startsWith("[");
80
const closingBracketIndex = processedIp.indexOf("]");
81
const hasPortAfterBracket = closingBracketIndex > 0 && processedIp[closingBracketIndex + 1] === ":";
82
if (bracketStart && hasPortAfterBracket) {
83
// Extract IPv6 part and port: [2001:db8::1]:8080 -> 2001:db8::1:8080
84
processedIp = processedIp.substring(1, closingBracketIndex) + processedIp.substring(closingBracketIndex + 1);
85
} else if (processedIp.startsWith("[") && processedIp.endsWith("]")) {
86
// Simple bracket removal: [2001:db8::1] -> 2001:db8::1
87
processedIp = processedIp.slice(1, -1);
88
}
89
90
// Strip port if present (handles both IPv4:port and IPv6:port)
91
if (processedIp.includes(":")) {
92
const lastColonIndex = processedIp.lastIndexOf(":");
93
if (lastColonIndex > 0) {
94
const potentialPort = processedIp.substring(lastColonIndex + 1);
95
// If the part after the last colon looks like a port number
96
if (/^\d+$/.test(potentialPort)) {
97
const potentialIP = processedIp.substring(0, lastColonIndex);
98
if (isIP(potentialIP)) {
99
processedIp = potentialIP;
100
}
101
}
102
}
103
}
104
105
return processedIp;
106
}
107
108
// Helper function to get header value case-insensitively
109
function getHeaderValue(
110
headers: Record<string, string | string[] | undefined>,
111
name: string,
112
): string | undefined {
113
const lowerName = name.toLowerCase();
114
115
// Check exact match first
116
const exactMatch = headers[lowerName];
117
if (exactMatch) {
118
return Array.isArray(exactMatch) ? exactMatch[0] : exactMatch;
119
}
120
121
// Check case-insensitive match
122
for (const [key, value] of Object.entries(headers)) {
123
if (key.toLowerCase() === lowerName && value) {
124
return Array.isArray(value) ? value[0] : value;
125
}
126
}
127
128
return undefined;
129
}
130
131