This repository has been archived by the owner on Jan 8, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from chris-die/master
Add `Serato\SwsApp\Slim\Middleware\GeoIpLookup` middleware
- Loading branch information
Showing
5 changed files
with
207 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,4 @@ composer.lock | |
/tests/reports | ||
debug.log | ||
error.log | ||
GeoLite2-City.mmdb |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
<?php | ||
namespace Serato\SwsApp\Slim\Middleware; | ||
|
||
use Slim\Http\Body; | ||
use Slim\Handlers\AbstractHandler; | ||
use Psr\Http\Message\RequestInterface as Request; | ||
use Psr\Http\Message\ResponseInterface as Response; | ||
use GeoIp2\Database\Reader; | ||
use GeoIp2\Model\City; | ||
use Exception; | ||
|
||
/** | ||
* GeoIpLookup Middleware | ||
* | ||
* A middleware that exposes the results of a geo IP lookup to the RequestInterface. | ||
* | ||
* Two attributes are added the RequestInterface: | ||
* | ||
* 1. `ipAddress` The source IP of the request. If the IP address can not be determined | ||
* the value will be NULL. | ||
* 2. `geoIpRecord` A `GeoIp2\Model\City` record of the IP address lookup. | ||
*/ | ||
class GeoIpLookup extends AbstractHandler | ||
{ | ||
const IP_ADDRESS = 'ipAddress'; | ||
const GEOIP_RECORD = 'geoIpRecord'; | ||
|
||
/* @var string */ | ||
private $geoLiteDbPath; | ||
|
||
/** | ||
* Constructs the object | ||
* | ||
* @param string $geoLiteDbPath Path to a GeoLite2 database file | ||
*/ | ||
public function __construct(string $geoLiteDbPath) | ||
{ | ||
$this->geoLiteDbPath = $geoLiteDbPath; | ||
} | ||
|
||
/** | ||
* Invoke the middleware | ||
* | ||
* @param ServerRequestInterface $request The most recent Request object | ||
* @param ResponseInterface $response The most recent Response object | ||
* @param Callable $next The next middleware to call | ||
* | ||
* @return ResponseInterface | ||
*/ | ||
public function __invoke(Request $request, Response $response, callable $next) : Response | ||
{ | ||
$ip = $request->getServerParam('HTTP_X_FORWARDED_FOR', ''); | ||
if ($ip === '') { | ||
$ip = $request->getServerParam('REMOTE_ADDR', ''); | ||
} | ||
|
||
$request = $request | ||
->withAttribute(self::IP_ADDRESS, ($ip === '' ? null : $ip)) | ||
->withAttribute(self::GEOIP_RECORD, $this->getGeoIpCityRecord($ip)); | ||
|
||
return $next($request, $response); | ||
} | ||
|
||
/** | ||
* @param string $ipAddress IP address | ||
* @return City | ||
*/ | ||
private function getGeoIpCityRecord(string $ipAddress): City | ||
{ | ||
$reader = new Reader($this->geoLiteDbPath); | ||
|
||
try { | ||
return $reader->city($ipAddress); | ||
} catch (Exception $e) { | ||
return new City([]); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
<?php | ||
namespace Serato\SwsApp\Test\Slim\Middleware; | ||
|
||
use Serato\SwsApp\Test\TestCase; | ||
use Serato\SwsApp\Slim\Middleware\GeoIpLookup; | ||
use Serato\SwsApp\Slim\Middleware\EmptyWare; | ||
use Serato\Slimulator\EnvironmentBuilder; | ||
use Serato\Slimulator\Request; | ||
use Slim\Http\Response; | ||
|
||
/** | ||
* Unit tests for Serato\SwsApp\Slim\Middleware\GeoIpLookup | ||
* | ||
* Note: | ||
* These tests are tagged into the `geoip-database` group and this group of tests is not | ||
* run by default. These tests require a GeoLite2 database. This database file is 50+ MB in size | ||
* and it doesn't make sense to store this file in the repo. | ||
* | ||
* So, to run these tests: | ||
* | ||
* - Get a GeoLite2 database file named `GeoLite2-City.mmdb` and place it in the root directory of this repo. | ||
* - Run the tests from a terminal with the `--group geoip-database` option. | ||
*/ | ||
class GeoIpLookupTest extends TestCase | ||
{ | ||
/** | ||
* @dataProvider ipAddressAttributeProvider | ||
* @group geoip-database | ||
*/ | ||
public function testNoIpAddress($requestRemoteIp, $requestXForwardedIp, $requestAttributeIp) | ||
{ | ||
$request = $this->getRequestViaMiddleware($requestRemoteIp, $requestXForwardedIp); | ||
|
||
$this->assertEquals( | ||
$requestAttributeIp, | ||
$request->getAttribute(GeoIpLookup::IP_ADDRESS) | ||
); | ||
} | ||
|
||
public function ipAddressAttributeProvider() | ||
{ | ||
return [ | ||
['1.1.1.1', '', '1.1.1.1'], | ||
['1.1.1.1', '2.2.2.2', '2.2.2.2'], | ||
['1.1.1.1', null, '1.1.1.1'], | ||
[null, '1.1.1.1', '1.1.1.1'], | ||
[null, null, null] | ||
]; | ||
} | ||
|
||
/** | ||
* This test is a bit fragile because the source IP addresses could (in theory) result | ||
* in different geo IP records in the future. Somewhat unlikely thought. | ||
* | ||
* @dataProvider geoIpRecordProvider | ||
* @group geoip-database | ||
*/ | ||
public function testGeoIpRecord($requestRemoteIp, $requestXForwardedIp, array $lookupData) | ||
{ | ||
$request = $this->getRequestViaMiddleware($requestRemoteIp, $requestXForwardedIp); | ||
$record = $request->getAttribute(GeoIpLookup::GEOIP_RECORD); | ||
// print_r([ | ||
// $record->city->name, | ||
// $record->postal->code, | ||
// $record->country->name, | ||
// $record->country->isoCode, | ||
// $record->continent->code | ||
// ]); | ||
$this->assertEquals($record->city->name, $lookupData[0]); | ||
$this->assertEquals($record->postal->code, $lookupData[1]); | ||
$this->assertEquals($record->country->name, $lookupData[2]); | ||
$this->assertEquals($record->country->isoCode, $lookupData[3]); | ||
$this->assertEquals($record->continent->code, $lookupData[4]); | ||
} | ||
|
||
public function geoIpRecordProvider() | ||
{ | ||
return [ | ||
['', '', ['', '', '', '', '']], | ||
['', '192.168.1.0', ['', '', '', '', '']], | ||
['203.94.44.199', '', ['', '', 'New Zealand', 'NZ', 'OC']], | ||
['', '64.0.0.0', ['', '', 'United States', 'US', 'NA']], | ||
['', '90.76.106.209', ['Blagnac', '31700', 'France', 'FR','EU']], | ||
['123.125.71.24', '', ['Beijing', '', 'China', 'CN', 'AS']], | ||
['213.205.194.98', '', ['Woking', 'GU22', 'United Kingdom', 'GB', 'EU']], | ||
['67.83.121.56', '', ['Port Washington', '11050', 'United States', 'US', 'NA']] | ||
]; | ||
} | ||
|
||
/** | ||
* Returns a Request instance that has been created by running a mock request | ||
* through the GeoIpLookup middleware. | ||
* | ||
* @param mixed $remoteIp | ||
* @param mixed $xForwardedIp | ||
* @return Request | ||
*/ | ||
private function getRequestViaMiddleware($remoteIp = null, $xForwardedIp = null): Request | ||
{ | ||
$middleware = new GeoIpLookup(realpath(__DIR__ . '/../../../GeoLite2-City.mmdb')); | ||
$emptyMiddleware = new EmptyWare; | ||
|
||
$env = EnvironmentBuilder::create(); | ||
|
||
if ($remoteIp !== null) { | ||
$env = $env->setRemoteIpAddress($remoteIp); | ||
} | ||
if ($xForwardedIp !== null) { | ||
$env = $env->setXForwardedForIpAddress($xForwardedIp); | ||
} | ||
|
||
$response = $middleware( | ||
Request::createFromEnvironmentBuilder($env), | ||
new Response, | ||
$emptyMiddleware | ||
); | ||
|
||
return $emptyMiddleware->getRequestInterface(); | ||
} | ||
} |