mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Merge pull request #238 from acelaya/feature/fix-ip-address
Feature/fix ip address
This commit is contained in:
commit
162d0560db
@ -9,10 +9,15 @@ branches:
|
|||||||
php:
|
php:
|
||||||
- 7.1
|
- 7.1
|
||||||
- 7.2
|
- 7.2
|
||||||
|
- 7.3
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- php: 7.3
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- phpenv config-add data/infra/travis-php/memcached.ini
|
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||||
- phpenv config-add data/infra/travis-php/apcu.ini
|
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- composer self-update
|
- composer self-update
|
||||||
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -1,5 +1,35 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
* [#233](https://github.com/shlinkio/shlink/issues/233) Added PHP 7.3 to build matrix allowing its failure.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Deprecated
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
* [#237](https://github.com/shlinkio/shlink/issues/233) Solved errors when trying to geo-locate `null` IP addresses.
|
||||||
|
|
||||||
|
Also improved how visitor IP addresses are discovered, thanks to [akrabat/ip-address-middleware](https://github.com/akrabat/ip-address-middleware) package.
|
||||||
|
|
||||||
|
|
||||||
## 1.13.1 - 2018-10-16
|
## 1.13.1 - 2018-10-16
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"acelaya/ze-content-based-error-handler": "^2.2",
|
"acelaya/ze-content-based-error-handler": "^2.2",
|
||||||
|
"akrabat/ip-address-middleware": "^1.0",
|
||||||
"cakephp/chronos": "^1.2",
|
"cakephp/chronos": "^1.2",
|
||||||
"cocur/slugify": "^3.0",
|
"cocur/slugify": "^3.0",
|
||||||
"doctrine/cache": "^1.6",
|
"doctrine/cache": "^1.6",
|
||||||
|
@ -1 +0,0 @@
|
|||||||
extension="apcu.so"
|
|
@ -1 +0,0 @@
|
|||||||
extension="memcached.so"
|
|
@ -59,6 +59,14 @@ class ProcessVisitsCommand extends Command
|
|||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ($visits as $visit) {
|
foreach ($visits as $visit) {
|
||||||
|
if (! $visit->hasRemoteAddr()) {
|
||||||
|
$io->writeln(
|
||||||
|
sprintf('<comment>%s</comment>', $this->translator->translate('Ignored visit with no IP address')),
|
||||||
|
OutputInterface::VERBOSITY_VERBOSE
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$ipAddr = $visit->getRemoteAddr();
|
$ipAddr = $visit->getRemoteAddr();
|
||||||
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||||
|
@ -11,8 +11,11 @@ use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
|||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use function count;
|
||||||
|
use function round;
|
||||||
|
|
||||||
class ProcessVisitsCommandTest extends TestCase
|
class ProcessVisitsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -67,15 +70,15 @@ class ProcessVisitsCommandTest extends TestCase
|
|||||||
'command' => 'visit:process',
|
'command' => 'visit:process',
|
||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertEquals(0, \strpos($output, 'Processing IP 1.2.3.0'));
|
$this->assertContains('Processing IP 1.2.3.0', $output);
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'Processing IP 4.3.2.0'));
|
$this->assertContains('Processing IP 4.3.2.0', $output);
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'Processing IP 12.34.56.0'));
|
$this->assertContains('Processing IP 12.34.56.0', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function localhostAddressIsIgnored()
|
public function localhostAndEmptyAddressIsIgnored()
|
||||||
{
|
{
|
||||||
$visits = [
|
$visits = [
|
||||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
(new Visit())->setRemoteAddr('1.2.3.4'),
|
||||||
@ -83,19 +86,22 @@ class ProcessVisitsCommandTest extends TestCase
|
|||||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
(new Visit())->setRemoteAddr('12.34.56.78'),
|
||||||
(new Visit())->setRemoteAddr('127.0.0.1'),
|
(new Visit())->setRemoteAddr('127.0.0.1'),
|
||||||
(new Visit())->setRemoteAddr('127.0.0.1'),
|
(new Visit())->setRemoteAddr('127.0.0.1'),
|
||||||
|
(new Visit())->setRemoteAddr(''),
|
||||||
|
(new Visit())->setRemoteAddr(null),
|
||||||
];
|
];
|
||||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(\count($visits) - 2);
|
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 4);
|
||||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||||
->shouldBeCalledTimes(\count($visits) - 2);
|
->shouldBeCalledTimes(count($visits) - 4);
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'visit:process',
|
'command' => 'visit:process',
|
||||||
]);
|
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'Ignored localhost address'));
|
$this->assertContains('Ignored localhost address', $output);
|
||||||
|
$this->assertContains('Ignored visit with no IP address', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -130,8 +136,8 @@ class ProcessVisitsCommandTest extends TestCase
|
|||||||
'command' => 'visit:process',
|
'command' => 'visit:process',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$getApiLimit->shouldHaveBeenCalledTimes(\count($visits));
|
$getApiLimit->shouldHaveBeenCalledTimes(count($visits));
|
||||||
$getApiInterval->shouldHaveBeenCalledTimes(\round(\count($visits) / $apiLimit));
|
$getApiInterval->shouldHaveBeenCalledTimes(round(count($visits) / $apiLimit));
|
||||||
$resolveIpLocation->shouldHaveBeenCalledTimes(\count($visits));
|
$resolveIpLocation->shouldHaveBeenCalledTimes(count($visits));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common;
|
||||||
|
|
||||||
use Doctrine\Common\Cache\Cache;
|
use Doctrine\Common\Cache\Cache;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use GuzzleHttp\Client as GuzzleClient;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Common\Factory;
|
use RKA\Middleware\IpAddress;
|
||||||
use Shlinkio\Shlink\Common\Image;
|
|
||||||
use Shlinkio\Shlink\Common\Image\ImageBuilder;
|
|
||||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
|
||||||
use Shlinkio\Shlink\Common\Service;
|
|
||||||
use Shlinkio\Shlink\Common\Template\Extension\TranslatorExtension;
|
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
@ -21,14 +19,16 @@ return [
|
|||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
EntityManager::class => Factory\EntityManagerFactory::class,
|
EntityManager::class => Factory\EntityManagerFactory::class,
|
||||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
GuzzleClient::class => InvokableFactory::class,
|
||||||
Cache::class => Factory\CacheFactory::class,
|
Cache::class => Factory\CacheFactory::class,
|
||||||
'Logger_Shlink' => Factory\LoggerFactory::class,
|
'Logger_Shlink' => Factory\LoggerFactory::class,
|
||||||
Filesystem::class => InvokableFactory::class,
|
Filesystem::class => InvokableFactory::class,
|
||||||
|
|
||||||
Translator::class => Factory\TranslatorFactory::class,
|
Translator::class => Factory\TranslatorFactory::class,
|
||||||
TranslatorExtension::class => ConfigAbstractFactory::class,
|
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
|
||||||
LocaleMiddleware::class => ConfigAbstractFactory::class,
|
|
||||||
|
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
|
||||||
|
IpAddress::class => Middleware\IpAddressMiddlewareFactory::class,
|
||||||
|
|
||||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ return [
|
|||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
'em' => EntityManager::class,
|
'em' => EntityManager::class,
|
||||||
'httpClient' => GuzzleHttp\Client::class,
|
'httpClient' => GuzzleClient::class,
|
||||||
'translator' => Translator::class,
|
'translator' => Translator::class,
|
||||||
'logger' => LoggerInterface::class,
|
'logger' => LoggerInterface::class,
|
||||||
Logger::class => 'Logger_Shlink',
|
Logger::class => 'Logger_Shlink',
|
||||||
@ -49,11 +49,11 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
TranslatorExtension::class => ['translator'],
|
Template\Extension\TranslatorExtension::class => ['translator'],
|
||||||
LocaleMiddleware::class => ['translator'],
|
Middleware\LocaleMiddleware::class => ['translator'],
|
||||||
Service\IpApiLocationResolver::class => ['httpClient'],
|
Service\IpApiLocationResolver::class => ['httpClient'],
|
||||||
Service\PreviewGenerator::class => [
|
Service\PreviewGenerator::class => [
|
||||||
ImageBuilder::class,
|
Image\ImageBuilder::class,
|
||||||
Filesystem::class,
|
Filesystem::class,
|
||||||
'config.preview_generation.files_location',
|
'config.preview_generation.files_location',
|
||||||
],
|
],
|
||||||
|
28
module/Common/src/Middleware/IpAddressMiddlewareFactory.php
Normal file
28
module/Common/src/Middleware/IpAddressMiddlewareFactory.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\Middleware;
|
||||||
|
|
||||||
|
use Interop\Container\ContainerInterface;
|
||||||
|
use RKA\Middleware\IpAddress;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
|
||||||
|
class IpAddressMiddlewareFactory implements FactoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an object
|
||||||
|
*
|
||||||
|
* @param ContainerInterface $container
|
||||||
|
* @param string $requestedName
|
||||||
|
* @param null|array $options
|
||||||
|
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||||
|
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||||
|
*/
|
||||||
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): IpAddress
|
||||||
|
{
|
||||||
|
return new IpAddress(true, [], Visitor::REMOTE_ADDRESS_ATTR);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,10 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Common\Util;
|
namespace Shlinkio\Shlink\Common\Util;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
|
use function count;
|
||||||
|
use function explode;
|
||||||
|
use function implode;
|
||||||
|
use function trim;
|
||||||
|
|
||||||
final class IpAddress
|
final class IpAddress
|
||||||
{
|
{
|
||||||
@ -43,9 +47,9 @@ final class IpAddress
|
|||||||
*/
|
*/
|
||||||
public static function fromString(string $address): self
|
public static function fromString(string $address): self
|
||||||
{
|
{
|
||||||
$address = \trim($address);
|
$address = trim($address);
|
||||||
$parts = \explode('.', $address);
|
$parts = explode('.', $address);
|
||||||
if (\count($parts) !== self::IPV4_PARTS_COUNT) {
|
if (count($parts) !== self::IPV4_PARTS_COUNT) {
|
||||||
throw WrongIpException::fromIpAddress($address);
|
throw WrongIpException::fromIpAddress($address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +68,7 @@ final class IpAddress
|
|||||||
|
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return \implode('.', [
|
return implode('.', [
|
||||||
$this->firstOctet,
|
$this->firstOctet,
|
||||||
$this->secondOctet,
|
$this->secondOctet,
|
||||||
$this->thirdOctet,
|
$this->thirdOctet,
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\Common\Middleware;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ReflectionObject;
|
||||||
|
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
|
class IpAddressMiddlewareFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
private $factory;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->factory = new IpAddressMiddlewareFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function returnedInstanceIsProperlyConfigured()
|
||||||
|
{
|
||||||
|
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||||
|
|
||||||
|
$ref = new ReflectionObject($instance);
|
||||||
|
$checkProxyHeaders = $ref->getProperty('checkProxyHeaders');
|
||||||
|
$checkProxyHeaders->setAccessible(true);
|
||||||
|
$trustedProxies = $ref->getProperty('trustedProxies');
|
||||||
|
$trustedProxies->setAccessible(true);
|
||||||
|
$attributeName = $ref->getProperty('attributeName');
|
||||||
|
$attributeName->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertTrue($checkProxyHeaders->getValue($instance));
|
||||||
|
$this->assertEquals([], $trustedProxies->getValue($instance));
|
||||||
|
$this->assertEquals(Visitor::REMOTE_ADDRESS_ATTR, $attributeName->getValue($instance));
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use RKA\Middleware\IpAddress;
|
||||||
use Shlinkio\Shlink\Core\Action;
|
use Shlinkio\Shlink\Core\Action;
|
||||||
use Shlinkio\Shlink\Core\Middleware;
|
use Shlinkio\Shlink\Core\Middleware;
|
||||||
|
|
||||||
@ -10,13 +11,19 @@ return [
|
|||||||
[
|
[
|
||||||
'name' => 'long-url-redirect',
|
'name' => 'long-url-redirect',
|
||||||
'path' => '/{shortCode}',
|
'path' => '/{shortCode}',
|
||||||
'middleware' => Action\RedirectAction::class,
|
'middleware' => [
|
||||||
|
IpAddress::class,
|
||||||
|
Action\RedirectAction::class,
|
||||||
|
],
|
||||||
'allowed_methods' => ['GET'],
|
'allowed_methods' => ['GET'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'pixel-tracking',
|
'name' => 'pixel-tracking',
|
||||||
'path' => '/{shortCode}/track',
|
'path' => '/{shortCode}/track',
|
||||||
'middleware' => Action\PixelAction::class,
|
'middleware' => [
|
||||||
|
IpAddress::class,
|
||||||
|
Action\PixelAction::class,
|
||||||
|
],
|
||||||
'allowed_methods' => ['GET'],
|
'allowed_methods' => ['GET'],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -12,6 +12,7 @@ use Psr\Log\NullLogger;
|
|||||||
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
|
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
|
||||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||||
@ -69,7 +70,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
|
|||||||
|
|
||||||
// Track visit to this short code
|
// Track visit to this short code
|
||||||
if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) {
|
if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) {
|
||||||
$this->visitTracker->track($shortCode, $request);
|
$this->visitTracker->track($shortCode, Visitor::fromRequest($request));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->createResp($url->getLongUrl());
|
return $this->createResp($url->getLongUrl());
|
||||||
|
@ -102,6 +102,11 @@ class Visit extends AbstractEntity implements \JsonSerializable
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasRemoteAddr(): bool
|
||||||
|
{
|
||||||
|
return ! empty($this->remoteAddr);
|
||||||
|
}
|
||||||
|
|
||||||
private function obfuscateAddress(?string $address): ?string
|
private function obfuscateAddress(?string $address): ?string
|
||||||
{
|
{
|
||||||
// Localhost addresses do not need to be obfuscated
|
// Localhost addresses do not need to be obfuscated
|
||||||
|
60
module/Core/src/Model/Visitor.php
Normal file
60
module/Core/src/Model/Visitor.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Model;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
final class Visitor
|
||||||
|
{
|
||||||
|
public const REMOTE_ADDRESS_ATTR = 'remote_address';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $userAgent;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $referer;
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
private $remoteAddress;
|
||||||
|
|
||||||
|
public function __construct(string $userAgent, string $referer, ?string $remoteAddress)
|
||||||
|
{
|
||||||
|
$this->userAgent = $userAgent;
|
||||||
|
$this->referer = $referer;
|
||||||
|
$this->remoteAddress = $remoteAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromRequest(ServerRequestInterface $request): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$request->getHeaderLine('User-Agent'),
|
||||||
|
$request->getHeaderLine('Referer'),
|
||||||
|
$request->getAttribute(self::REMOTE_ADDRESS_ATTR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function emptyInstance(): self
|
||||||
|
{
|
||||||
|
return new self('', '', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserAgent(): string
|
||||||
|
{
|
||||||
|
return $this->userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReferer(): string
|
||||||
|
{
|
||||||
|
return $this->referer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRemoteAddress(): ?string
|
||||||
|
{
|
||||||
|
return $this->remoteAddress;
|
||||||
|
}
|
||||||
|
}
|
@ -4,11 +4,11 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Service;
|
namespace Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
use Doctrine\ORM;
|
use Doctrine\ORM;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||||
|
|
||||||
class VisitsTracker implements VisitsTrackerInterface
|
class VisitsTracker implements VisitsTrackerInterface
|
||||||
@ -24,14 +24,9 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks a new visit to provided short code, using an array of data to look up information
|
* Tracks a new visit to provided short code from provided visitor
|
||||||
*
|
|
||||||
* @param string $shortCode
|
|
||||||
* @param ServerRequestInterface $request
|
|
||||||
* @throws ORM\ORMInvalidArgumentException
|
|
||||||
* @throws ORM\OptimisticLockException
|
|
||||||
*/
|
*/
|
||||||
public function track($shortCode, ServerRequestInterface $request): void
|
public function track(string $shortCode, Visitor $visitor): void
|
||||||
{
|
{
|
||||||
/** @var ShortUrl $shortUrl */
|
/** @var ShortUrl $shortUrl */
|
||||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||||
@ -40,9 +35,9 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||||||
|
|
||||||
$visit = new Visit();
|
$visit = new Visit();
|
||||||
$visit->setShortUrl($shortUrl)
|
$visit->setShortUrl($shortUrl)
|
||||||
->setUserAgent($request->getHeaderLine('User-Agent'))
|
->setUserAgent($visitor->getUserAgent())
|
||||||
->setReferer($request->getHeaderLine('Referer'))
|
->setReferer($visitor->getReferer())
|
||||||
->setRemoteAddr($this->findOutRemoteAddr($request));
|
->setRemoteAddr($visitor->getRemoteAddress());
|
||||||
|
|
||||||
/** @var ORM\EntityManager $em */
|
/** @var ORM\EntityManager $em */
|
||||||
$em = $this->em;
|
$em = $this->em;
|
||||||
@ -50,21 +45,6 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||||||
$em->flush($visit);
|
$em->flush($visit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ServerRequestInterface $request
|
|
||||||
*/
|
|
||||||
private function findOutRemoteAddr(ServerRequestInterface $request): ?string
|
|
||||||
{
|
|
||||||
$forwardedFor = $request->getHeaderLine('X-Forwarded-For');
|
|
||||||
if (empty($forwardedFor)) {
|
|
||||||
$serverParams = $request->getServerParams();
|
|
||||||
return $serverParams['REMOTE_ADDR'] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ips = \explode(',', $forwardedFor);
|
|
||||||
return $ips[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the visits on certain short code
|
* Returns the visits on certain short code
|
||||||
*
|
*
|
||||||
|
@ -3,20 +3,17 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Service;
|
namespace Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
|
||||||
interface VisitsTrackerInterface
|
interface VisitsTrackerInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Tracks a new visit to provided short code, using an array of data to look up information
|
* Tracks a new visit to provided short code from provided visitor
|
||||||
*
|
|
||||||
* @param string $shortCode
|
|
||||||
* @param ServerRequestInterface $request
|
|
||||||
*/
|
*/
|
||||||
public function track($shortCode, ServerRequestInterface $request): void;
|
public function track(string $shortCode, Visitor $visitor): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the visits on certain short code
|
* Returns the visits on certain short code
|
||||||
|
@ -10,9 +10,9 @@ use Prophecy\Argument;
|
|||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
||||||
use Zend\Diactoros\ServerRequestFactory;
|
|
||||||
|
|
||||||
class VisitsTrackerTest extends TestCase
|
class VisitsTrackerTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -44,13 +44,13 @@ class VisitsTrackerTest extends TestCase
|
|||||||
$this->em->persist(Argument::any())->shouldBeCalledTimes(1);
|
$this->em->persist(Argument::any())->shouldBeCalledTimes(1);
|
||||||
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
|
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$this->visitsTracker->track($shortCode, ServerRequestFactory::fromGlobals());
|
$this->visitsTracker->track($shortCode, Visitor::emptyInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function trackUsesForwardedForHeaderIfPresent()
|
public function trackedIpAddressGetsObfuscated()
|
||||||
{
|
{
|
||||||
$shortCode = '123ABC';
|
$shortCode = '123ABC';
|
||||||
$test = $this;
|
$test = $this;
|
||||||
@ -65,9 +65,7 @@ class VisitsTrackerTest extends TestCase
|
|||||||
})->shouldBeCalledTimes(1);
|
})->shouldBeCalledTimes(1);
|
||||||
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
|
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$this->visitsTracker->track($shortCode, ServerRequestFactory::fromGlobals(
|
$this->visitsTracker->track($shortCode, new Visitor('', '', '4.3.2.1'));
|
||||||
['REMOTE_ADDR' => '1.2.3.4']
|
|
||||||
)->withHeader('X-Forwarded-For', '4.3.2.1,99.99.99.99'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user