mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Added option to disable tracking based on IP address patterns
This commit is contained in:
parent
db98d811b0
commit
ed1d886f01
@ -46,6 +46,7 @@
|
|||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
"pugx/shortid-php": "^0.7",
|
"pugx/shortid-php": "^0.7",
|
||||||
"ramsey/uuid": "^3.9",
|
"ramsey/uuid": "^3.9",
|
||||||
|
"rlanvin/php-ip": "3.0.0-rc2",
|
||||||
"shlinkio/shlink-common": "^4.0",
|
"shlinkio/shlink-common": "^4.0",
|
||||||
"shlinkio/shlink-config": "^1.2",
|
"shlinkio/shlink-config": "^1.2",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.1",
|
"shlinkio/shlink-event-dispatcher": "^2.1",
|
||||||
|
@ -28,6 +28,9 @@ return [
|
|||||||
|
|
||||||
// If true, the user agent will not be tracked
|
// If true, the user agent will not be tracked
|
||||||
'disable_ua_tracking' => (bool) env('DISABLE_UA_TRACKING', false),
|
'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 Laminas\Stdlib\AbstractOptions;
|
||||||
|
|
||||||
|
use function array_key_exists;
|
||||||
|
use function explode;
|
||||||
|
use function is_array;
|
||||||
|
|
||||||
class TrackingOptions extends AbstractOptions
|
class TrackingOptions extends AbstractOptions
|
||||||
{
|
{
|
||||||
private bool $anonymizeRemoteAddr = true;
|
private bool $anonymizeRemoteAddr = true;
|
||||||
@ -15,6 +19,7 @@ class TrackingOptions extends AbstractOptions
|
|||||||
private bool $disableIpTracking = false;
|
private bool $disableIpTracking = false;
|
||||||
private bool $disableReferrerTracking = false;
|
private bool $disableReferrerTracking = false;
|
||||||
private bool $disableUaTracking = false;
|
private bool $disableUaTracking = false;
|
||||||
|
private array $disableTrackingFrom = [];
|
||||||
|
|
||||||
public function anonymizeRemoteAddr(): bool
|
public function anonymizeRemoteAddr(): bool
|
||||||
{
|
{
|
||||||
@ -41,6 +46,11 @@ class TrackingOptions extends AbstractOptions
|
|||||||
return $this->disableTrackParam;
|
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
|
protected function setDisableTrackParam(?string $disableTrackParam): void
|
||||||
{
|
{
|
||||||
$this->disableTrackParam = $disableTrackParam;
|
$this->disableTrackParam = $disableTrackParam;
|
||||||
@ -85,4 +95,23 @@ class TrackingOptions extends AbstractOptions
|
|||||||
{
|
{
|
||||||
$this->disableUaTracking = $disableUaTracking;
|
$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;
|
namespace Shlinkio\Shlink\Core\Visit;
|
||||||
|
|
||||||
use Fig\Http\Message\RequestMethodInterface;
|
use Fig\Http\Message\RequestMethodInterface;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
||||||
|
use PhpIP\IP;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
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
|
class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
|
||||||
{
|
{
|
||||||
@ -37,24 +44,63 @@ class RequestTracker implements RequestTrackerInterface, RequestMethodInterface
|
|||||||
$notFoundType = $request->getAttribute(NotFoundType::class);
|
$notFoundType = $request->getAttribute(NotFoundType::class);
|
||||||
$visitor = Visitor::fromRequest($request);
|
$visitor = Visitor::fromRequest($request);
|
||||||
|
|
||||||
if ($notFoundType?->isBaseUrl()) {
|
match (true) { // @phpstan-ignore-line
|
||||||
$this->visitsTracker->trackBaseUrlVisit($visitor);
|
$notFoundType?->isBaseUrl() => $this->visitsTracker->trackBaseUrlVisit($visitor),
|
||||||
} elseif ($notFoundType?->isRegularNotFound()) {
|
$notFoundType?->isRegularNotFound() => $this->visitsTracker->trackRegularNotFoundVisit($visitor),
|
||||||
$this->visitsTracker->trackRegularNotFoundVisit($visitor);
|
$notFoundType?->isInvalidShortUrl() => $this->visitsTracker->trackInvalidShortUrlVisit($visitor),
|
||||||
} elseif ($notFoundType?->isInvalidShortUrl()) {
|
};
|
||||||
$this->visitsTracker->trackInvalidShortUrlVisit($visitor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function shouldTrackRequest(ServerRequestInterface $request): bool
|
private function shouldTrackRequest(ServerRequestInterface $request): bool
|
||||||
{
|
{
|
||||||
$query = $request->getQueryParams();
|
|
||||||
$disableTrackParam = $this->trackingOptions->getDisableTrackParam();
|
|
||||||
$forwardedMethod = $request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE);
|
$forwardedMethod = $request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE);
|
||||||
if ($forwardedMethod === self::METHOD_HEAD) {
|
if ($forwardedMethod === self::METHOD_HEAD) {
|
||||||
return false;
|
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\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
@ -37,7 +38,10 @@ class RequestTrackerTest extends TestCase
|
|||||||
|
|
||||||
$this->requestTracker = new RequestTracker(
|
$this->requestTracker = new RequestTracker(
|
||||||
$this->visitsTracker->reveal(),
|
$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(
|
$this->request = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||||
@ -69,6 +73,18 @@ class RequestTrackerTest extends TestCase
|
|||||||
yield 'disable track param as null' => [
|
yield 'disable track param as null' => [
|
||||||
ServerRequestFactory::fromGlobals()->withQueryParams(['foobar' => 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 */
|
/** @test */
|
||||||
|
Loading…
Reference in New Issue
Block a user