mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-23 01:16:23 -06:00
Merge pull request #1199 from acelaya-forks/feature/address-based-tracking
Feature/address based tracking
This commit is contained in:
commit
f49e94052d
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## [Unreleased]
|
||||
## [2.9.0] - 2021-10-10
|
||||
### Added
|
||||
* [#1015](https://github.com/shlinkio/shlink/issues/1015) Shlink now accepts configuration via env vars even when not using docker.
|
||||
|
||||
@ -26,6 +26,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
|
||||
The option is disabled by default, as the payload is backwards incompatible. You will need to adapt your webhooks to treat the `shortUrl` property as optional before enabling this option.
|
||||
|
||||
* [#1104](https://github.com/shlinkio/shlink/issues/1104) Added ability to disable tracking based on IP addresses.
|
||||
|
||||
IP addresses can be provided in the form of fixed addresses, CIDR blocks, or wildcard patterns (192.168.*.*).
|
||||
|
||||
### Changed
|
||||
* [#1142](https://github.com/shlinkio/shlink/issues/1142) Replaced `doctrine/cache` package with `symfony/cache`.
|
||||
* [#1157](https://github.com/shlinkio/shlink/issues/1157) All routes now support CORS, not only rest ones.
|
||||
|
@ -46,11 +46,12 @@
|
||||
"predis/predis": "^1.1",
|
||||
"pugx/shortid-php": "^0.7",
|
||||
"ramsey/uuid": "^3.9",
|
||||
"rlanvin/php-ip": "3.0.0-rc2",
|
||||
"shlinkio/shlink-common": "^4.0",
|
||||
"shlinkio/shlink-config": "^1.2",
|
||||
"shlinkio/shlink-event-dispatcher": "^2.1",
|
||||
"shlinkio/shlink-importer": "^2.3.1",
|
||||
"shlinkio/shlink-installer": "dev-develop#b45a340 as 6.2",
|
||||
"shlinkio/shlink-installer": "^6.2",
|
||||
"shlinkio/shlink-ip-geolocation": "^2.0",
|
||||
"symfony/console": "^5.3",
|
||||
"symfony/filesystem": "^5.3",
|
||||
@ -63,7 +64,7 @@
|
||||
"devster/ubench": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.3.0",
|
||||
"eaglewu/swoole-ide-helper": "dev-master",
|
||||
"infection/infection": "^0.24.0",
|
||||
"infection/infection": "^0.25.0",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpstan/phpstan": "^0.12.94",
|
||||
"phpstan/phpstan-doctrine": "^0.12.42",
|
||||
|
@ -47,6 +47,7 @@ return [
|
||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||
Option\Tracking\DisableTrackingFromConfigOption::class,
|
||||
Option\Tracking\DisableTrackingConfigOption::class,
|
||||
Option\Tracking\DisableIpTrackingConfigOption::class,
|
||||
Option\Tracking\DisableReferrerTrackingConfigOption::class,
|
||||
|
@ -28,6 +28,9 @@ return [
|
||||
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false),
|
||||
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => env('DISABLE_TRACKING_FROM'),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -6,6 +6,10 @@ namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
use Laminas\Stdlib\AbstractOptions;
|
||||
|
||||
use function array_key_exists;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
|
||||
class TrackingOptions extends AbstractOptions
|
||||
{
|
||||
private bool $anonymizeRemoteAddr = true;
|
||||
@ -15,6 +19,7 @@ class TrackingOptions extends AbstractOptions
|
||||
private bool $disableIpTracking = false;
|
||||
private bool $disableReferrerTracking = false;
|
||||
private bool $disableUaTracking = false;
|
||||
private array $disableTrackingFrom = [];
|
||||
|
||||
public function anonymizeRemoteAddr(): bool
|
||||
{
|
||||
@ -41,6 +46,11 @@ class TrackingOptions extends AbstractOptions
|
||||
return $this->disableTrackParam;
|
||||
}
|
||||
|
||||
public function queryHasDisableTrackParam(array $query): bool
|
||||
{
|
||||
return $this->disableTrackParam !== null && array_key_exists($this->disableTrackParam, $query);
|
||||
}
|
||||
|
||||
protected function setDisableTrackParam(?string $disableTrackParam): void
|
||||
{
|
||||
$this->disableTrackParam = $disableTrackParam;
|
||||
@ -85,4 +95,23 @@ class TrackingOptions extends AbstractOptions
|
||||
{
|
||||
$this->disableUaTracking = $disableUaTracking;
|
||||
}
|
||||
|
||||
public function disableTrackingFrom(): array
|
||||
{
|
||||
return $this->disableTrackingFrom;
|
||||
}
|
||||
|
||||
public function hasDisableTrackingFrom(): bool
|
||||
{
|
||||
return ! empty($this->disableTrackingFrom);
|
||||
}
|
||||
|
||||
protected function setDisableTrackingFrom(string|array|null $disableTrackingFrom): void
|
||||
{
|
||||
if (is_array($disableTrackingFrom)) {
|
||||
$this->disableTrackingFrom = $disableTrackingFrom;
|
||||
} else {
|
||||
$this->disableTrackingFrom = $disableTrackingFrom === null ? [] : explode(',', $disableTrackingFrom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,21 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Visit;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use InvalidArgumentException;
|
||||
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
||||
use PhpIP\IP;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||
|
||||
use function array_key_exists;
|
||||
use function explode;
|
||||
use function Functional\map;
|
||||
use function Functional\some;
|
||||
use function implode;
|
||||
use function str_contains;
|
||||
|
||||
class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
|
||||
{
|
||||
@ -37,24 +44,63 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
|
||||
$notFoundType = $request->getAttribute(NotFoundType::class);
|
||||
$visitor = Visitor::fromRequest($request);
|
||||
|
||||
if ($notFoundType?->isBaseUrl()) {
|
||||
$this->visitsTracker->trackBaseUrlVisit($visitor);
|
||||
} elseif ($notFoundType?->isRegularNotFound()) {
|
||||
$this->visitsTracker->trackRegularNotFoundVisit($visitor);
|
||||
} elseif ($notFoundType?->isInvalidShortUrl()) {
|
||||
$this->visitsTracker->trackInvalidShortUrlVisit($visitor);
|
||||
}
|
||||
match (true) { // @phpstan-ignore-line
|
||||
$notFoundType?->isBaseUrl() => $this->visitsTracker->trackBaseUrlVisit($visitor),
|
||||
$notFoundType?->isRegularNotFound() => $this->visitsTracker->trackRegularNotFoundVisit($visitor),
|
||||
$notFoundType?->isInvalidShortUrl() => $this->visitsTracker->trackInvalidShortUrlVisit($visitor),
|
||||
};
|
||||
}
|
||||
|
||||
private function shouldTrackRequest(ServerRequestInterface $request): bool
|
||||
{
|
||||
$query = $request->getQueryParams();
|
||||
$disableTrackParam = $this->trackingOptions->getDisableTrackParam();
|
||||
$forwardedMethod = $request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE);
|
||||
if ($forwardedMethod === self::METHOD_HEAD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $disableTrackParam === null || ! array_key_exists($disableTrackParam, $query);
|
||||
$remoteAddr = $request->getAttribute(IpAddressMiddlewareFactory::REQUEST_ATTR);
|
||||
if ($this->shouldDisableTrackingFromAddress($remoteAddr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = $request->getQueryParams();
|
||||
return ! $this->trackingOptions->queryHasDisableTrackParam($query);
|
||||
}
|
||||
|
||||
private function shouldDisableTrackingFromAddress(?string $remoteAddr): bool
|
||||
{
|
||||
if ($remoteAddr === null || ! $this->trackingOptions->hasDisableTrackingFrom()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$ip = IP::create($remoteAddr);
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$remoteAddrParts = explode('.', $remoteAddr);
|
||||
$disableTrackingFrom = $this->trackingOptions->disableTrackingFrom();
|
||||
|
||||
return some($disableTrackingFrom, function (string $value) use ($ip, $remoteAddrParts): bool {
|
||||
try {
|
||||
return match (true) {
|
||||
str_contains($value, '*') => $ip->matches($this->parseValueWithWildcards($value, $remoteAddrParts)),
|
||||
str_contains($value, '/') => $ip->isIn($value),
|
||||
default => $ip->matches($value),
|
||||
};
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function parseValueWithWildcards(string $value, array $remoteAddrParts): string
|
||||
{
|
||||
// Replace wildcard parts with the corresponding ones from the remote address
|
||||
return implode('.', map(
|
||||
explode('.', $value),
|
||||
fn (string $part, int $index) => $part === '*' ? $remoteAddrParts[$index] : $part,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
@ -37,7 +38,10 @@ class RequestTrackerTest extends TestCase
|
||||
|
||||
$this->requestTracker = new RequestTracker(
|
||||
$this->visitsTracker->reveal(),
|
||||
new TrackingOptions(['disable_track_param' => 'foobar']),
|
||||
new TrackingOptions([
|
||||
'disable_track_param' => 'foobar',
|
||||
'disable_tracking_from' => ['80.90.100.110', '192.168.10.0/24', '1.2.*.*'],
|
||||
]),
|
||||
);
|
||||
|
||||
$this->request = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||
@ -69,6 +73,18 @@ class RequestTrackerTest extends TestCase
|
||||
yield 'disable track param as null' => [
|
||||
ServerRequestFactory::fromGlobals()->withQueryParams(['foobar' => null]),
|
||||
];
|
||||
yield 'exact remote address' => [ServerRequestFactory::fromGlobals()->withAttribute(
|
||||
IpAddressMiddlewareFactory::REQUEST_ATTR,
|
||||
'80.90.100.110',
|
||||
)];
|
||||
yield 'matching wildcard remote address' => [ServerRequestFactory::fromGlobals()->withAttribute(
|
||||
IpAddressMiddlewareFactory::REQUEST_ATTR,
|
||||
'1.2.3.4',
|
||||
)];
|
||||
yield 'matching CIDR block remote address' => [ServerRequestFactory::fromGlobals()->withAttribute(
|
||||
IpAddressMiddlewareFactory::REQUEST_ATTR,
|
||||
'192.168.10.100',
|
||||
)];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
Loading…
Reference in New Issue
Block a user