Skip to content

Commit

Permalink
Merge pull request #13 from bilfeldt/feature/message-accessor
Browse files Browse the repository at this point in the history
Implement new MessageAccessor class
  • Loading branch information
bilfeldt authored Dec 18, 2021
2 parents 52861fe + 67bb851 commit d7e612e
Show file tree
Hide file tree
Showing 3 changed files with 325 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
],
"require": {
"php": "^7.4|^8.0",
"ext-json": "*",
"guzzlehttp/guzzle": "^7.2",
"illuminate/http": "^8.0",
"illuminate/support": "^8.0",
Expand Down
167 changes: 167 additions & 0 deletions src/MessageAccessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

namespace Bilfeldt\LaravelHttpClientLogger;

use GuzzleHttp\Psr7\Utils;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;

class MessageAccessor
{
private array $values;
private array $queryFilters;
private array $headersFilters;
private array $jsonFilters;
private string $replace;

public function __construct(
array $jsonFilers = [],
array $queryFilters = [],
array $headersFilters = [],
array $values = [],
string $replace = '********'
) {
$this->values = $values;
$this->queryFilters = $queryFilters;
$this->headersFilters = $headersFilters;
$this->jsonFilters = $jsonFilers;
$this->replace = $replace;
}

public function getUri(RequestInterface $request): UriInterface
{
$uri = $request->getUri();
parse_str($uri->getQuery(), $query);

return $uri
->withUserInfo($this->replace($this->values, $this->replace, $uri->getUserInfo()))
->withHost($this->replace($this->values, $this->replace, $uri->getHost()))
->withPath($this->replace($this->values, $this->replace, $uri->getPath()))
->withQuery(Arr::query($this->replaceParameters($query, $this->queryFilters, $this->values, $this->replace)));
}

public function getBase(RequestInterface $request): string
{
$uri = $this->getUri($request);

$base = '';
if ($uri->getScheme()) {
$base .= $uri->getScheme().'://';
}
if ($uri->getUserInfo()) {
$base .= $uri->getUserInfo().'@';
}
if ($uri->getHost()) {
$base .= $uri->getHost();
}
if ($uri->getPort()) {
$base .= ':'.$uri->getPort();
}

return $base;
}

public function getQuery(RequestInterface $request): array
{
parse_str($this->getUri($request)->getQuery(), $query);

return $query;
}

public function getHeaders(MessageInterface $message): array
{
foreach ($this->headersFilters as $headersFilter) {
if ($message->hasHeader($headersFilter)) {
$message = $message->withHeader($headersFilter, $this->replace);
}
}

// Header filter applied above as this is an array with two layers
return $this->replaceParameters($message->getHeaders(), [], $this->values, $this->replace, false);
}

/**
* Determine if the request is JSON.
*
* @see vendor/laravel/framework/src/Illuminate/Http/Client/Request.php
*
* @param MessageInterface $message
*
* @return bool
*/
public function isJson(MessageInterface $message): bool
{
return $message->hasHeader('Content-Type') &&
Str::contains($message->getHeaderLine('Content-Type'), 'json');
}

public function getJson(MessageInterface $message): ?array
{
return $this->replaceParameters(
json_decode($message->getBody()->__toString(), true),
$this->jsonFilters,
$this->values,
$this->replace
);
}

public function getContent(MessageInterface $message): string
{
if ($this->isJson($message)) {
$body = json_encode($this->getJson($message));
} else {
$body = $message->getBody()->__toString();
foreach ($this->values as $value) {
$body = str_replace($value, $this->replace, $body);
}
}

return $body;
}

public function filter(MessageInterface $message): MessageInterface
{
$body = $this->getContent($message);

foreach ($this->getHeaders($message) as $header => $values) {
$message = $message->withHeader($header, $values);
}

return $message->withBody(Utils::streamFor($body));
}

protected function replaceParameters(array $array, array $parameters, array $values, string $replace, $strict = true): array
{
foreach ($parameters as $parameter) {
if (data_get($array, $parameter, null)) {
data_set($array, $parameter, $replace);
}
}

array_walk_recursive($array, function (&$item) use ($values, $replace, $strict) {
foreach ($values as $value) {
if (!$strict && str_contains($item, $value)) {
$item = str_replace($value, $replace, $item);
} elseif ($strict && $value === $item) {
$item = $replace;
}
}

return $item;
});

return $array;
}

protected function replace($search, $replace, ?string $subject): ?string
{
if (is_null($subject)) {
return null;
}

return str_replace($search, $replace, $subject);
}
}
157 changes: 157 additions & 0 deletions tests/MessageAccessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace Bilfeldt\LaravelHttpClientLogger\Tests;

use Bilfeldt\LaravelHttpClientLogger\MessageAccessor;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class MessageAccessorTest extends TestCase
{
public MessageAccessor $messageAccessor;
public RequestInterface $request;
public ResponseInterface $response;

public function setUp(): void
{
parent::setUp();

$this->messageAccessor = new MessageAccessor(
['data.baz.*.password'],
['search', 'filter.field2'],
['Authorization'],
['secret'],
);

$this->request = new Request(
'POST',
'https://user:[email protected]:9000/some-path/secret/should-not-be-removed?test=true&search=foo&filter[field1]=A&filter[field2]=B#anchor',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => 'Bearer 1234567890',
],
json_encode([
'data' => [
'foo' => 'bar',
'baz' => [
[
'field_1' => 'value1',
'field_2' => 'value2',
'password' => '123456',
'secret' => 'this is not for everyone',
],
],
],
])
);
}

public function test_get_uri()
{
$uri = $this->messageAccessor->getUri($this->request);

$this->assertEquals('https', $uri->getScheme());
$this->assertEquals('user%3A********@********.example.com:9000', $uri->getAuthority());
$this->assertEquals('user%3A********', $uri->getUserInfo());
$this->assertEquals('********.example.com', $uri->getHost());
$this->assertEquals('9000', $uri->getPort());
$this->assertEquals('/some-path/********/should-not-be-removed', $uri->getPath());
$this->assertEquals('test=true&search=********&filter[field1]=A&filter[field2]=********', urldecode($uri->getQuery()));
$this->assertEquals('anchor', $uri->getFragment());
}

public function test_get_base()
{
$this->assertEquals(
'https://user:********@********.example.com:9000',
urldecode($this->messageAccessor->getBase($this->request))
);
}

public function test_get_query()
{
$query = $this->messageAccessor->getQuery($this->request);

$this->assertIsArray($query);
$this->assertEquals([
'test' => 'true',
'search' => '********',
'filter' => [
'field1' => 'A',
'field2' => '********',
],
], $query);
}

public function test_get_headers()
{
$headers = $this->messageAccessor->getHeaders($this->request);

$this->assertIsArray($headers);
$this->assertEquals([
'Accept' => ['application/json'],
'Content-Type' => ['application/json'],
'Authorization' => ['********'],
'Host' => ['********.example.com:9000'],
], $headers);
}

public function test_is_json()
{
$this->assertTrue($this->messageAccessor->isJson($this->request));
$this->assertFalse($this->messageAccessor->isJson(new Response(200, ['Content-Type' => 'text/html'], '<html></html>')));
}

public function test_get_json()
{
$json = $this->messageAccessor->getJson($this->request);

$this->assertIsArray($json);
$this->assertEquals([
'data' => [
'foo' => 'bar',
'baz' => [
[
'field_1' => 'value1',
'field_2' => 'value2',
'password' => '********',
'secret' => 'this is not for everyone', // Note that keys are NOT filtered
],
],
],
], $json);
}

public function test_get_content()
{
$content = $this->messageAccessor->getContent($this->request);

$this->assertEquals(json_encode([
'data' => [
'foo' => 'bar',
'baz' => [
[
'field_1' => 'value1',
'field_2' => 'value2',
'password' => '********',
'secret' => 'this is not for everyone', // Note that keys are NOT filtered
],
],
],
]), $content);
}

public function test_filter()
{
$request = $this->messageAccessor->filter($this->request);

// Note that it is required to use double quotes for the Carriage Return (\r) to work and have it on one line to pass on Windows
$output = "POST /some-path/secret/should-not-be-removed?test=true&search=foo&filter%5Bfield1%5D=A&filter%5Bfield2%5D=B HTTP/1.1\r\nHost: ********.example.com:9000\r\nAccept: application/json\r\nContent-Type: application/json\r\nAuthorization: ********\r\n\r\n{\"data\":{\"foo\":\"bar\",\"baz\":[{\"field_1\":\"value1\",\"field_2\":\"value2\",\"password\":\"********\",\"secret\":\"this is not for everyone\"}]}}";

$this->assertEquals($output, Message::toString($request));
}
}

0 comments on commit d7e612e

Please sign in to comment.