diff --git a/CHANGELOG.md b/CHANGELOG.md index 5500505f0e..a261028768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased -## 3.0.4 (2020-11-6) +- Fix capturing of the request body in the `RequestIntegration` integration (#1139) + +## 3.0.4 (2020-11-06) - Fix stacktrace missing from payload for non-exception events (#1123) - Fix capturing of the request body in the `RequestIntegration` integration when the stream is empty (#1119) diff --git a/composer.json b/composer.json index 772fdb2b3f..7350d1a285 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "ext-json": "*", "ext-mbstring": "*", "guzzlehttp/promises": "^1.3", - "guzzlehttp/psr7": "^1.6", + "guzzlehttp/psr7": "^1.7", "jean85/pretty-package-versions": "^1.5", "ocramius/package-versions": "^1.8", "php-http/async-client-implementation": "^1.0", diff --git a/src/Integration/RequestIntegration.php b/src/Integration/RequestIntegration.php index 10a075fd8a..d9f2cac177 100644 --- a/src/Integration/RequestIntegration.php +++ b/src/Integration/RequestIntegration.php @@ -4,6 +4,7 @@ namespace Sentry\Integration; +use GuzzleHttp\Psr7\Utils; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UploadedFileInterface; use Sentry\Event; @@ -36,6 +37,17 @@ final class RequestIntegration implements IntegrationInterface */ private const REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH = 10 ** 4; + /** + * This constant is a map of maximum allowed sizes for each value of the + * `max_request_body_size` option. + */ + private const MAX_REQUEST_BODY_SIZE_OPTION_TO_MAX_LENGTH_MAP = [ + 'none' => 0, + 'small' => self::REQUEST_BODY_SMALL_MAX_CONTENT_LENGTH, + 'medium' => self::REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH, + 'always' => -1, + ]; + /** * @var RequestFetcherInterface PSR-7 request fetcher */ @@ -155,16 +167,9 @@ static function (string $key) use ($keysToRemove): bool { private function captureRequestBody(Options $options, ServerRequestInterface $request) { $maxRequestBodySize = $options->getMaxRequestBodySize(); + $requestBodySize = (int) $request->getHeaderLine('Content-Length'); - $requestBody = $request->getBody(); - $requestBodySize = $request->getBody()->getSize(); - - if ( - !$requestBodySize || - 'none' === $maxRequestBodySize || - ('small' === $maxRequestBodySize && $requestBodySize > self::REQUEST_BODY_SMALL_MAX_CONTENT_LENGTH) || - ('medium' === $maxRequestBodySize && $requestBodySize > self::REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH) - ) { + if (!$this->isRequestBodySizeWithinReadBounds($requestBodySize, $maxRequestBodySize)) { return null; } @@ -178,15 +183,17 @@ private function captureRequestBody(Options $options, ServerRequestInterface $re return $requestData; } + $requestBody = Utils::copyToString($request->getBody(), self::MAX_REQUEST_BODY_SIZE_OPTION_TO_MAX_LENGTH_MAP[$maxRequestBodySize]); + if ('application/json' === $request->getHeaderLine('Content-Type')) { try { - return JSON::decode($requestBody->getContents()); + return JSON::decode($requestBody); } catch (JsonException $exception) { // Fallback to returning the raw data from the request body } } - return $requestBody->getContents(); + return $requestBody; } /** @@ -217,4 +224,25 @@ private function parseUploadedFiles(array $uploadedFiles): array return $result; } + + private function isRequestBodySizeWithinReadBounds(int $requestBodySize, string $maxRequestBodySize): bool + { + if ($requestBodySize <= 0) { + return false; + } + + if ('none' === $maxRequestBodySize) { + return false; + } + + if ('small' === $maxRequestBodySize && $requestBodySize > self::REQUEST_BODY_SMALL_MAX_CONTENT_LENGTH) { + return false; + } + + if ('medium' === $maxRequestBodySize && $requestBodySize > self::REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH) { + return false; + } + + return true; + } } diff --git a/tests/Integration/RequestIntegrationTest.php b/tests/Integration/RequestIntegrationTest.php index 12c3e48913..e8d2a69f2a 100644 --- a/tests/Integration/RequestIntegrationTest.php +++ b/tests/Integration/RequestIntegrationTest.php @@ -7,10 +7,10 @@ use GuzzleHttp\Psr7\ServerRequest; use GuzzleHttp\Psr7\UploadedFile; use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Psr7\Utils; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\StreamInterface; use Sentry\ClientInterface; use Sentry\Event; use Sentry\Integration\RequestFetcherInterface; @@ -189,16 +189,14 @@ public function invokeDataProvider(): iterable 'max_request_body_size' => 'none', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) - ->withParsedBody([ - 'foo' => 'foo value', - 'bar' => 'bar value', - ]) - ->withBody($this->getStreamMock(1)), + ->withHeader('Content-Length', '3') + ->withBody(Utils::streamFor('foo')), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], + 'Content-Length' => ['3'], ], ], null, @@ -210,21 +208,16 @@ public function invokeDataProvider(): iterable 'max_request_body_size' => 'small', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) - ->withParsedBody([ - 'foo' => 'foo value', - 'bar' => 'bar value', - ]) - ->withBody($this->getStreamMock(10 ** 3)), + ->withHeader('Content-Length', 10 ** 3) + ->withBody(Utils::streamFor('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at placerat est. Donec maximus odio augue, vitae bibendum nisi euismod nec. Nunc vel velit ligula. Ut non ultricies magna, non condimentum turpis. Donec pellentesque id nunc at facilisis. Sed fermentum ultricies nunc, id posuere ex ullamcorper quis. Sed varius tincidunt nulla, id varius nulla interdum sit amet. Pellentesque molestie sapien at mi tristique consequat. Nullam id eleifend arcu. Vivamus sed placerat neque. Ut sapien magna, elementum in euismod pretium, rhoncus vitae augue. Nam ullamcorper dui et tortor semper, eu feugiat elit faucibus. Curabitur vel auctor odio. Phasellus vestibulum ullamcorper dictum. Suspendisse fringilla, ipsum bibendum venenatis vulputate, nunc orci facilisis leo, commodo finibus mi arcu in turpis. Mauris ut ultrices est. Nam quis purus ut nulla interdum ornare. Proin in tellus egestas, commodo magna porta, consequat justo. Vivamus in convallis odio. Pellentesque porttitor, urna non gravida.')), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], + 'Content-Length' => ['1000'], ], - 'data' => [ - 'foo' => 'foo value', - 'bar' => 'bar value', - ], + 'data' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at placerat est. Donec maximus odio augue, vitae bibendum nisi euismod nec. Nunc vel velit ligula. Ut non ultricies magna, non condimentum turpis. Donec pellentesque id nunc at facilisis. Sed fermentum ultricies nunc, id posuere ex ullamcorper quis. Sed varius tincidunt nulla, id varius nulla interdum sit amet. Pellentesque molestie sapien at mi tristique consequat. Nullam id eleifend arcu. Vivamus sed placerat neque. Ut sapien magna, elementum in euismod pretium, rhoncus vitae augue. Nam ullamcorper dui et tortor semper, eu feugiat elit faucibus. Curabitur vel auctor odio. Phasellus vestibulum ullamcorper dictum. Suspendisse fringilla, ipsum bibendum venenatis vulputate, nunc orci facilisis leo, commodo finibus mi arcu in turpis. Mauris ut ultrices est. Nam quis purus ut nulla interdum ornare. Proin in tellus egestas, commodo magna porta, consequat justo. Vivamus in convallis odio. Pellentesque porttitor, urna non gravid', ], null, null, @@ -235,16 +228,21 @@ public function invokeDataProvider(): iterable 'max_request_body_size' => 'small', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) + ->withHeader('Content-Length', (string) (10 ** 3)) ->withParsedBody([ 'foo' => 'foo value', 'bar' => 'bar value', - ]) - ->withBody($this->getStreamMock(10 ** 3 + 1)), + ]), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], + 'Content-Length' => ['1000'], + ], + 'data' => [ + 'foo' => 'foo value', + 'bar' => 'bar value', ], ], null, @@ -253,23 +251,20 @@ public function invokeDataProvider(): iterable yield [ [ - 'max_request_body_size' => 'medium', + 'max_request_body_size' => 'small', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) + ->withHeader('Content-Length', (string) (10 ** 3 + 1)) ->withParsedBody([ 'foo' => 'foo value', 'bar' => 'bar value', - ]) - ->withBody($this->getStreamMock(10 ** 4)), + ]), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], - ], - 'data' => [ - 'foo' => 'foo value', - 'bar' => 'bar value', + 'Content-Length' => ['1001'], ], ], null, @@ -281,16 +276,21 @@ public function invokeDataProvider(): iterable 'max_request_body_size' => 'medium', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) + ->withHeader('Content-Length', (string) (10 ** 4)) ->withParsedBody([ 'foo' => 'foo value', 'bar' => 'bar value', - ]) - ->withBody($this->getStreamMock(10 ** 4 + 1)), + ]), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], + 'Content-Length' => ['10000'], + ], + 'data' => [ + 'foo' => 'foo value', + 'bar' => 'bar value', ], ], null, @@ -299,25 +299,20 @@ public function invokeDataProvider(): iterable yield [ [ - 'max_request_body_size' => 'always', + 'max_request_body_size' => 'medium', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) - ->withUploadedFiles([ - 'foo' => new UploadedFile('foo content', 123, UPLOAD_ERR_OK, 'foo.ext', 'application/text'), - ]) - ->withBody($this->getStreamMock(123 + 321)), + ->withHeader('Content-Length', (string) (10 ** 4 + 1)) + ->withParsedBody([ + 'foo' => 'foo value', + 'bar' => 'bar value', + ]), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], - ], - 'data' => [ - 'foo' => [ - 'client_filename' => 'foo.ext', - 'client_media_type' => 'application/text', - 'size' => 123, - ], + 'Content-Length' => ['10001'], ], ], null, @@ -329,18 +324,19 @@ public function invokeDataProvider(): iterable 'max_request_body_size' => 'always', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) + ->withHeader('Content-Length', '444') ->withUploadedFiles([ 'foo' => [ new UploadedFile('foo content', 123, UPLOAD_ERR_OK, 'foo.ext', 'application/text'), new UploadedFile('bar content', 321, UPLOAD_ERR_OK, 'bar.ext', 'application/octet-stream'), ], - ]) - ->withBody($this->getStreamMock(123 + 321)), + ]), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], + 'Content-Length' => ['444'], ], 'data' => [ 'foo' => [ @@ -361,60 +357,21 @@ public function invokeDataProvider(): iterable null, ]; - yield [ - [ - 'max_request_body_size' => 'always', - ], - (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) - ->withUploadedFiles([ - 'foo' => [ - 'bar' => [ - new UploadedFile('foo content', 123, UPLOAD_ERR_OK, 'foo.ext', 'application/text'), - new UploadedFile('bar content', 321, UPLOAD_ERR_OK, 'bar.ext', 'application/octet-stream'), - ], - ], - ]) - ->withBody($this->getStreamMock(123 + 321)), - [ - 'url' => 'http://www.example.com/foo', - 'method' => 'POST', - 'headers' => [ - 'Host' => ['www.example.com'], - ], - 'data' => [ - 'foo' => [ - 'bar' => [ - [ - 'client_filename' => 'foo.ext', - 'client_media_type' => 'application/text', - 'size' => 123, - ], - [ - 'client_filename' => 'bar.ext', - 'client_media_type' => 'application/octet-stream', - 'size' => 321, - ], - ], - ], - ], - ], - null, - null, - ]; - yield [ [ 'max_request_body_size' => 'always', ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) ->withHeader('Content-Type', 'application/json') - ->withBody($this->getStreamMock(13, '{"foo":"bar"}')), + ->withHeader('Content-Length', '13') + ->withBody(Utils::streamFor('{"foo":"bar"}')), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', 'headers' => [ 'Host' => ['www.example.com'], 'Content-Type' => ['application/json'], + 'Content-Length' => ['13'], ], 'data' => [ 'foo' => 'bar', @@ -430,27 +387,7 @@ public function invokeDataProvider(): iterable ], (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) ->withHeader('Content-Type', 'application/json') - ->withBody($this->getStreamMock(1, '{')), - [ - 'url' => 'http://www.example.com/foo', - 'method' => 'POST', - 'headers' => [ - 'Host' => ['www.example.com'], - 'Content-Type' => ['application/json'], - ], - 'data' => '{', - ], - null, - null, - ]; - - yield [ - [ - 'max_request_body_size' => 'always', - ], - (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) - ->withHeader('Content-Type', 'application/json') - ->withBody($this->getStreamMock(null)), + ->withBody(Utils::streamFor('{"foo":"bar"}')), [ 'url' => 'http://www.example.com/foo', 'method' => 'POST', @@ -462,40 +399,6 @@ public function invokeDataProvider(): iterable null, null, ]; - - yield [ - [ - 'max_request_body_size' => 'always', - ], - (new ServerRequest('POST', new Uri('http://www.example.com/foo'))) - ->withHeader('Content-Type', 'application/json') - ->withBody($this->getStreamMock(0)), - [ - 'url' => 'http://www.example.com/foo', - 'method' => 'POST', - 'headers' => [ - 'Host' => ['www.example.com'], - 'Content-Type' => ['application/json'], - ], - ], - null, - null, - ]; - } - - private function getStreamMock(?int $size, string $content = ''): StreamInterface - { - /** @var MockObject&StreamInterface $stream */ - $stream = $this->createMock(StreamInterface::class); - $stream->expects($this->any()) - ->method('getSize') - ->willReturn($size); - - $stream->expects(null === $size ? $this->never() : $this->any()) - ->method('getContents') - ->willReturn($content); - - return $stream; } private function createRequestFetcher(ServerRequestInterface $request): RequestFetcherInterface