mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 13:35:54 +00:00
318 lines
9.1 KiB
PHP
318 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace App\Logger;
|
|
|
|
use Carbon\Carbon;
|
|
use function GuzzleHttp\Psr7\parse_request;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
use Laminas\Http\Request;
|
|
use Laminas\Http\Response;
|
|
use Namshi\Cuzzle\Formatter\CurlFormatter;
|
|
use Riverline\MultiPartParser\StreamedPart;
|
|
|
|
class LoggedRequest implements \JsonSerializable
|
|
{
|
|
/** @var string */
|
|
protected $rawRequest;
|
|
|
|
/** @var Request */
|
|
protected $parsedRequest;
|
|
|
|
/** @var string */
|
|
protected $rawResponse;
|
|
|
|
/** @var Response */
|
|
protected $parsedResponse;
|
|
|
|
/** @var string */
|
|
protected $id;
|
|
|
|
/** @var Carbon */
|
|
protected $startTime;
|
|
|
|
/** @var Carbon */
|
|
protected $stopTime;
|
|
|
|
/** @var string */
|
|
protected $subdomain;
|
|
|
|
/** @var array */
|
|
protected $additionalData = [];
|
|
|
|
public function __construct(string $rawRequest, Request $parsedRequest)
|
|
{
|
|
$this->startTime = now();
|
|
$this->rawRequest = $rawRequest;
|
|
$this->parsedRequest = $parsedRequest;
|
|
$this->id = $this->getRequestId();
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function jsonSerialize()
|
|
{
|
|
$data = [
|
|
'id' => $this->id,
|
|
'performed_at' => $this->startTime->toDateTimeString(),
|
|
'duration' => $this->getDuration(),
|
|
'subdomain' => $this->detectSubdomain(),
|
|
'request' => [
|
|
'raw' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->rawRequest,
|
|
'method' => $this->parsedRequest->getMethod(),
|
|
'uri' => $this->parsedRequest->getUriString(),
|
|
'headers' => $this->parsedRequest->getHeaders()->toArray(),
|
|
'body' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->parsedRequest->getContent(),
|
|
'query' => $this->parsedRequest->getQuery()->toArray(),
|
|
'post' => $this->getPostData(),
|
|
'curl' => $this->getRequestAsCurl(),
|
|
'additional_data' => $this->additionalData,
|
|
],
|
|
];
|
|
|
|
if ($this->parsedResponse) {
|
|
$logBody = $this->shouldReturnBody();
|
|
|
|
try {
|
|
$body = $logBody ? $this->parsedResponse->getBody() : '';
|
|
} catch (\Exception $e) {
|
|
$body = '';
|
|
}
|
|
|
|
$data['response'] = [
|
|
'raw' => $logBody ? $this->rawResponse : 'SKIPPED BY CONFIG OR BINARY RESPONSE',
|
|
'status' => $this->parsedResponse->getStatusCode(),
|
|
'headers' => $this->parsedResponse->getHeaders()->toArray(),
|
|
'reason' => $this->parsedResponse->getReasonPhrase(),
|
|
'body' => $logBody ? $body : 'SKIPPED BY CONFIG OR BINARY RESPONSE',
|
|
];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
public function setAdditionalData(array $data)
|
|
{
|
|
$this->additionalData = array_merge($this->additionalData, $data);
|
|
}
|
|
|
|
public function getAdditionalData(): array
|
|
{
|
|
return $this->additionalData;
|
|
}
|
|
|
|
protected function isBinary(string $string): bool
|
|
{
|
|
return preg_match('~[^\x20-\x7E\t\r\n]~', $string) > 0;
|
|
}
|
|
|
|
protected function shouldReturnBody(): bool
|
|
{
|
|
if ($this->skipByStatus()) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->skipByContentType()) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->skipByExtension()) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->skipBySize()) {
|
|
return false;
|
|
}
|
|
|
|
$header = $this->parsedResponse->getHeaders()->get('Content-Type');
|
|
$contentType = $header ? $header->getMediaType() : '';
|
|
$patterns = [
|
|
'application/json',
|
|
'text/*',
|
|
'*javascript*',
|
|
];
|
|
|
|
return Str::is($patterns, $contentType);
|
|
}
|
|
|
|
protected function skipByStatus(): bool
|
|
{
|
|
if (empty(config()->get('expose.skip_body_log.status'))) {
|
|
return false;
|
|
}
|
|
|
|
return Str::is(config()->get('expose.skip_body_log.status'), $this->parsedResponse->getStatusCode());
|
|
}
|
|
|
|
protected function skipByContentType(): bool
|
|
{
|
|
if (empty(config()->get('expose.skip_body_log.content_type'))) {
|
|
return false;
|
|
}
|
|
|
|
$header = $this->parsedResponse->getHeaders()->get('Content-Type');
|
|
$contentType = $header ? $header->getMediaType() : '';
|
|
|
|
return Str::is(config()->get('expose.skip_body_log.content_type'), $contentType);
|
|
}
|
|
|
|
protected function skipByExtension(): bool
|
|
{
|
|
if (empty(config()->get('expose.skip_body_log.extension'))) {
|
|
return false;
|
|
}
|
|
|
|
return Str::is(config()->get('expose.skip_body_log.extension'), $this->parsedRequest->getUri()->getPath());
|
|
}
|
|
|
|
protected function skipBySize(): bool
|
|
{
|
|
$configSize = $this->getConfigSize(config()->get('expose.skip_body_log.size', '1MB'));
|
|
$contentLength = $this->parsedResponse->getHeaders()->get('Content-Length');
|
|
|
|
if (! $contentLength) {
|
|
return false;
|
|
}
|
|
|
|
$contentSize = $contentLength->getFieldValue() ?? 0;
|
|
|
|
return $contentSize > $configSize;
|
|
}
|
|
|
|
protected function getConfigSize(string $size): int
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB'];
|
|
$number = substr($size, 0, -2);
|
|
$suffix = strtoupper(substr($size, -2));
|
|
|
|
// B or no suffix
|
|
if (is_numeric(substr($suffix, 0, 1))) {
|
|
return preg_replace('/[^\d]/', '', $size);
|
|
}
|
|
|
|
// if we have an error in the input, default to GB
|
|
$exponent = array_flip($units)[$suffix] ?? 5;
|
|
|
|
return $number * (1024 ** $exponent);
|
|
}
|
|
|
|
public function getRequest()
|
|
{
|
|
return $this->parsedRequest;
|
|
}
|
|
|
|
public function setResponse(string $rawResponse, Response $response)
|
|
{
|
|
$this->parsedResponse = $response;
|
|
|
|
$this->rawResponse = $rawResponse;
|
|
|
|
if (is_null($this->stopTime)) {
|
|
$this->stopTime = now();
|
|
}
|
|
}
|
|
|
|
public function id(): string
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
public function getRequestData(): ?string
|
|
{
|
|
return $this->rawRequest;
|
|
}
|
|
|
|
public function getResponse(): ?Response
|
|
{
|
|
return $this->parsedResponse;
|
|
}
|
|
|
|
public function getPostData()
|
|
{
|
|
$postData = [];
|
|
|
|
$contentType = Arr::get($this->parsedRequest->getHeaders()->toArray(), 'Content-Type');
|
|
|
|
switch ($contentType) {
|
|
case 'application/x-www-form-urlencoded':
|
|
parse_str($this->parsedRequest->getContent(), $postData);
|
|
$postData = collect($postData)->map(function ($value, $key) {
|
|
return [
|
|
'name' => $key,
|
|
'value' => $value,
|
|
];
|
|
})->toArray();
|
|
break;
|
|
case 'application/json':
|
|
$postData = collect(json_decode($this->parsedRequest->getContent(), true))->map(function ($value, $key) {
|
|
return [
|
|
'name' => $key,
|
|
'value' => $value,
|
|
];
|
|
})->values()->toArray();
|
|
|
|
break;
|
|
default:
|
|
$stream = fopen('php://temp', 'rw');
|
|
fwrite($stream, $this->rawRequest);
|
|
rewind($stream);
|
|
|
|
try {
|
|
$document = new StreamedPart($stream);
|
|
if ($document->isMultiPart()) {
|
|
$postData = collect($document->getParts())->map(function (StreamedPart $part) {
|
|
return [
|
|
'name' => $part->getName(),
|
|
'value' => $part->isFile() ? null : $part->getBody(),
|
|
'is_file' => $part->isFile(),
|
|
'filename' => $part->isFile() ? $part->getFileName() : null,
|
|
'mime_type' => $part->isFile() ? $part->getMimeType() : null,
|
|
];
|
|
})->toArray();
|
|
}
|
|
} catch (\Exception $e) {
|
|
//
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $postData;
|
|
}
|
|
|
|
protected function detectSubdomain()
|
|
{
|
|
return collect($this->parsedRequest->getHeaders()->toArray())
|
|
->mapWithKeys(function ($value, $key) {
|
|
return [strtolower($key) => $value];
|
|
})->get('x-original-host');
|
|
}
|
|
|
|
protected function getRequestId()
|
|
{
|
|
return collect($this->parsedRequest->getHeaders()->toArray())
|
|
->mapWithKeys(function ($value, $key) {
|
|
return [strtolower($key) => $value];
|
|
})->get('x-expose-request-id', (string) Str::uuid());
|
|
}
|
|
|
|
public function getStartTime()
|
|
{
|
|
return $this->startTime;
|
|
}
|
|
|
|
public function getDuration()
|
|
{
|
|
return $this->startTime->diffInMilliseconds($this->stopTime, false);
|
|
}
|
|
|
|
protected function getRequestAsCurl(): string
|
|
{
|
|
try {
|
|
return (new CurlFormatter())->format(parse_request($this->rawRequest));
|
|
} catch (\Throwable $e) {
|
|
return '';
|
|
}
|
|
}
|
|
}
|