Skip to content

Commit d6782ce

Browse files
committed
Implement new MessageAccessor class
1 parent 52861fe commit d6782ce

File tree

3 files changed

+334
-0
lines changed

3 files changed

+334
-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: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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 $values = [],
22+
array $queryFilters = [],
23+
array $headersFilters = [],
24+
array $jsonFilers = [],
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+
* @param MessageInterface $message
91+
* @return bool
92+
*/
93+
public function isJson(MessageInterface $message): bool
94+
{
95+
return $message->hasHeader('Content-Type') &&
96+
Str::contains($message->getHeaderLine('Content-Type'), 'json');
97+
}
98+
99+
public function getJson(MessageInterface $message): ?array
100+
{
101+
return $this->replaceParameters(
102+
json_decode($message->getBody(), true),
103+
$this->jsonFilters,
104+
$this->values,
105+
$this->replace
106+
);
107+
}
108+
109+
public function getContent(MessageInterface $message): string
110+
{
111+
if ($this->isJson($message)) {
112+
$body = json_encode($this->getJson($message));
113+
} else {
114+
$body = $message->getBody()->getContents();
115+
foreach($this->values as $value) {
116+
$body = $this->replace($value, $this->replace, $body);
117+
}
118+
}
119+
120+
return $body;
121+
}
122+
123+
public function filter(MessageInterface $message): MessageInterface
124+
{
125+
if ($this->isJson($message)) {
126+
$body = json_encode($this->getJson($message));
127+
} else {
128+
$body = $message->getBody()->getContents();
129+
foreach ($this->values as $value) {
130+
$body = $this->replace($value, $this->replace, $body);
131+
}
132+
}
133+
134+
foreach ($this->getHeaders($message) as $header => $values) {
135+
$message = $message->withHeader($header, $values);
136+
}
137+
138+
return $message->withBody(Utils::streamFor($body));
139+
}
140+
141+
protected function replaceParameters(array $array, array $parameters, array $values, string $replace, $strict = true): array
142+
{
143+
foreach ($parameters as $parameter) {
144+
if (data_get($array, $parameter, null)) {
145+
data_set($array, $parameter, $replace);
146+
}
147+
}
148+
149+
array_walk_recursive( $array, function (&$item, $key) use ($values, $replace, $strict) {
150+
foreach ($values as $value) {
151+
if (! $strict && str_contains($item, $value)) {
152+
$item = str_replace($value, $replace, $item);
153+
} elseif ($strict && $value === $item) {
154+
$item = $replace;
155+
}
156+
}
157+
158+
return $item;
159+
});
160+
161+
return $array;
162+
}
163+
164+
protected function replace($search, $replace, ?string $subject): ?string
165+
{
166+
if (is_null($subject)) {
167+
return null;
168+
}
169+
170+
return str_replace($search, $replace, $subject);
171+
}
172+
}

tests/MessageAccessorTest.php

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
['secret'],
24+
['search', 'filter.field2'],
25+
['Authorization'],
26+
['data.baz.*.password']
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('https://user:********@********.example.com:9000',
70+
urldecode($this->messageAccessor->getBase($this->request)));
71+
}
72+
73+
public function test_get_query()
74+
{
75+
$query = $this->messageAccessor->getQuery($this->request);
76+
77+
$this->assertIsArray($query);
78+
$this->assertEquals([
79+
'test' => 'true',
80+
'search' => '********',
81+
'filter' => [
82+
'field1' => 'A',
83+
'field2' => '********',
84+
],
85+
], $query);
86+
}
87+
88+
public function test_get_headers()
89+
{
90+
$headers = $this->messageAccessor->getHeaders($this->request);
91+
92+
$this->assertIsArray($headers);
93+
$this->assertEquals([
94+
'Accept' => ['application/json'],
95+
'Content-Type' => ['application/json'],
96+
'Authorization' => ['********'],
97+
'Host' => ['********.example.com:9000'],
98+
], $headers);
99+
}
100+
101+
public function test_is_json()
102+
{
103+
$this->assertTrue($this->messageAccessor->isJson($this->request));
104+
$this->assertFalse($this->messageAccessor->isJson(new Response(200, ['Content-Type' => 'text/html'], '<html></html>')));
105+
}
106+
107+
public function test_get_json()
108+
{
109+
$json = $this->messageAccessor->getJson($this->request);
110+
111+
$this->assertIsArray($json);
112+
$this->assertEquals([
113+
'data' => [
114+
'foo' => 'bar',
115+
'baz' => [
116+
[
117+
'field_1' => 'value1',
118+
'field_2' => 'value2',
119+
'password' => '********',
120+
'secret' => 'this is not for everyone', // Note that keys are NOT filtered
121+
]
122+
]
123+
],
124+
], $json);
125+
}
126+
127+
public function test_get_content()
128+
{
129+
$content = $this->messageAccessor->getContent($this->request);
130+
131+
$this->assertEquals(json_encode([
132+
'data' => [
133+
'foo' => 'bar',
134+
'baz' => [
135+
[
136+
'field_1' => 'value1',
137+
'field_2' => 'value2',
138+
'password' => '********',
139+
'secret' => 'this is not for everyone', // Note that keys are NOT filtered
140+
]
141+
]
142+
],
143+
]), $content);
144+
}
145+
146+
public function test_filter()
147+
{
148+
$request = $this->messageAccessor->filter($this->request);
149+
150+
// Note that this is require to use double quotes for the Carriage Return (\r) to work
151+
$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
152+
Host: ********.example.com:9000\r
153+
Accept: application/json\r
154+
Content-Type: application/json\r
155+
Authorization: ********\r
156+
\r
157+
{\"data\":{\"foo\":\"bar\",\"baz\":[{\"field_1\":\"value1\",\"field_2\":\"value2\",\"password\":\"********\",\"secret\":\"this is not for everyone\"}]}}";
158+
159+
$this->assertEquals($output, Message::toString($request));
160+
}
161+
}

0 commit comments

Comments
 (0)