-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #27 : Fix Sentry trap
- Loading branch information
Showing
28 changed files
with
866 additions
and
203 deletions.
There are no files selected for viewing
File renamed without changes.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
POST /api/1/store/ HTTP/1.1 | ||
Host: 127.0.0.1:9912 | ||
User-Agent: sentry-python/1.0 | ||
Content-Type: application/json | ||
X-Sentry-Auth: Sentry sentry_version=7, sentry_key=b70a31b3510c4cf793964a185cfe1fd0, sentry_secret=b7d80b520139450f903720eb7991bf3d, sentry_client=sentry-python/1.0 | ||
Contet-Length: 336 | ||
|
||
{ | ||
"event_id": "fc6d8c0c43fc4630ad850ee518f1b9d0", | ||
"transaction": "my.module.function_name", | ||
"timestamp": "2011-05-02T17:41:36", | ||
"tags": { | ||
"ios_version": "4.0" | ||
}, | ||
"exception": {"values":[{ | ||
"type": "SyntaxError", | ||
"value": "Wattttt!", | ||
"module": "__builtins__" | ||
}]} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Buggregator\Trap\Handler\Http\Middleware; | ||
|
||
use Buggregator\Trap\Handler\Http\Middleware; | ||
use Buggregator\Trap\Handler\Http\Middleware\SentryTrap\EnvelopeParser; | ||
use Buggregator\Trap\Proto\Frame; | ||
use Fiber; | ||
use Nyholm\Psr7\Response; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
/** | ||
* @internal | ||
* @psalm-internal Buggregator\Trap | ||
*/ | ||
final class SentryTrap implements Middleware | ||
{ | ||
private const MAX_BODY_SIZE = 2 * 1024 * 1024; | ||
|
||
public function handle(ServerRequestInterface $request, callable $next): ResponseInterface | ||
{ | ||
try { | ||
// Detect Sentry envelope | ||
if ($request->getHeaderLine('Content-Type') === 'application/x-sentry-envelope' | ||
&& \str_ends_with($request->getUri()->getPath(), '/envelope/') | ||
) { | ||
return $this->processEnvelope($request); | ||
} | ||
|
||
if (\str_ends_with($request->getUri()->getPath(), '/store/') | ||
&& ( | ||
$request->getHeaderLine('X-Buggregator-Event') === 'sentry' | ||
|| $request->hasHeader('X-Sentry-Auth') | ||
|| $request->getUri()->getUserInfo() === 'sentry' | ||
) | ||
) { | ||
return $this->processStore($request); | ||
} | ||
} catch (\JsonException) { | ||
// Reject invalid JSON | ||
return new Response(400); | ||
} catch (\Throwable) { | ||
// Reject invalid request | ||
return new Response(400); | ||
} | ||
|
||
return $next($request); | ||
} | ||
|
||
/** | ||
* @param ServerRequestInterface $request | ||
* @return Response | ||
* @throws \Throwable | ||
*/ | ||
public function processEnvelope(ServerRequestInterface $request): ResponseInterface | ||
{ | ||
$size = $request->getBody()->getSize(); | ||
if ($size === null || $size > self::MAX_BODY_SIZE) { | ||
// Reject too big envelope | ||
return new Response(413); | ||
} | ||
|
||
$request->getBody()->rewind(); | ||
$frame = EnvelopeParser::parse($request->getBody(), $request->getAttribute('begin_at', null)); | ||
Fiber::suspend($frame); | ||
|
||
return new Response(200); | ||
} | ||
|
||
private function processStore(ServerRequestInterface $request): ResponseInterface | ||
{ | ||
$size = $request->getBody()->getSize(); | ||
if ($size === null || $size > self::MAX_BODY_SIZE) { | ||
// Reject too big content | ||
return new Response(413); | ||
} | ||
|
||
$payload = \json_decode((string)$request->getBody(), true, 96, \JSON_THROW_ON_ERROR); | ||
|
||
Fiber::suspend( | ||
new Frame\Sentry\SentryStore( | ||
message: $payload, | ||
time: $request->getAttribute('begin_at', null), | ||
) | ||
); | ||
|
||
return new Response(200); | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
src/Handler/Http/Middleware/SentryTrap/EnvelopeParser.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Buggregator\Trap\Handler\Http\Middleware\SentryTrap; | ||
|
||
use Buggregator\Trap\Proto\Frame\Sentry\EnvelopeItem; | ||
use Buggregator\Trap\Proto\Frame\Sentry\SentryEnvelope; | ||
use Buggregator\Trap\Support\StreamHelper; | ||
use DateTimeImmutable; | ||
use Fiber; | ||
use Psr\Http\Message\StreamInterface; | ||
|
||
/** | ||
* @internal | ||
* @psalm-internal Buggregator\Trap\Handler\Http\Middleware | ||
*/ | ||
final class EnvelopeParser | ||
{ | ||
private const MAX_TEXT_ITEM_SIZE = 1024 * 1024; // 1MB | ||
private const MAX_BINARY_ITEM_SIZE = 100 * 1024 * 1024; // 100MB | ||
|
||
public static function parse( | ||
StreamInterface $stream, | ||
DateTimeImmutable $time = new DateTimeImmutable(), | ||
): SentryEnvelope { | ||
// Parse headers | ||
$headers = \json_decode(self::readLine($stream), true, 4, JSON_THROW_ON_ERROR); | ||
|
||
// Parse items | ||
$items = []; | ||
do { | ||
try { | ||
$items[] = self::parseItem($stream); | ||
} catch (\Throwable) { | ||
break; | ||
} | ||
} while (true); | ||
|
||
return new SentryEnvelope($headers, $items, $time); | ||
} | ||
|
||
/** | ||
* @throws \Throwable | ||
*/ | ||
private static function parseItem(StreamInterface $stream): EnvelopeItem | ||
{ | ||
// Parse item header | ||
$itemHeader = \json_decode(self::readLine($stream), true, 4, JSON_THROW_ON_ERROR); | ||
|
||
$length = isset($itemHeader['length']) ? (int)$itemHeader['length'] : null; | ||
$length >= 0 or throw new \RuntimeException('Invalid item length.'); | ||
|
||
$type = $itemHeader['type'] ?? null; | ||
|
||
if ($length > ($type === 'attachment' ? self::MAX_BINARY_ITEM_SIZE : self::MAX_TEXT_ITEM_SIZE)) { | ||
throw new \RuntimeException('Item is too big.'); | ||
} | ||
|
||
/** @var mixed $itemPayload */ | ||
$itemPayload = match (true) { | ||
// Store attachments as a file stream | ||
$type === 'attachment' => $length === null | ||
? StreamHelper::createFileStream()->write(self::readLine($stream)) | ||
: StreamHelper::createFileStream()->write(self::readBytes($stream, $length)), | ||
|
||
// Text items | ||
default => $length === null | ||
? \json_decode(self::readLine($stream), true, 512, JSON_THROW_ON_ERROR) | ||
: \json_decode(self::readBytes($stream, $length), true, 512, JSON_THROW_ON_ERROR), | ||
}; | ||
|
||
return new EnvelopeItem($itemHeader, $itemPayload); | ||
} | ||
|
||
/** | ||
* @param positive-int $possibleBytes Maximum number of bytes to read. If the read fragment is longer than this | ||
* an exception will be thrown. Default is 10MB | ||
* @throws \Throwable | ||
*/ | ||
private static function readLine(StreamInterface $stream, int $possibleBytes = self::MAX_TEXT_ITEM_SIZE): string | ||
{ | ||
$currentPos = $stream->tell(); | ||
$relOffset = StreamHelper::strpos($stream, "\n"); | ||
$size = $stream->getSize(); | ||
$offset = $relOffset === false ? $size : $currentPos + $relOffset; | ||
|
||
// Validate offset | ||
$offset === null and throw new \RuntimeException('Failed to detect line end.'); | ||
$offset - $currentPos > $possibleBytes and throw new \RuntimeException('Line is too long.'); | ||
$offset === $currentPos and throw new \RuntimeException('End of stream.'); | ||
|
||
$result = self::readBytes($stream, $offset - $currentPos); | ||
$size === $offset or $stream->seek(1, \SEEK_CUR); | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* @param int<0, max> $length | ||
* @throws \Throwable | ||
*/ | ||
private static function readBytes(StreamInterface $stream, int $length): string | ||
{ | ||
if ($length === 0) { | ||
return ''; | ||
} | ||
|
||
$currentPos = $stream->tell(); | ||
$size = $stream->getSize(); | ||
|
||
$size !== null && $size - $currentPos < $length and throw new \RuntimeException('Not enough bytes to read.'); | ||
|
||
/** @var non-empty-string $result */ | ||
$result = ''; | ||
do { | ||
$read = $stream->read($length); | ||
$read === '' and throw new \RuntimeException('Failed to read bytes.'); | ||
|
||
$result .= $read; | ||
$length -= \strlen($read); | ||
if ($length === 0) { | ||
break; | ||
} | ||
|
||
Fiber::suspend(); | ||
} while (true); | ||
|
||
return $result; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.