Migrated to GeoLite2 for IP location resolution

This commit is contained in:
Alejandro Celaya 2018-11-11 12:40:40 +01:00
parent 2c3cbe7146
commit bbe85cde31
6 changed files with 100 additions and 6 deletions

1
.gitignore vendored
View File

@ -5,5 +5,6 @@ composer.phar
vendor/
.env
data/database.sqlite
data/GeoLite2-City.mmdb
docs/swagger-ui
docker-compose.override.yml

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
return [
'geolite2' => [
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
],
];

View File

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
@ -65,7 +65,7 @@ return [
Command\Visit\ProcessVisitsCommand::class => [
Service\VisitService::class,
IpApiLocationResolver::class,
IpLocationResolverInterface::class,
'translator',
],

View File

@ -68,10 +68,10 @@ class ProcessVisitsCommand extends Command
}
$ipAddr = $visit->getRemoteAddr();
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
$io->write(sprintf('%s <fg=blue>%s</>', $this->translator->translate('Processing IP'), $ipAddr));
if ($ipAddr === IpAddress::LOCALHOST) {
$io->writeln(
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
sprintf(' [<comment>%s</comment>]', $this->translator->translate('Ignored localhost address'))
);
continue;
}
@ -85,12 +85,15 @@ class ProcessVisitsCommand extends Command
$this->visitService->saveVisit($visit);
$io->writeln(sprintf(
' (' . $this->translator->translate('Address located at "%s"') . ')',
' [<info>' . $this->translator->translate('Address located at "%s"') . '</info>]',
$location->getCityName()
));
} catch (WrongIpException $e) {
$io->writeln(
sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
sprintf(
' [<fg=red>%s</>]',
$this->translator->translate('An error occurred while locating IP. Skipped')
)
);
if ($io->isVerbose()) {
$this->getApplication()->renderException($e, $output);

View File

@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\Common;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use GeoIp2\Database\Reader;
use GuzzleHttp\Client as GuzzleClient;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
@ -23,6 +24,7 @@ return [
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Filesystem::class => InvokableFactory::class,
Reader::class => ConfigAbstractFactory::class,
Translator::class => Factory\TranslatorFactory::class,
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
@ -33,6 +35,7 @@ return [
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
Service\IpApiLocationResolver::class => ConfigAbstractFactory::class,
Service\GeoLite2LocationResolver::class => ConfigAbstractFactory::class,
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
],
'aliases' => [
@ -42,6 +45,7 @@ return [
'logger' => LoggerInterface::class,
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
Service\IpLocationResolverInterface::class => Service\GeoLite2LocationResolver::class,
],
'abstract_factories' => [
Factory\DottedAccessConfigAbstractFactory::class,
@ -49,9 +53,12 @@ return [
],
ConfigAbstractFactory::class => [
Reader::class => ['config.geolite2.db_location'],
Template\Extension\TranslatorExtension::class => ['translator'],
Middleware\LocaleMiddleware::class => ['translator'],
Service\IpApiLocationResolver::class => ['httpClient'],
Service\GeoLite2LocationResolver::class => [Reader::class],
Service\PreviewGenerator::class => [
Image\ImageBuilder::class,
Filesystem::class,

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Service;
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
use GeoIp2\Model\City;
use MaxMind\Db\Reader\InvalidDatabaseException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
class GeoLite2LocationResolver implements IpLocationResolverInterface
{
/**
* @var Reader
*/
private $geoLiteDbReader;
public function __construct(Reader $geoLiteDbReader)
{
$this->geoLiteDbReader = $geoLiteDbReader;
}
/**
* @param string $ipAddress
* @return array
* @throws WrongIpException
*/
public function resolveIpLocation(string $ipAddress): array
{
try {
$city = $this->geoLiteDbReader->city($ipAddress);
return $this->mapFields($city);
} catch (AddressNotFoundException $e) {
throw WrongIpException::fromIpAddress($ipAddress, $e);
} catch (InvalidDatabaseException $e) {
throw new WrongIpException('Provided GeoLite2 db file is invalid', 0, $e);
}
}
private function mapFields(City $city): array
{
return [
'country_code' => $city->country->isoCode ?? '',
'country_name' => $city->country->name ?? '',
'region_name' => $city->mostSpecificSubdivision->name ?? '',
'city' => $city->city->name ?? '',
'latitude' => (string) $city->location->latitude, // FIXME Cast to string for BC compatibility
'longitude' => (string) $city->location->longitude, // FIXME Cast to string for BC compatibility
'time_zone' => $city->location->timeZone ?? '',
];
}
/**
* Returns the interval in seconds that needs to be waited when the API limit is reached
*
* @return int
*/
public function getApiInterval(): int
{
return 0;
}
/**
* Returns the limit of requests that can be performed to the API in a specific interval, or null if no limit exists
*
* @return int|null
*/
public function getApiLimit(): ?int
{
return null;
}
}