Skip to content

Commit d7e612e

Browse files
authored
Merge pull request #13 from bilfeldt/feature/message-accessor
Implement new MessageAccessor class
2 parents 52861fe + 67bb851 commit d7e612e

File tree

3 files changed

+325
-0
lines changed

3 files changed

+325
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
],
2222
"require": {
2323
"php": "^7.4|^8.0",
24+
"ext-json": "*",
2425
"guzzlehttp/guzzle": "^7.2",
2526
"illuminate/http": "^8.0",
2627
"illuminate/support": "^8.0",

src/MessageAccessor.php

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
3+
namespace Bilfeldt\LaravelHttpClientLogger;
4+
5+
use GuzzleHttp\Psr7\Utils;
6+
use Illuminate\Support\Arr;
7+
use Illuminate\Support\Str;
8+
use Psr\Http\Message\MessageInterface;
9+
use Psr\Http\Message\RequestInterface;
10+
use Psr\Http\Message\UriInterface;
11+
12+
class MessageAccessor
13+
{
14+
private array $values;
15+
private array $queryFilters;
16+
private array $headersFilters;
17+
private array $jsonFilters;
18+
private string $replace;
19+
20+
public function __construct(
21+
array $jsonFilers = [],
22+
array $queryFilters = [],
23+
array $headersFilters = [],
24+
array $values = [],
25+
string $replace = '********'
26+
) {
27+
$this->values = $values;
28+
$this->queryFilters = $queryFilters;
29+
$this->headersFilters = $headersFilters;
30+
$this->jsonFilters = $jsonFilers;
31+
$this->replace = $replace;
32+
}
33+
34+
public function getUri(RequestInterface $request): UriInterface
35+
{
36+
$uri = $request->getUri();
37+
parse_str($uri->getQuery(), $query);
38+
39+
return $uri
40+
->withUserInfo($this->replace($this->values, $this->replace, $uri->getUserInfo()))
41+
->withHost($this->replace($this->values, $this->replace, $uri->getHost()))
42+
->withPath($this->replace($this->values, $this->replace, $uri->getPath()))
43+
->withQuery(Arr::query($this->replaceParameters($query, $this->queryFilters, $this->values, $this->replace)));
44+
}
45+
46+
public function getBase(RequestInterface $request): string
47+
{
48+
$uri = $this->getUri($request);
49+
50+
$base = '';
51+
if ($uri->getScheme()) {
52+
$base .= $uri->getScheme().'://';
53+
}
54+
if ($uri->getUserInfo()) {
55+
$base .= $uri->getUserInfo().'@';
56+
}
57+
if ($uri->getHost()) {
58+
$base .= $uri->getHost();
59+
}
60+
if ($uri->getPort()) {
61+
$base .= ':'.$uri->getPort();
62+
}
63+
64+
return $base;
65+
}
66+
67+
public function getQuery(RequestInterface $request): array
68+
{
69+
parse_str($this->getUri($request)->getQuery(), $query);
70+
71+
return $query;
72+
}
73+
74+
public function getHeaders(MessageInterface $message): array
75+
{
76+
foreach ($this->headersFilters as $headersFilter) {
77+
if ($message->hasHeader($headersFilter)) {
78+
$message = $message->withHeader($headersFilter, $this->replace);
79+
}
80+
}
81+
82+
// Header filter applied above as this is an array with two layers
83+
return $this->replaceParameters($message->getHeaders(), [], $this->values, $this->replace, false);
84+
}
85+
86+
/**
87+
* Determine if the request is JSON.
88+
*
89+
* @see vendor/laravel/framework/src/Illuminate/Http/Client/Request.php
90+
*
91+
* @param MessageInterface $message
92+
*
93+
* @return bool
94+
*/
95+
public function isJson(MessageInterface $message): bool
96+
{
97+
return $message->hasHeader('Content-Type') &&
98+
Str::contains($message->getHeaderLine('Content-Type'), 'json');
99+
}
100+
101+
public function getJson(MessageInterface $message): ?array
102+
{
103+
return $this->replaceParameters(
104+
json_decode($message->getBody()->__toString(), true),
105+
$this->jsonFilters,
106+
$this->values,
107+
$this->replace
108+
);
109+
}
110+
111+
public function getContent(MessageInterface $message): string
112+
{
113+
if ($this->isJson($message)) {
114+
$body = json_encode($this->getJson($message));
115+
} else {
116+
$body = $message->getBody()->__toString();
117+
foreach ($this->values as $value) {
118+
$body = str_replace($value, $this->replace, $body);
119+
}
120+
}
121+
122+
return $body;
123+
}
124+
125+
public function filter(MessageInterface $message): MessageInterface
126+
{
127+
$body = $this->getContent($message);
128+
129+
foreach ($this->getHeaders($message) as $header => $values) {
130+
$message = $message->withHeader($header, $values);
131+
}
132+
133+
return $message->withBody(Utils::streamFor($body));
134+
}
135+
136+
protected function replaceParameters(array $array, array $parameters, array $values, string $replace, $strict = true): array
137+
{
138+
foreach ($parameters as $parameter) {
139+
if (data_get($array, $parameter, null)) {
140+
data_set($array, $parameter, $replace);
141+
}
142+
}
143+
144+
array_walk_recursive($array, function (&$item) use ($values, $replace, $strict) {
145+
foreach ($values as $value) {
146+
if (!$strict && str_contains($item, $value)) {
147+
$item = str_replace($value, $replace, $item);
148+
} elseif ($strict && $value === $item) {
149+
$item = $replace;
150+
}
151+
}
152+
153+
return $item;
154+
});
155+
156+
return $array;
157+
}
158+
159+
protected function replace($search, $replace, ?string $subject): ?string
160+
{
161+
if (is_null($subject)) {
162+
return null;
163+
}
164+
165+
return str_replace($search, $replace, $subject);
166+
}
167+
}

tests/MessageAccessorTest.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace Bilfeldt\LaravelHttpClientLogger\Tests;
4+
5+
use Bilfeldt\LaravelHttpClientLogger\MessageAccessor;
6+
use GuzzleHttp\Psr7\Message;
7+
use GuzzleHttp\Psr7\Request;
8+
use GuzzleHttp\Psr7\Response;
9+
use Psr\Http\Message\RequestInterface;
10+
use Psr\Http\Message\ResponseInterface;
11+
12+
class MessageAccessorTest extends TestCase
13+
{
14+
public MessageAccessor $messageAccessor;
15+
public RequestInterface $request;
16+
public ResponseInterface $response;
17+
18+
public function setUp(): void
19+
{
20+
parent::setUp();
21+
22+
$this->messageAccessor = new MessageAccessor(
23+
['data.baz.*.password'],
24+
['search', 'filter.field2'],
25+
['Authorization'],
26+
['secret'],
27+
);
28+
29+
$this->request = new Request(
30+
'POST',
31+
'https://user:[email protected]:9000/some-path/secret/should-not-be-removed?test=true&search=foo&filter[field1]=A&filter[field2]=B#anchor',
32+
[
33+
'Accept' => 'application/json',
34+
'Content-Type' => 'application/json',
35+
'Authorization' => 'Bearer 1234567890',
36+
],
37+
json_encode([
38+
'data' => [
39+
'foo' => 'bar',
40+
'baz' => [
41+
[
42+
'field_1' => 'value1',
43+
'field_2' => 'value2',
44+
'password' => '123456',
45+
'secret' => 'this is not for everyone',
46+
],
47+
],
48+
],
49+
])
50+
);
51+
}
52+
53+
public function test_get_uri()
54+
{
55+
$uri = $this->messageAccessor->getUri($this->request);
56+
57+
$this->assertEquals('https', $uri->getScheme());
58+
$this->assertEquals('user%3A********@********.example.com:9000', $uri->getAuthority());
59+
$this->assertEquals('user%3A********', $uri->getUserInfo());
60+
$this->assertEquals('********.example.com', $uri->getHost());
61+
$this->assertEquals('9000', $uri->getPort());
62+
$this->assertEquals('/some-path/********/should-not-be-removed', $uri->getPath());
63+
$this->assertEquals('test=true&search=********&filter[field1]=A&filter[field2]=********', urldecode($uri->getQuery()));
64+
$this->assertEquals('anchor', $uri->getFragment());
65+
}
66+
67+
public function test_get_base()
68+
{
69+
$this->assertEquals(
70+
'https://user:********@********.example.com:9000',
71+
urldecode($this->messageAccessor->getBase($this->request))
72+
);
73+
}
74+
75+
public function test_get_query()
76+
{
77+
$query = $this->messageAccessor->getQuery($this->request);
78+
79+
$this->assertIsArray($query);
80+
$this->assertEquals([
81+
'test' => 'true',
82+
'search' => '********',
83+
'filter' => [
84+
'field1' => 'A',
85+
'field2' => '********',
86+
],
87+
], $query);
88+
}
89+
90+
public function test_get_headers()
91+
{
92+
$headers = $this->messageAccessor->getHeaders($this->request);
93+
94+
$this->assertIsArray($headers);
95+
$this->assertEquals([
96+
'Accept' => ['application/json'],
97+
'Content-Type' => ['application/json'],
98+
'Authorization' => ['********'],
99+
'Host' => ['********.example.com:9000'],
100+
], $headers);
101+
}
102+
103+
public function test_is_json()
104+
{
105+
$this->assertTrue($this->messageAccessor->isJson($this->request));
106+
$this->assertFalse($this->messageAccessor->isJson(new Response(200, ['Content-Type' => 'text/html'], '<html></html>')));
107+
}
108+
109+
public function test_get_json()
110+
{
111+
$json = $this->messageAccessor->getJson($this->request);
112+
113+
$this->assertIsArray($json);
114+
$this->assertEquals([
115+
'data' => [
116+
'foo' => 'bar',
117+
'baz' => [
118+
[
119+
'field_1' => 'value1',
120+
'field_2' => 'value2',
121+
'password' => '********',
122+
'secret' => 'this is not for everyone', // Note that keys are NOT filtered
123+
],
124+
],
125+
],
126+
], $json);
127+
}
128+
129+
public function test_get_content()
130+
{
131+
$content = $this->messageAccessor->getContent($this->request);
132+
133+
$this->assertEquals(json_encode([
134+
'data' => [
135+
'foo' => 'bar',
136+
'baz' => [
137+
[
138+
'field_1' => 'value1',
139+
'field_2' => 'value2',
140+
'password' => '********',
141+
'secret' => 'this is not for everyone', // Note that keys are NOT filtered
142+
],
143+
],
144+
],
145+
]), $content);
146+
}
147+
148+
public function test_filter()
149+
{
150+
$request = $this->messageAccessor->filter($this->request);
151+
152+
// 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
153+
$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\"}]}}";
154+
155+
$this->assertEquals($output, Message::toString($request));
156+
}
157+
}

0 commit comments

Comments
 (0)