mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 13:17:19 +01:00
114 lines
2.5 KiB
TypeScript
114 lines
2.5 KiB
TypeScript
import ipaddr from 'ipaddr.js';
|
|
|
|
export const IP_ADDRESS_HEADERS = [
|
|
'true-client-ip', // CDN
|
|
'cf-connecting-ip', // Cloudflare
|
|
'fastly-client-ip', // Fastly
|
|
'x-nf-client-connection-ip', // Netlify
|
|
'do-connecting-ip', // Digital Ocean
|
|
'x-real-ip', // Reverse proxy
|
|
'x-appengine-user-ip', // Google App Engine
|
|
'x-forwarded-for',
|
|
'forwarded',
|
|
'x-client-ip',
|
|
'x-cluster-client-ip',
|
|
'x-forwarded',
|
|
];
|
|
|
|
/**
|
|
* Normalize IP strings to a canonical form:
|
|
* - strips IPv4-mapped IPv6 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1)
|
|
* - keeps valid IPv4/IPv6 as-is (canonically formatted by ipaddr.js)
|
|
*/
|
|
function normalizeIp(ip?: string | null) {
|
|
if (!ip) return ip;
|
|
|
|
try {
|
|
const parsed = ipaddr.parse(ip);
|
|
|
|
if (parsed.kind() === 'ipv6' && (parsed as ipaddr.IPv6).isIPv4MappedAddress()) {
|
|
return (parsed as ipaddr.IPv6).toIPv4Address().toString();
|
|
}
|
|
|
|
return parsed.toString();
|
|
} catch {
|
|
// Fallback: return original if parsing fails
|
|
return ip;
|
|
}
|
|
}
|
|
|
|
function resolveIp(ip?: string | null) {
|
|
if (!ip) return ip;
|
|
|
|
// First, try as-is
|
|
const normalized = normalizeIp(ip);
|
|
try {
|
|
ipaddr.parse(normalized);
|
|
return normalized;
|
|
} catch {
|
|
// try stripping port (handles IPv4:port; leaves IPv6 intact)
|
|
const stripped = stripPort(ip);
|
|
if (stripped !== ip) {
|
|
const normalizedStripped = normalizeIp(stripped);
|
|
try {
|
|
ipaddr.parse(normalizedStripped);
|
|
return normalizedStripped;
|
|
} catch {
|
|
return normalizedStripped;
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
}
|
|
|
|
export function getIpAddress(headers: Headers) {
|
|
const customHeader = process.env.CLIENT_IP_HEADER;
|
|
|
|
if (customHeader && headers.get(customHeader)) {
|
|
return resolveIp(headers.get(customHeader));
|
|
}
|
|
|
|
const header = IP_ADDRESS_HEADERS.find(name => headers.get(name));
|
|
if (!header) {
|
|
return undefined;
|
|
}
|
|
|
|
const ip = headers.get(header);
|
|
|
|
if (header === 'x-forwarded-for') {
|
|
return resolveIp(ip?.split(',')?.[0]?.trim());
|
|
}
|
|
|
|
if (header === 'forwarded') {
|
|
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
|
|
|
|
if (match) {
|
|
return resolveIp(match[1]);
|
|
}
|
|
}
|
|
|
|
return resolveIp(ip);
|
|
}
|
|
|
|
export function stripPort(ip?: string | null) {
|
|
if (!ip) {
|
|
return ip;
|
|
}
|
|
|
|
if (ip.startsWith('[')) {
|
|
const endBracket = ip.indexOf(']');
|
|
if (endBracket !== -1) {
|
|
return ip.slice(0, endBracket + 1);
|
|
}
|
|
}
|
|
|
|
const idx = ip.lastIndexOf(':');
|
|
if (idx !== -1) {
|
|
if (ip.includes('.') || /^[a-zA-Z0-9.-]+$/.test(ip.slice(0, idx))) {
|
|
return ip.slice(0, idx);
|
|
}
|
|
}
|
|
|
|
return ip;
|
|
}
|