Skip to content

Commit 1b68970

Browse files
committed
fix: maxmind service implementation
1 parent 6250344 commit 1b68970

3 files changed

Lines changed: 286 additions & 3 deletions

File tree

config/request-analytics.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
'enabled' => env('REQUEST_ANALYTICS_GEO_ENABLED', true),
3535
'provider' => env('REQUEST_ANALYTICS_GEO_PROVIDER', 'ipapi'), // ipapi, ipgeolocation, maxmind
3636
'api_key' => env('REQUEST_ANALYTICS_GEO_API_KEY'),
37+
38+
// MaxMind specific configuration
39+
'maxmind' => [
40+
'type' => env('REQUEST_ANALYTICS_MAXMIND_TYPE', 'webservice'), // webservice or database
41+
'user_id' => env('REQUEST_ANALYTICS_MAXMIND_USER_ID'),
42+
'license_key' => env('REQUEST_ANALYTICS_MAXMIND_LICENSE_KEY'),
43+
'database_path' => env('REQUEST_ANALYTICS_MAXMIND_DB_PATH', storage_path('app/GeoLite2-City.mmdb')),
44+
],
3745
],
3846

3947
'privacy' => [

src/Services/GeolocationService.php

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace MeShaon\RequestAnalytics\Services;
44

5+
use GeoIp2\Database\Reader;
6+
use GeoIp2\Exception\AddressNotFoundException;
57
use Illuminate\Support\Facades\Cache;
68
use Illuminate\Support\Facades\Http;
79
use Illuminate\Support\Facades\Log;
@@ -119,9 +121,111 @@ protected function lookupWithIpGeolocation(string $ip): array
119121

120122
protected function lookupWithMaxMind(string $ip): array
121123
{
122-
// This would require the GeoIP2 PHP library
123-
// composer require geoip2/geoip2
124-
// Implementation would depend on whether using web service or local database
124+
$maxmindType = config('request-analytics.geolocation.maxmind.type', 'webservice');
125+
126+
return match ($maxmindType) {
127+
'database' => $this->lookupWithMaxMindDatabase($ip),
128+
'webservice' => $this->lookupWithMaxMindWebService($ip),
129+
default => $this->getDefaultLocation(),
130+
};
131+
}
132+
133+
protected function lookupWithMaxMindWebService(string $ip): array
134+
{
135+
$userId = config('request-analytics.geolocation.maxmind.user_id');
136+
$licenseKey = config('request-analytics.geolocation.maxmind.license_key');
137+
138+
if (! $userId || ! $licenseKey) {
139+
Log::warning('MaxMind web service credentials not configured');
140+
141+
return $this->getDefaultLocation();
142+
}
143+
144+
try {
145+
$response = Http::timeout(10)
146+
->withBasicAuth($userId, $licenseKey)
147+
->get("https://geoip.maxmind.com/geoip/v2.1/city/{$ip}");
148+
149+
if ($response->successful()) {
150+
$data = $response->json();
151+
152+
return [
153+
'country' => $data['country']['names']['en'] ?? '',
154+
'country_code' => $data['country']['iso_code'] ?? '',
155+
'region' => $data['subdivisions'][0]['names']['en'] ?? '',
156+
'city' => $data['city']['names']['en'] ?? '',
157+
'latitude' => $data['location']['latitude'] ?? null,
158+
'longitude' => $data['location']['longitude'] ?? null,
159+
'timezone' => $data['location']['time_zone'] ?? '',
160+
'isp' => $data['traits']['isp'] ?? '',
161+
];
162+
}
163+
164+
if ($response->status() === 404) {
165+
// IP not found in database
166+
return $this->getDefaultLocation();
167+
}
168+
169+
Log::warning('MaxMind web service returned error', [
170+
'ip' => $ip,
171+
'status' => $response->status(),
172+
'body' => $response->body(),
173+
]);
174+
175+
} catch (\Exception $e) {
176+
Log::warning('MaxMind web service lookup failed', [
177+
'ip' => $ip,
178+
'error' => $e->getMessage(),
179+
]);
180+
}
181+
182+
return $this->getDefaultLocation();
183+
}
184+
185+
protected function lookupWithMaxMindDatabase(string $ip): array
186+
{
187+
$databasePath = config('request-analytics.geolocation.maxmind.database_path');
188+
189+
if (! $databasePath || ! file_exists($databasePath)) {
190+
Log::warning('MaxMind database file not found', [
191+
'path' => $databasePath,
192+
]);
193+
194+
return $this->getDefaultLocation();
195+
}
196+
197+
// Check if GeoIP2 library is available
198+
if (! class_exists('GeoIp2\Database\Reader')) {
199+
Log::warning('GeoIP2 library not installed. Please run: composer require geoip2/geoip2');
200+
201+
return $this->getDefaultLocation();
202+
}
203+
204+
try {
205+
$reader = new Reader($databasePath);
206+
$record = $reader->city($ip);
207+
208+
return [
209+
'country' => $record->country->name ?? '',
210+
'country_code' => $record->country->isoCode ?? '',
211+
'region' => $record->mostSpecificSubdivision->name ?? '',
212+
'city' => $record->city->name ?? '',
213+
'latitude' => $record->location->latitude,
214+
'longitude' => $record->location->longitude,
215+
'timezone' => $record->location->timeZone ?? '',
216+
'isp' => '', // ISP data requires separate database
217+
];
218+
219+
} catch (AddressNotFoundException) {
220+
// IP not found in database - this is normal for some IPs
221+
return $this->getDefaultLocation();
222+
} catch (\Exception $e) {
223+
Log::warning('MaxMind database lookup failed', [
224+
'ip' => $ip,
225+
'database' => $databasePath,
226+
'error' => $e->getMessage(),
227+
]);
228+
}
125229

126230
return $this->getDefaultLocation();
127231
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Cache;
4+
use Illuminate\Support\Facades\Config;
5+
use Illuminate\Support\Facades\Log;
6+
use MeShaon\RequestAnalytics\Services\GeolocationService;
7+
8+
beforeEach(function (): void {
9+
Cache::flush();
10+
});
11+
12+
it('returns default location for local IPs', function (): void {
13+
$service = new GeolocationService;
14+
15+
$result = $service->lookup('127.0.0.1');
16+
17+
expect($result)->toBe([
18+
'country' => '',
19+
'country_code' => '',
20+
'region' => '',
21+
'city' => '',
22+
'latitude' => null,
23+
'longitude' => null,
24+
'timezone' => '',
25+
'isp' => '',
26+
]);
27+
});
28+
29+
it('detects various local IP ranges', function (): void {
30+
$service = new GeolocationService;
31+
32+
$localIps = [
33+
'127.0.0.1',
34+
'::1',
35+
'192.168.1.1',
36+
'10.0.0.1',
37+
'172.16.0.1',
38+
];
39+
40+
foreach ($localIps as $ip) {
41+
$result = $service->lookup($ip);
42+
expect($result['country'])->toBe('');
43+
}
44+
});
45+
46+
it('handles MaxMind web service configuration validation', function (): void {
47+
Config::set('request-analytics.geolocation.provider', 'maxmind');
48+
Config::set('request-analytics.geolocation.maxmind.type', 'webservice');
49+
Config::set('request-analytics.geolocation.maxmind.user_id', null);
50+
Config::set('request-analytics.geolocation.maxmind.license_key', null);
51+
52+
Log::shouldReceive('warning')
53+
->once()
54+
->with('MaxMind web service credentials not configured');
55+
56+
$service = new GeolocationService;
57+
$result = $service->lookup('8.8.8.8');
58+
59+
expect($result)->toBe([
60+
'country' => '',
61+
'country_code' => '',
62+
'region' => '',
63+
'city' => '',
64+
'latitude' => null,
65+
'longitude' => null,
66+
'timezone' => '',
67+
'isp' => '',
68+
]);
69+
});
70+
71+
it('handles MaxMind database when file does not exist', function (): void {
72+
Config::set('request-analytics.geolocation.provider', 'maxmind');
73+
Config::set('request-analytics.geolocation.maxmind.type', 'database');
74+
Config::set('request-analytics.geolocation.maxmind.database_path', '/nonexistent/path.mmdb');
75+
76+
Log::shouldReceive('warning')
77+
->once()
78+
->with('MaxMind database file not found', ['path' => '/nonexistent/path.mmdb']);
79+
80+
$service = new GeolocationService;
81+
$result = $service->lookup('8.8.8.8');
82+
83+
expect($result)->toBe([
84+
'country' => '',
85+
'country_code' => '',
86+
'region' => '',
87+
'city' => '',
88+
'latitude' => null,
89+
'longitude' => null,
90+
'timezone' => '',
91+
'isp' => '',
92+
]);
93+
});
94+
95+
it('handles MaxMind database when GeoIP2 library is not installed', function (): void {
96+
Config::set('request-analytics.geolocation.provider', 'maxmind');
97+
Config::set('request-analytics.geolocation.maxmind.type', 'database');
98+
99+
// Create a temporary file to simulate database existence
100+
$tempFile = tempnam(sys_get_temp_dir(), 'test_geo');
101+
Config::set('request-analytics.geolocation.maxmind.database_path', $tempFile);
102+
103+
Log::shouldReceive('warning')
104+
->once()
105+
->with('GeoIP2 library not installed. Please run: composer require geoip2/geoip2');
106+
107+
$service = new GeolocationService;
108+
$result = $service->lookup('8.8.8.8');
109+
110+
expect($result)->toBe([
111+
'country' => '',
112+
'country_code' => '',
113+
'region' => '',
114+
'city' => '',
115+
'latitude' => null,
116+
'longitude' => null,
117+
'timezone' => '',
118+
'isp' => '',
119+
]);
120+
121+
unlink($tempFile);
122+
});
123+
124+
it('uses correct MaxMind configuration types', function (): void {
125+
Config::set('request-analytics.geolocation.provider', 'maxmind');
126+
127+
// Test webservice type
128+
Config::set('request-analytics.geolocation.maxmind.type', 'webservice');
129+
Config::set('request-analytics.geolocation.maxmind.user_id', null);
130+
131+
Log::shouldReceive('warning')
132+
->once()
133+
->with('MaxMind web service credentials not configured');
134+
135+
$service = new GeolocationService;
136+
$result = $service->lookup('8.8.8.8');
137+
expect($result['country'])->toBe('');
138+
});
139+
140+
it('handles MaxMind database type configuration', function (): void {
141+
Config::set('request-analytics.geolocation.provider', 'maxmind');
142+
Config::set('request-analytics.geolocation.maxmind.type', 'database');
143+
Config::set('request-analytics.geolocation.maxmind.database_path', '/nonexistent/path.mmdb');
144+
145+
Log::shouldReceive('warning')
146+
->once()
147+
->with('MaxMind database file not found', ['path' => '/nonexistent/path.mmdb']);
148+
149+
$service = new GeolocationService;
150+
$result = $service->lookup('8.8.8.8');
151+
expect($result['country'])->toBe('');
152+
});
153+
154+
it('handles unknown MaxMind type gracefully', function (): void {
155+
Config::set('request-analytics.geolocation.provider', 'maxmind');
156+
Config::set('request-analytics.geolocation.maxmind.type', 'unknown_type');
157+
158+
$service = new GeolocationService;
159+
$result = $service->lookup('8.8.8.8');
160+
161+
expect($result)->toBe([
162+
'country' => '',
163+
'country_code' => '',
164+
'region' => '',
165+
'city' => '',
166+
'latitude' => null,
167+
'longitude' => null,
168+
'timezone' => '',
169+
'isp' => '',
170+
]);
171+
});

0 commit comments

Comments
 (0)