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
34 changes: 32 additions & 2 deletions src/Cache/Stores/FileStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@

use Closure;
use Phenix\Cache\CacheStore;
use Phenix\Crypto\Exceptions\MissingKeyException;
use Phenix\Facades\Config;
use Phenix\Facades\File;
use Phenix\Util\Arr;
use Phenix\Util\Date;

use function hash_equals;
use function is_array;

class FileStore extends CacheStore
Expand All @@ -31,7 +34,7 @@ public function get(string $key, Closure|null $callback = null): mixed

$data = json_decode($raw, true);

if (! is_array($data) || ! Arr::has($data, ['expires_at', 'value'])) {
if (! is_array($data) || ! Arr::has($data, ['expires_at', 'value', 'mac']) || ! $this->hasValidMac($data)) {
$this->delete($key);

return $this->resolveCallback($key, $callback);
Expand All @@ -58,6 +61,8 @@ public function set(string $key, mixed $value, Date|null $ttl = null): void
'value' => base64_encode(serialize($value)),
];

$payload['mac'] = $this->mac($payload['value'], $expiresAt);

File::put($this->filename($key), json_encode($payload, JSON_THROW_ON_ERROR));
}

Expand All @@ -68,6 +73,8 @@ public function forever(string $key, mixed $value): void
'value' => base64_encode(serialize($value)),
];

$payload['mac'] = $this->mac($payload['value'], null);

File::put($this->filename($key), json_encode($payload, JSON_THROW_ON_ERROR));
}

Expand All @@ -81,7 +88,7 @@ public function has(string $key): bool

$data = json_decode($raw, true);

if (! is_array($data)) {
if (! is_array($data) || ! Arr::has($data, ['expires_at', 'value', 'mac']) || ! $this->hasValidMac($data)) {
return false;
}

Expand Down Expand Up @@ -121,6 +128,29 @@ protected function filename(string $key): string
return $this->path . DIRECTORY_SEPARATOR . sha1($this->prefix . $key) . '.cache';
}

protected function hasValidMac(array $data): bool
{
return hash_equals(
(string) $data['mac'],
$this->mac((string) $data['value'], $data['expires_at'])
);
}

protected function mac(string $value, int|string|null $expiresAt): string
{
$key = Config::get('app.key');

if (empty($key)) {
throw new MissingKeyException('The application key is not set.');
}

return hash_hmac(
'sha256',
"{$value}|" . ($expiresAt ?? 'forever'),
(string) $key
);
}

protected function resolveCallback(string $key, Closure|null $callback): mixed
{
if ($callback === null) {
Expand Down
9 changes: 9 additions & 0 deletions src/Http/ExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Amp\Http\HttpStatus;
use Amp\Http\Server\ExceptionHandler as ExceptionHandlerContract;
use Amp\Http\Server\HttpErrorException;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Throwable;
Expand All @@ -25,6 +26,14 @@ public function handleException(Request $request, Throwable $exception): Respons
'client' => $request->getClient()->getRemoteAddress()->toString(),
]);

if ($exception instanceof HttpErrorException) {
return $this->errorHandler->handleError(
status: $exception->getStatus(),
reason: $exception->getReason(),
request: $request
);
}

if (! $this->errorHandler->shouldExposeDebugDetails()) {
return $this->errorHandler->handleError(status: HttpStatus::INTERNAL_SERVER_ERROR, request: $request);
}
Expand Down
59 changes: 35 additions & 24 deletions src/Http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use Amp\Http\Server\RequestBody;
use Amp\Http\Server\Router;
use Amp\Http\Server\Session\Session as ServerSession;
use Amp\Http\Server\Trailers;
use League\Uri\Components\Query;
use Phenix\Contracts\Arrayable;
use Phenix\Http\Constants\ContentType;
Expand All @@ -20,6 +19,7 @@
use Phenix\Http\Requests\Concerns\HasCookies;
use Phenix\Http\Requests\Concerns\HasHeaders;
use Phenix\Http\Requests\Concerns\HasQueryParameters;
use Phenix\Http\Requests\Concerns\HasTrailers;
use Phenix\Http\Requests\Concerns\HasUser;
use Phenix\Http\Requests\FormParser;
use Phenix\Http\Requests\JsonParser;
Expand All @@ -32,9 +32,12 @@ class Request implements Arrayable
use HasUser;
use HasHeaders;
use HasCookies;
use HasTrailers;
use HasQueryParameters;

protected readonly BodyParser $body;
protected const DEFAULT_BODY_SIZE_LIMIT = 120 * 1024 * 1024;

protected BodyParser|null $body;

protected readonly Query $query;

Expand All @@ -60,7 +63,7 @@ public function __construct(

$this->query = Query::fromUri($request->getUri());
$this->routeAttributes = new RouteAttributes($routeAttributes);
$this->body = $this->getParser();
$this->body = null;
}

public function getClient(): Client
Expand All @@ -83,21 +86,6 @@ public function setBody(ReadableStream|string $body): void
$this->request->setBody($body);
}

public function getTrailers(): Trailers|null
{
return $this->request->getTrailers();
}

public function setTrailers(Trailers $trailers): void
{
$this->request->setTrailers($trailers);
}

public function removeTrailers(): void
{
$this->request->removeTrailers();
}

public function getMethod(): string
{
return $this->request->getMethod();
Expand Down Expand Up @@ -133,11 +121,13 @@ public function query(string|null $key = null, array|string|int|null $default =

public function body(string|null $key = null, array|string|int|null $default = null): BodyParser|BufferedFile|array|string|int|null
{
$body = $this->bodyParser();

if ($key) {
return $this->body->hasFile($key) ? $this->body->getFile($key) : $this->body->get($key, $default);
return $body->hasFile($key) ? $body->getFile($key) : $body->get($key, $default);
}

return $this->body;
return $body;
}

public function session(string|null $key = null, array|string|int|null $default = null): Session|array|string|int|null
Expand All @@ -156,28 +146,49 @@ public function ip(): Ip

public function toArray(): array
{
return $this->body->toArray();
return $this->bodyParser()->toArray();
}

protected function mode(): RequestMode
{
return RequestMode::BUFFERED;
}

protected function bodySizeLimit(): int
{
return self::DEFAULT_BODY_SIZE_LIMIT;
}

protected function fieldCountLimit(): int|null
{
return null;
}

protected function bodyParser(): BodyParser
{
return $this->body ??= $this->getParser();
}

protected function getParser(): BodyParser
{
$contentType = ContentType::fromValue($this->request->getHeader('content-type'));

if ($contentType === ContentType::JSON) {
return JsonParser::fromRequest($this->request);
return JsonParser::fromRequest($this->request, [
'body_size_limit' => $this->bodySizeLimit(),
]);
}

if ($this->mode() === RequestMode::STREAMED) {
return StreamParser::fromRequest($this->request, [
'body_size_limit' => 120 * 1024 * 1024,
'body_size_limit' => $this->bodySizeLimit(),
'field_count_limit' => $this->fieldCountLimit(),
]);
}

return FormParser::fromRequest($this->request);
return FormParser::fromRequest($this->request, [
'body_size_limit' => $this->bodySizeLimit(),
'field_count_limit' => $this->fieldCountLimit(),
]);
}
}
25 changes: 25 additions & 0 deletions src/Http/Requests/Concerns/HasTrailers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Phenix\Http\Requests\Concerns;

use Amp\Http\Server\Trailers;

trait HasTrailers
{
public function getTrailers(): Trailers|null
{
return $this->request->getTrailers();
}

public function setTrailers(Trailers $trailers): void
{
$this->request->setTrailers($trailers);
}

public function removeTrailers(): void
{
$this->request->removeTrailers();
}
}
26 changes: 22 additions & 4 deletions src/Http/Requests/FormParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

namespace Phenix\Http\Requests;

use Amp\ByteStream\BufferException;
use Amp\Http\HttpStatus;
use Amp\Http\Server\FormParser\BufferedFile;
use Amp\Http\Server\FormParser\Form;
use Amp\Http\Server\FormParser\FormParser as AmpFormParser;
use Amp\Http\Server\HttpErrorException;
use Amp\Http\Server\Request;

use function Amp\Http\Server\FormParser\parseContentBoundary;
use function is_array;
use function is_null;
use function is_numeric;
Expand All @@ -16,14 +21,19 @@ class FormParser extends BodyParser
{
private Form|null $form;

public function __construct()
{
public function __construct(
private readonly int $bodySizeLimit = 120 * 1024 * 1024,
private readonly int|null $fieldCountLimit = null
) {
$this->form = null;
}

public static function fromRequest(Request $request, array $options = []): self
{
$parser = new self();
$parser = new self(
$options['body_size_limit'] ?? 120 * 1024 * 1024,
$options['field_count_limit'] ?? null
);
$parser->parse($request);

return $parser;
Expand Down Expand Up @@ -79,7 +89,15 @@ public function toArray(): array

protected function parse(Request $request): self
{
$this->form = Form::fromRequest($request);
$boundary = parseContentBoundary($request->getHeader('content-type') ?? '');

try {
$body = $boundary === null ? '' : $request->getBody()->buffer(limit: $this->bodySizeLimit);
} catch (BufferException $exception) {
throw new HttpErrorException(HttpStatus::PAYLOAD_TOO_LARGE, 'Request body is too large', $exception);
}

$this->form = (new AmpFormParser($this->fieldCountLimit))->parseBody($body, $boundary);

return $this;
}
Expand Down
20 changes: 15 additions & 5 deletions src/Http/Requests/JsonParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

namespace Phenix\Http\Requests;

use Amp\ByteStream\BufferException;
use Amp\Http\HttpStatus;
use Amp\Http\Server\FormParser\BufferedFile;
use Amp\Http\Server\HttpErrorException;
use Amp\Http\Server\Request;

use function is_numeric;
Expand All @@ -13,14 +16,15 @@ class JsonParser extends BodyParser
{
private array $body;

public function __construct()
{
public function __construct(
private readonly int $bodySizeLimit = 120 * 1024 * 1024
) {
$this->body = [];
}

public static function fromRequest(Request $request, array $options = []): self
{
return (new self())->parse($request);
return (new self($options['body_size_limit'] ?? 120 * 1024 * 1024))->parse($request);
}

public function get(string $key, array|string|int|null $default = null): array|string|int|null
Expand Down Expand Up @@ -66,9 +70,15 @@ public function toArray(): array

protected function parse(Request $request): self
{
$body = json_decode($request->getBody()->read() ?? '', true);
try {
$raw = $request->getBody()->buffer(limit: $this->bodySizeLimit);
} catch (BufferException $exception) {
throw new HttpErrorException(HttpStatus::PAYLOAD_TOO_LARGE, 'Request body is too large', $exception);
}

$body = json_decode($raw, true);

if (json_last_error() === JSON_ERROR_NONE) {
if (json_last_error() === JSON_ERROR_NONE && is_array($body)) {
$this->body = $body;
}

Expand Down
Loading
Loading