Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/DataCollection/HttpBodyCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Sentry\DataCollection;

use GuzzleHttp\Psr7\Query;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ServerRequestInterface;
use Sentry\Exception\JsonException;
use Sentry\Util\JSON;
Expand Down Expand Up @@ -37,6 +38,14 @@ public static function collectServerRequest(DataCollectionPolicy $policy, Server
return self::collectSource($policy, HttpMessageType::incomingRequest(), new ServerRequestBodySource($request));
}

/**
* @return array<array-key, mixed>|string|null
*/
public static function collectPsr7Message(DataCollectionPolicy $policy, HttpMessageType $messageType, MessageInterface $message)
{
return self::collectSource($policy, $messageType, new Psr7MessageBodySource($message));
}

/**
* @return array<array-key, mixed>|string|null
*/
Expand Down
39 changes: 39 additions & 0 deletions src/DataCollection/HttpCookieCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Sentry\DataCollection;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

final class HttpCookieCollector
{
private function __construct()
Expand All @@ -29,6 +32,32 @@ public static function collect(DataCollectionPolicy $policy, HttpMessageType $ty
return (new KeyValueDataFilter($dataCollection->getCookies()))->filterKeyValueData($cookies);
}

/**
* @return array<array-key, mixed>|string|null `null` if cookies are not collected, `[Filtered]` if they
* could not be parsed
*/
public static function collectPsr7Request(DataCollectionPolicy $policy, HttpMessageType $type, RequestInterface $request)
{
if (!self::shouldCollect($policy, $type)) {
return null;
}

return self::collectGroupedPairs($policy, $type, HttpCookieParser::parseCookieHeaders($request->getHeader('Cookie')));
}

/**
* @return array<array-key, mixed>|string|null `null` if cookies are not collected, `[Filtered]` if they
* could not be parsed
*/
public static function collectPsr7Response(DataCollectionPolicy $policy, HttpMessageType $type, ResponseInterface $response)
{
if (!self::shouldCollect($policy, $type)) {
return null;
}

return self::collectGroupedPairs($policy, $type, HttpCookieParser::parseSetCookieHeaders($response->getHeader('Set-Cookie')));
}

/**
* @param array<int, array{string, mixed}> $cookies
*
Expand Down Expand Up @@ -78,6 +107,16 @@ public static function collectGroupedPairs(DataCollectionPolicy $policy, HttpMes
return $grouped;
}

private static function shouldCollect(DataCollectionPolicy $policy, HttpMessageType $type): bool
{
$dataCollection = $policy->getDataCollection();
if ($dataCollection === null) {
return self::shouldCollectLegacyCookies($policy, $type);
}

return !$dataCollection->getCookies()->isOff();
}

/**
* The legacy options only collected the cookies of incoming requests, and only with `send_default_pii`.
*/
Expand Down
46 changes: 46 additions & 0 deletions src/DataCollection/Psr7MessageBodySource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Sentry\DataCollection;

use Psr\Http\Message\MessageInterface;

/**
* @internal
*/
final class Psr7MessageBodySource implements HttpBodySourceInterface
{
/**
* @var MessageInterface
*/
private $message;

public function __construct(MessageInterface $message)
{
$this->message = $message;
}

public function getKnownLength(): ?int
{
$length = $this->message->getHeaderLine('Content-Length');

return is_numeric($length) ? (int) $length : null;
}

public function getContentType(): string
{
return $this->message->getHeaderLine('Content-Type');
}

public function read(int $limit): ?string
{
$stream = $this->message->getBody();
$size = $stream->getSize();
if ($limit !== -1 && $size !== null && $size > $limit) {
return null;
}

return Psr7BodyReader::read($stream, $limit);
}
}
77 changes: 77 additions & 0 deletions tests/DataCollection/HttpBodyCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
namespace Sentry\Tests\DataCollection;

use GuzzleHttp\Psr7\NoSeekStream;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Sentry\DataCollection\DataCollectionPolicy;
use Sentry\DataCollection\HttpBodyCollector;
use Sentry\DataCollection\HttpBodySourceInterface;
use Sentry\DataCollection\HttpMessageType;
use Sentry\DataCollection\Psr7MessageBodySource;
use Sentry\Event;
use Sentry\Options;
use Sentry\Serializer\PayloadSerializer;
Expand Down Expand Up @@ -505,6 +509,79 @@ public function disabledServerRequestCollectionProvider(): \Generator
yield 'configured bodies disabled' => [['data_collection' => ['http_bodies' => []]]];
}

public function testPsr7BodyIsCollectedWithoutChangingTheStreamPosition(): void
{
$response = new Response(
200,
['Content-Type' => 'application/json'],
'{"name":"Alice","token":"secret"}'
);
$response->getBody()->seek(4);

$this->assertSame(['name' => 'Alice', 'token' => '[Filtered]'], HttpBodyCollector::collectPsr7Message(
$this->options(),
HttpMessageType::incomingResponse(),
$response
));
$this->assertSame(4, $response->getBody()->tell());
}

/**
* @dataProvider disabledSourceProvider
*
* @param array<string, mixed>|null $options
*/
public function testDisabledPsr7MessagesAreNotAccessed(?array $options, HttpMessageType $messageType): void
{
$policy = DataCollectionPolicy::fromOptions($options === null ? null : new Options($options));
$message = $this->createMock(MessageInterface::class);
$message->expects($this->never())->method('getHeaderLine');
$message->expects($this->never())->method('getBody');

$this->assertNull(HttpBodyCollector::collectPsr7Message($policy, $messageType, $message));
}

public function testDeclaredOversizedPsr7MessagesAreNotRead(): void
{
$message = $this->createMock(MessageInterface::class);
$message->expects($this->once())->method('getHeaderLine')->with('Content-Length')->willReturn('100001');
$message->expects($this->never())->method('getBody');

$this->assertNull(HttpBodyCollector::collectSource($this->options(), HttpMessageType::incomingResponse(), new Psr7MessageBodySource($message)));
}

public function testOversizedPsr7StreamsAreNotRead(): void
{
$stream = $this->createMock(StreamInterface::class);
$stream->method('isReadable')->willReturn(true);
$stream->method('isSeekable')->willReturn(true);
$stream->expects($this->once())->method('getSize')->willReturn(100001);
$stream->expects($this->never())->method('read');
$stream->expects($this->never())->method('rewind');
$message = $this->createMock(MessageInterface::class);
$message->expects($this->once())->method('getHeaderLine')->with('Content-Length')->willReturn('1');
$message->expects($this->once())->method('getBody')->willReturn($stream);

$this->assertNull(HttpBodyCollector::collectSource($this->options(), HttpMessageType::incomingResponse(), new Psr7MessageBodySource($message)));
}

public function testPsr7StreamsWithUnknownSizeUseTheConfiguredReadLimit(): void
{
$stream = $this->createMock(StreamInterface::class);
$stream->method('isReadable')->willReturn(true);
$stream->method('isSeekable')->willReturn(true);
$stream->method('getSize')->willReturn(null);
$stream->method('tell')->willReturn(4);
$stream->method('eof')->willReturn(false);
$stream->expects($this->once())->method('read')->with(1001)->willReturn(str_repeat('a', 1001));
$stream->expects($this->once())->method('seek')->with(4);
$message = $this->createMock(MessageInterface::class);
$message->expects($this->once())->method('getHeaderLine')->with('Content-Length')->willReturn('');
$message->expects($this->once())->method('getBody')->willReturn($stream);

$this->assertNull(HttpBodyCollector::collectSource($this->options(['max_request_body_size' => 'small']), HttpMessageType::outgoingRequest(), new Psr7MessageBodySource($message)));
}

/**
* @param array<string, mixed> $options
*/
Expand Down
67 changes: 67 additions & 0 deletions tests/DataCollection/HttpCookieCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

namespace Sentry\Tests\DataCollection;

use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Sentry\DataCollection\DataCollectionPolicy;
use Sentry\DataCollection\HttpCookieCollector;
use Sentry\DataCollection\HttpMessageType;
Expand All @@ -27,6 +31,69 @@ public function testRequestCookiesAreFiltered(): void
]));
}

public function testPsr7RequestCookiesAreParsedAndFiltered(): void
{
$request = new Request('GET', '/', ['Cookie' => 'theme=dark; session_id=secret; theme=light']);

$this->assertSame([
'theme' => 'light',
'session_id' => '[Filtered]',
], HttpCookieCollector::collectPsr7Request($this->policy(), HttpMessageType::outgoingRequest(), $request));
}

public function testPsr7ResponseCookiesAreParsedAndGrouped(): void
{
$response = new Response(200, ['Set-Cookie' => [
'theme=dark; Path=/',
'theme=light; Path=/settings',
'session_id=secret; HttpOnly',
]]);

$this->assertSame([
'theme' => ['dark', 'light'],
'session_id' => '[Filtered]',
], HttpCookieCollector::collectPsr7Response($this->policy(), HttpMessageType::incomingResponse(), $response));
}

public function testDisabledPsr7RequestCookiesAreNotRead(): void
{
$request = $this->createMock(RequestInterface::class);
$request->expects($this->never())->method('getHeader');

$this->assertNull(HttpCookieCollector::collectPsr7Request($this->policy(['cookies' => ['mode' => 'off']]), HttpMessageType::outgoingRequest(), $request));
}

public function testDisabledPsr7ResponseCookiesAreNotRead(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->expects($this->never())->method('getHeader');

$this->assertNull(HttpCookieCollector::collectPsr7Response($this->policy(['cookies' => ['mode' => 'off']]), HttpMessageType::incomingResponse(), $response));
}

public function testLegacyModeWithPiiCollectsIncomingPsr7RequestCookiesUnfiltered(): void
{
$policy = DataCollectionPolicy::fromOptions(new Options(['send_default_pii' => true]));
$request = new Request('GET', '/', ['Cookie' => 'theme=dark; session_id=secret']);

$this->assertSame(
['theme' => 'dark', 'session_id' => 'secret'],
HttpCookieCollector::collectPsr7Request($policy, HttpMessageType::incomingRequest(), $request)
);
}

public function testLegacyModeDoesNotReadOutgoingPsr7Cookies(): void
{
$policy = DataCollectionPolicy::fromOptions(new Options(['send_default_pii' => true]));
$request = $this->createMock(RequestInterface::class);
$request->expects($this->never())->method('getHeader');
$response = $this->createMock(ResponseInterface::class);
$response->expects($this->never())->method('getHeader');

$this->assertNull(HttpCookieCollector::collectPsr7Request($policy, HttpMessageType::outgoingRequest(), $request));
$this->assertNull(HttpCookieCollector::collectPsr7Response($policy, HttpMessageType::incomingResponse(), $response));
}

public function testDisabledCollectionDoesNotCollectCookies(): void
{
$policy = $this->policy(['cookies' => ['mode' => 'off']]);
Expand Down
Loading