1+ import ipaddr from 'ipaddr.js' ;
2+
13export const IP_ADDRESS_HEADERS = [
24 'true-client-ip' , // CDN
35 'cf-connecting-ip' , // Cloudflare
@@ -13,35 +15,87 @@ export const IP_ADDRESS_HEADERS = [
1315 'x-forwarded' ,
1416] ;
1517
18+ /**
19+ * Normalize IP strings to a canonical form:
20+ * - strips IPv4-mapped IPv6 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1)
21+ * - keeps valid IPv4/IPv6 as-is (canonically formatted by ipaddr.js)
22+ */
23+ function normalizeIp ( ip ?: string | null ) {
24+ if ( ! ip ) return ip ;
25+
26+ try {
27+ const parsed = ipaddr . parse ( ip ) ;
28+
29+ if ( parsed . kind ( ) === 'ipv6' && ( parsed as ipaddr . IPv6 ) . isIPv4MappedAddress ( ) ) {
30+ return ( parsed as ipaddr . IPv6 ) . toIPv4Address ( ) . toString ( ) ;
31+ }
32+
33+ return parsed . toString ( ) ;
34+ } catch {
35+ // Fallback: return original if parsing fails
36+ return ip ;
37+ }
38+ }
39+
40+ function resolveIp ( ip ?: string | null ) {
41+ if ( ! ip ) return ip ;
42+
43+ // First, try as-is
44+ const normalized = normalizeIp ( ip ) ;
45+ try {
46+ ipaddr . parse ( normalized ) ;
47+ return normalized ;
48+ } catch {
49+ // try stripping port (handles IPv4:port; leaves IPv6 intact)
50+ const stripped = stripPort ( ip ) ;
51+ if ( stripped !== ip ) {
52+ const normalizedStripped = normalizeIp ( stripped ) ;
53+ try {
54+ ipaddr . parse ( normalizedStripped ) ;
55+ return normalizedStripped ;
56+ } catch {
57+ return normalizedStripped ;
58+ }
59+ }
60+
61+ return normalized ;
62+ }
63+ }
64+
1665export function getIpAddress ( headers : Headers ) {
1766 const customHeader = process . env . CLIENT_IP_HEADER ;
1867
1968 if ( customHeader && headers . get ( customHeader ) ) {
20- return headers . get ( customHeader ) ;
69+ return resolveIp ( headers . get ( customHeader ) ) ;
2170 }
2271
23- const header = IP_ADDRESS_HEADERS . find ( name => {
24- return headers . get ( name ) ;
25- } ) ;
72+ const header = IP_ADDRESS_HEADERS . find ( name => headers . get ( name ) ) ;
73+ if ( ! header ) {
74+ return undefined ;
75+ }
2676
2777 const ip = headers . get ( header ) ;
2878
2979 if ( header === 'x-forwarded-for' ) {
30- return ip ?. split ( ',' ) ?. [ 0 ] ?. trim ( ) ;
80+ return resolveIp ( ip ?. split ( ',' ) ?. [ 0 ] ?. trim ( ) ) ;
3181 }
3282
3383 if ( header === 'forwarded' ) {
3484 const match = ip . match ( / f o r = ( \[ ? [ 0 - 9 a - f A - F : . ] + \] ? ) / ) ;
3585
3686 if ( match ) {
37- return match [ 1 ] ;
87+ return resolveIp ( match [ 1 ] ) ;
3888 }
3989 }
4090
41- return ip ;
91+ return resolveIp ( ip ) ;
4292}
4393
44- export function stripPort ( ip : string ) {
94+ export function stripPort ( ip ?: string | null ) {
95+ if ( ! ip ) {
96+ return ip ;
97+ }
98+
4599 if ( ip . startsWith ( '[' ) ) {
46100 const endBracket = ip . indexOf ( ']' ) ;
47101 if ( endBracket !== - 1 ) {
0 commit comments