11import { createHash } from 'node:crypto' ;
22import type { City } from '@maxmind/geoip2-node' ;
3- import {
4- AddressNotFoundError ,
5- BadMethodCallError ,
6- Reader ,
7- } from '@maxmind/geoip2-node' ;
8- import { logger } from '../lib/logger' ;
3+ import { AddressNotFoundError , Reader } from '@maxmind/geoip2-node' ;
4+
5+ // import { logger } from '../lib/logger';
6+ const logger = console ;
97
108interface GeoIPReader extends Reader {
119 city ( ip : string ) : City ;
1210}
1311
14- const CDN_URL = 'https://cdn.databuddy.cc/GeoLite2-City.mmdb' ;
12+ const IPV4_URL = process . env . IPV4_URL ;
13+ const IPV6_URL = process . env . IPV6_URL ;
14+
15+ const CDN_URLS = {
16+ ipv4 : IPV4_URL ,
17+ ipv6 : IPV6_URL ,
18+ } as const ;
1519
16- let reader : GeoIPReader | null = null ;
20+ let ipv4Reader : GeoIPReader | null = null ;
21+ let ipv6Reader : GeoIPReader | null = null ;
1722let isLoading = false ;
18- let loadPromise : Promise < void > | null = null ;
19- let loadError : Error | null = null ;
2023
21- async function loadDatabaseFromCdn ( ) : Promise < Buffer > {
24+ // Initialize databases on module load
25+ loadDatabases ( ) . catch ( ( error ) => {
26+ logger . error ( 'Failed to initialize GeoIP databases on startup:' , { error } ) ;
27+ } ) ;
28+
29+ async function loadDatabases ( ) {
30+ if ( isLoading || ( ipv4Reader && ipv6Reader ) ) {
31+ logger . info ( 'GeoIP databases already loaded or loading' ) ;
32+ return ;
33+ }
34+
35+ logger . info ( 'Starting to load GeoIP databases...' ) ;
36+ isLoading = true ;
37+
2238 try {
23- const response = await fetch ( CDN_URL ) ;
24- if ( ! response . ok ) {
39+ logger . info ( 'Fetching IPv4 and IPv6 databases from CDN...' ) ;
40+ const [ ipv4Response , ipv6Response ] = await Promise . all ( [
41+ fetch ( CDN_URLS . ipv4 || '' ) ,
42+ fetch ( CDN_URLS . ipv6 || '' ) ,
43+ ] ) ;
44+
45+ if ( ! ipv4Response . ok ) {
2546 throw new Error (
26- `Failed to fetch database from CDN : ${ response . status } ${ response . statusText } `
47+ `IPv4 database fetch failed : ${ ipv4Response . status } ${ ipv4Response . statusText } `
2748 ) ;
2849 }
29-
30- const arrayBuffer = await response . arrayBuffer ( ) ;
31- const dbBuffer = Buffer . from ( arrayBuffer ) ;
32-
33- if ( dbBuffer . length < 1_000_000 ) {
50+ if ( ! ipv6Response . ok ) {
3451 throw new Error (
35- `Database file seems too small : ${ dbBuffer . length } bytes `
52+ `IPv6 database fetch failed : ${ ipv6Response . status } ${ ipv6Response . statusText } `
3653 ) ;
3754 }
3855
39- return dbBuffer ;
40- } catch ( error ) {
41- logger . error ( 'Failed to load database from CDN:' , { error } ) ;
42- throw error ;
43- }
44- }
56+ logger . info ( 'Converting database responses to buffers...' ) ;
57+ const [ ipv4Buffer , ipv6Buffer ] = await Promise . all ( [
58+ ipv4Response . arrayBuffer ( ) ,
59+ ipv6Response . arrayBuffer ( ) ,
60+ ] ) ;
4561
46- function loadDatabase ( ) {
47- if ( loadError ) {
48- throw loadError ;
49- }
62+ logger . info (
63+ `Database sizes - IPv4: ${ ipv4Buffer . byteLength } bytes, IPv6: ${ ipv6Buffer . byteLength } bytes`
64+ ) ;
5065
51- if ( isLoading && loadPromise ) {
52- return loadPromise ;
53- }
66+ logger . info ( 'Opening database readers...' ) ;
67+ ipv4Reader = Reader . openBuffer ( Buffer . from ( ipv4Buffer ) ) as GeoIPReader ;
68+ ipv6Reader = Reader . openBuffer ( Buffer . from ( ipv6Buffer ) ) as GeoIPReader ;
5469
55- if ( reader ) {
56- return ;
70+ logger . info ( 'GeoIP databases loaded successfully' ) ;
71+ } catch ( error ) {
72+ logger . error ( 'Failed to load GeoIP databases:' , {
73+ error : error instanceof Error ? error . message : String ( error ) ,
74+ stack : error instanceof Error ? error . stack : undefined ,
75+ } ) ;
76+ } finally {
77+ isLoading = false ;
5778 }
58-
59- isLoading = true ;
60- loadPromise = ( async ( ) => {
61- try {
62- const dbBuffer = await loadDatabaseFromCdn ( ) ;
63-
64- await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
65-
66- reader = Reader . openBuffer ( dbBuffer ) as GeoIPReader ;
67- } catch ( error ) {
68- logger . error ( 'Failed to load GeoIP database:' , { error } ) ;
69- loadError = error as Error ;
70- reader = null ;
71- } finally {
72- isLoading = false ;
73- }
74- } ) ( ) ;
75-
76- return loadPromise ;
7779}
7880
7981const ignore = [ '127.0.0.1' , '::1' ] ;
@@ -83,67 +85,79 @@ const ipv4Regex =
8385
8486const ipv6Regex = / ^ (?: [ 0 - 9 a - f A - F ] { 1 , 4 } : ) { 7 } [ 0 - 9 a - f A - F ] { 1 , 4 } $ / ;
8587
86- function isValidIp ( ip : string ) : boolean {
88+ function getIpType ( ip : string ) : 'ipv4' | 'ipv6' | null {
8789 if ( ! ip ) {
88- return false ;
90+ return null ;
8991 }
9092
9193 if ( ipv4Regex . test ( ip ) ) {
92- return true ;
94+ return 'ipv4' ;
9395 }
9496
9597 if ( ipv6Regex . test ( ip ) ) {
96- return true ;
98+ return 'ipv6' ;
9799 }
98100
99- return false ;
101+ return null ;
100102}
101103
102104export async function getGeoLocation ( ip : string ) {
103- if ( ! ip || ignore . includes ( ip ) || ! isValidIp ( ip ) ) {
105+ if ( ! ip || ignore . includes ( ip ) ) {
106+ logger . debug ( 'IP ignored or empty' , { ip, ignored : ignore . includes ( ip ) } ) ;
104107 return { country : undefined , region : undefined , city : undefined } ;
105108 }
106109
107- if ( ! ( reader || isLoading || loadError ) ) {
108- try {
109- await loadDatabase ( ) ;
110- } catch ( error ) {
111- logger . error ( 'Failed to load database for IP lookup:' , { error } ) ;
112- return { country : undefined , region : undefined , city : undefined } ;
113- }
110+ const ipType = getIpType ( ip ) ;
111+ if ( ! ipType ) {
112+ logger . warn ( 'Invalid IP format' , { ip } ) ;
113+ return { country : undefined , region : undefined , city : undefined } ;
114114 }
115115
116+ logger . debug ( 'Processing IP lookup' , { ip, type : ipType } ) ;
117+
118+ await loadDatabases ( ) ;
119+
120+ const reader = ipType === 'ipv4' ? ipv4Reader : ipv6Reader ;
116121 if ( ! reader ) {
122+ logger . error ( 'Database reader not available' , {
123+ ip,
124+ type : ipType ,
125+ hasIpv4Reader : ! ! ipv4Reader ,
126+ hasIpv6Reader : ! ! ipv6Reader ,
127+ } ) ;
117128 return { country : undefined , region : undefined , city : undefined } ;
118129 }
119130
120131 try {
121132 const response = reader . city ( ip ) ;
122-
123- // Extract region and city data
124133 const region = response . subdivisions ?. [ 0 ] ?. names ?. en ;
125134 const city = response . city ?. names ?. en ;
135+ const country = response . country ?. names ?. en ;
136+
137+ logger . debug ( 'IP lookup successful' , {
138+ ip,
139+ type : ipType ,
140+ country,
141+ region,
142+ city,
143+ } ) ;
126144
127145 return {
128- country : response . country ?. names ?. en ,
146+ country,
129147 region,
130148 city,
131149 } ;
132150 } catch ( error ) {
133- // Handle AddressNotFoundError specifically (IP not in database)
134151 if ( error instanceof AddressNotFoundError ) {
152+ logger . debug ( 'IP not found in database' , { ip, type : ipType } ) ;
135153 return { country : undefined , region : undefined , city : undefined } ;
136154 }
137-
138- // Handle BadMethodCallError (wrong database type)
139- if ( error instanceof BadMethodCallError ) {
140- logger . error (
141- 'Database type mismatch - using city() method with ipinfo database'
142- ) ;
143- return { country : undefined , region : undefined , city : undefined } ;
144- }
145-
146- logger . error ( 'Error looking up IP:' , { ip, error } ) ;
155+ logger . error ( 'Error looking up IP:' , {
156+ ip,
157+ type : ipType ,
158+ error : error instanceof Error ? error . message : String ( error ) ,
159+ stack : error instanceof Error ? error . stack : undefined ,
160+ } ) ;
147161 return { country : undefined , region : undefined , city : undefined } ;
148162 }
149163}
0 commit comments