Skip to content
This repository has been archived by the owner on Jan 8, 2025. It is now read-only.

Commit

Permalink
Merge pull request #35 from chris-die/master
Browse files Browse the repository at this point in the history
Add `Serato\SwsApp\Slim\Middleware\GeoIpLookup` middleware
  • Loading branch information
chris-die authored Jul 25, 2018
2 parents 5580e3c + 52b04ce commit f728611
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ composer.lock
/tests/reports
debug.log
error.log
GeoLite2-City.mmdb
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"aws/aws-sdk-php": "^3.0",
"propel/propel": "2.0.0-alpha7",
"willdurand/negotiation": "~2.2.0",
"symfony/cache": "^3.0"
"symfony/cache": "^3.0",
"geoip2/geoip2": "^2.0.0"
},
"require-dev": {
"phpunit/phpunit": "~6.0",
Expand Down
6 changes: 6 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
</whitelist>
</filter>

<groups>
<exclude>
<group>geoip-database</group>
</exclude>
</groups>

<!--
<logging>
<log type="coverage-clover" target="tests/reports/logs/clover.xml"/>
Expand Down
78 changes: 78 additions & 0 deletions src/Slim/Middleware/GeoIpLookup.php
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([]);
}
}
}
120 changes: 120 additions & 0 deletions tests/Slim/Middleware/GeoIpLookupTest.php
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();
}
}

0 comments on commit f728611

Please sign in to comment.