diff --git a/.envDefault b/.envDefault index 8290d4f..1fa763a 100644 --- a/.envDefault +++ b/.envDefault @@ -1,3 +1,5 @@ displayError=true logErrors=true logErrorDetails=true +RequestLimit=1 +RequestLimitTimeout=10000 diff --git a/app/ErrorHandler/CustomHtmlErrorRenderer.php b/app/ErrorHandler/CustomHtmlErrorRenderer.php new file mode 100644 index 0000000..c2238aa --- /dev/null +++ b/app/ErrorHandler/CustomHtmlErrorRenderer.php @@ -0,0 +1,108 @@ +The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderExceptionFragment($exception); + } else { + $html = "

{$exception->getMessage()}
{$this->getErrorDescription($exception)}

"; + } + + return $this->renderHtmlBody($this->getErrorTitle($exception), $html); + } + + /** + * @param Throwable $exception + * @return string + */ + private function renderExceptionFragment(Throwable $exception): string + { + $html = sprintf('
Type: %s
', get_class($exception)); + + $code = $exception->getCode(); + if ($code !== null) { + $html .= sprintf('
Code: %s
', $code); + } + + $message = $exception->getMessage(); + if ($message !== null) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + $file = $exception->getFile(); + if ($file !== null) { + $html .= sprintf('
File: %s
', $file); + } + + $line = $exception->getLine(); + if ($line !== null) { + $html .= sprintf('
Line: %s
', $line); + } + + $trace = $exception->getTraceAsString(); + if ($trace !== null) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } + + /** + * @param string $title + * @param string $html + * @return string + */ + public function renderHtmlBody(string $title = '', string $html = ''): string + { + return sprintf( + '' . + ' ' . + " " . + ' %s' . + ' ' . + ' ' . + ' ' . + '

%s

' . + '
%s
' . + ' ' . + '', + $title, + $title, + $html + ); + } +} diff --git a/app/Kernel.php b/app/Kernel.php index 58726fa..afda506 100644 --- a/app/Kernel.php +++ b/app/Kernel.php @@ -2,6 +2,7 @@ namespace App; +use App\ErrorHandler\CustomHtmlErrorRenderer; use App\Middlewares\ThrottleMiddleware; use Dotenv\Dotenv; use Dotenv\Exception\InvalidPathException; @@ -51,6 +52,8 @@ class Kernel env('logErrors', false), env('logErrorDetails', false) ); + $errorHandler = $errorMiddleware->getDefaultErrorHandler(); + $errorHandler->registerErrorRenderer('text/html', CustomHtmlErrorRenderer::class); } private static function loadEnv() @@ -69,7 +72,7 @@ class Kernel public static function env(string $key, $default = null) { - $value = getenv($key); + $value = self::$env[$key] ?? $default; if ($value === false) { return $default; @@ -93,10 +96,6 @@ class Kernel return; } - // if (Str::startsWith($value, '"') && Str::endsWith($value, '"')) { - // return substr($value, 1, -1); - // } - return $value; } diff --git a/app/Middlewares/ThrottleMiddleware.php b/app/Middlewares/ThrottleMiddleware.php index 0b12206..5dc8384 100644 --- a/app/Middlewares/ThrottleMiddleware.php +++ b/app/Middlewares/ThrottleMiddleware.php @@ -5,6 +5,7 @@ namespace App\Middlewares; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use GuzzleHttp\Psr7\Response; +use Slim\Exception\HttpForbiddenException; use Stiphle\Throttle\LeakyBucket; class ThrottleMiddleware @@ -23,8 +24,12 @@ class ThrottleMiddleware //! Enable APC in Laragon https://forum.laragon.org/topic/1245/regarding-caching-plugin-availability/4 $storage = new \Stiphle\Storage\Apcu(); $throttle->setStorage($storage); - // limit to 1 per second - $throttle->throttle('api.request', 1, 10000); + // limit to 1 per second by default + $estimate = $throttle->getEstimate('api.request', env('RequestLimit', 1), env('RequestLimitTimeout', 1000)); + if ($estimate > 1000) { + throw new HttpForbiddenException($request, 'Too many requests'); + } + $throttle->throttle('api.request', env('RequestLimit', 1), env('RequestLimitTimeout', 1000)); return $handler->handle($request); } }