Skip to content

Commit

Permalink
Merge pull request #27 : Fix Sentry trap
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk committed Nov 12, 2023
2 parents bb1db04 + ec9d02f commit 11ea96e
Show file tree
Hide file tree
Showing 28 changed files with 866 additions and 203 deletions.
File renamed without changes.
Binary file added resources/payloads/sentry-store-2.http
Binary file not shown.
20 changes: 20 additions & 0 deletions resources/payloads/sentry-store.http
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__"
}]}
}
52 changes: 0 additions & 52 deletions resources/templates/sentry-store.php

This file was deleted.

1 change: 1 addition & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
new Middleware\Resources(),
new Middleware\DebugPage(),
new Middleware\RayRequestDump(),
new Middleware\SentryTrap(),
], [new Websocket()]),
new Traffic\Dispatcher\Smtp(),
new Traffic\Dispatcher\Monolog(),
Expand Down
4 changes: 3 additions & 1 deletion src/Command/Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ protected function execute(
\usleep(100_000);
$this->mail($output, false);
\usleep(100_000);
$this->sendContent('sentry.http');
$this->sendContent('sentry-store.http'); // Sentry Store very short
$this->sendContent('sentry-store-2.http'); // Sentry Store full
$this->sendContent('sentry-envelope.http'); // Sentry envelope
\usleep(100_000);
$this->sendContent('90275024.png');

Expand Down
46 changes: 38 additions & 8 deletions src/Handler/Http/Handler/Fallback.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Buggregator\Trap\Proto\Frame;
use Buggregator\Trap\Traffic\StreamClient;
use DateTimeInterface;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

Expand All @@ -35,7 +36,7 @@ public function __construct(
$middlewares,
/** @see Middleware::handle() */
'handle',
static fn (): ResponseInterface => new \Nyholm\Psr7\Response(404),
static fn (): ResponseInterface => new Response(404),
ResponseInterface::class,
);
}
Expand All @@ -44,15 +45,44 @@ public function handle(StreamClient $streamClient, ServerRequestInterface $reque
{
$time = $request->getAttribute('begin_at', null);
$time = $time instanceof DateTimeInterface ? $time : new \DateTimeImmutable();
$gotFrame = false;

$response = ($this->pipeline)($request);
HttpEmitter::emit($streamClient, $response);
try {
$fiber = new \Fiber(($this->pipeline)(...));
do {
/** @var mixed $got */
$got = $fiber->isStarted() ? $fiber->resume() : $fiber->start($request);

$streamClient->disconnect();
if ($got instanceof Frame) {
$gotFrame = true;
yield $got;
}

yield new Frame\Http(
$request,
$time,
);
if ($fiber->isTerminated()) {
$response = $fiber->getReturn();
if (!$response instanceof ResponseInterface) {
throw new \RuntimeException('Invalid response type.');
}

break;
}

\Fiber::suspend();
} while (true);

HttpEmitter::emit($streamClient, $response);
} catch (\Throwable) {
// Emit error response
HttpEmitter::emit($streamClient, new Response(500));
} finally {
$streamClient->disconnect();
}

if (!$gotFrame) {
yield new Frame\Http(
$request,
$time,
);
}
}
}
92 changes: 92 additions & 0 deletions src/Handler/Http/Middleware/SentryTrap.php
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 src/Handler/Http/Middleware/SentryTrap/EnvelopeParser.php
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;
}
}
2 changes: 1 addition & 1 deletion src/Info.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class Info
{
public const NAME = 'Buggregator Trap';
public const VERSION = '1.0.4';
public const VERSION = '1.1.0';
public const LOGO_CLI_COLOR = <<<CONSOLE
\e[44;97;1m \e[0m
\e[44;97;1m ▄█▀ ▀█▄ \e[0m
Expand Down
Loading

0 comments on commit 11ea96e

Please sign in to comment.