From d6782ce67258faa13f0628be22ccc64bb425b02b Mon Sep 17 00:00:00 2001 From: Anders Bilfeldt Date: Sat, 18 Dec 2021 19:36:14 +0100 Subject: [PATCH 1/5] Implement new MessageAccessor class --- composer.json | 1 + src/MessageAccessor.php | 172 ++++++++++++++++++++++++++++++++++ tests/MessageAccessorTest.php | 161 +++++++++++++++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 src/MessageAccessor.php create mode 100644 tests/MessageAccessorTest.php diff --git a/composer.json b/composer.json index f00bd7f..218d1fa 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ ], "require": { "php": "^7.4|^8.0", + "ext-json": "*", "guzzlehttp/guzzle": "^7.2", "illuminate/http": "^8.0", "illuminate/support": "^8.0", diff --git a/src/MessageAccessor.php b/src/MessageAccessor.php new file mode 100644 index 0000000..949b82c --- /dev/null +++ b/src/MessageAccessor.php @@ -0,0 +1,172 @@ +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(), 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()->getContents(); + foreach($this->values as $value) { + $body = $this->replace($value, $this->replace, $body); + } + } + + return $body; + } + + public function filter(MessageInterface $message): MessageInterface + { + if ($this->isJson($message)) { + $body = json_encode($this->getJson($message)); + } else { + $body = $message->getBody()->getContents(); + foreach ($this->values as $value) { + $body = $this->replace($value, $this->replace, $body); + } + } + + 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, $key) 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); + } +} diff --git a/tests/MessageAccessorTest.php b/tests/MessageAccessorTest.php new file mode 100644 index 0000000..32a98ea --- /dev/null +++ b/tests/MessageAccessorTest.php @@ -0,0 +1,161 @@ +messageAccessor = new MessageAccessor( + ['secret'], + ['search', 'filter.field2'], + ['Authorization'], + ['data.baz.*.password'] + ); + + $this->request = new Request( + 'POST', + 'https://user:secret@secret.example.com: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'], ''))); + } + + 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 this is require to use double quotes for the Carriage Return (\r) to work + $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 +Host: ********.example.com:9000\r +Accept: application/json\r +Content-Type: application/json\r +Authorization: ********\r +\r +{\"data\":{\"foo\":\"bar\",\"baz\":[{\"field_1\":\"value1\",\"field_2\":\"value2\",\"password\":\"********\",\"secret\":\"this is not for everyone\"}]}}"; + + $this->assertEquals($output, Message::toString($request)); + } +} From 49010daad60c1d99cad85c546f2cb7cff89bdb75 Mon Sep 17 00:00:00 2001 From: Anders Bilfeldt Date: Sat, 18 Dec 2021 19:43:23 +0100 Subject: [PATCH 2/5] Psalm fixes --- src/MessageAccessor.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/MessageAccessor.php b/src/MessageAccessor.php index 949b82c..72eca77 100644 --- a/src/MessageAccessor.php +++ b/src/MessageAccessor.php @@ -99,7 +99,7 @@ public function isJson(MessageInterface $message): bool public function getJson(MessageInterface $message): ?array { return $this->replaceParameters( - json_decode($message->getBody(), true), + json_decode($message->getBody()->__toString(), true), $this->jsonFilters, $this->values, $this->replace @@ -111,9 +111,9 @@ public function getContent(MessageInterface $message): string if ($this->isJson($message)) { $body = json_encode($this->getJson($message)); } else { - $body = $message->getBody()->getContents(); + $body = $message->getBody()->__toString(); foreach($this->values as $value) { - $body = $this->replace($value, $this->replace, $body); + $body = str_replace($value, $this->replace, $body); } } @@ -122,14 +122,7 @@ public function getContent(MessageInterface $message): string public function filter(MessageInterface $message): MessageInterface { - if ($this->isJson($message)) { - $body = json_encode($this->getJson($message)); - } else { - $body = $message->getBody()->getContents(); - foreach ($this->values as $value) { - $body = $this->replace($value, $this->replace, $body); - } - } + $body = $this->getContent($message); foreach ($this->getHeaders($message) as $header => $values) { $message = $message->withHeader($header, $values); @@ -146,7 +139,7 @@ protected function replaceParameters(array $array, array $parameters, array $val } } - array_walk_recursive( $array, function (&$item, $key) use ($values, $replace, $strict) { + 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); From 33741234f8b7c9fb94b7fcf986637543b469840c Mon Sep 17 00:00:00 2001 From: Bilfeldt Date: Sat, 18 Dec 2021 18:44:43 +0000 Subject: [PATCH 3/5] Apply fixes from StyleCI --- src/MessageAccessor.php | 24 ++++++++++-------- tests/MessageAccessorTest.php | 48 ++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/MessageAccessor.php b/src/MessageAccessor.php index 72eca77..338791b 100644 --- a/src/MessageAccessor.php +++ b/src/MessageAccessor.php @@ -18,12 +18,12 @@ class MessageAccessor private string $replace; public function __construct( - array $values = [], - array $queryFilters = [], - array $headersFilters = [], - array $jsonFilers = [], + array $values = [], + array $queryFilters = [], + array $headersFilters = [], + array $jsonFilers = [], string $replace = '********' - ){ + ) { $this->values = $values; $this->queryFilters = $queryFilters; $this->headersFilters = $headersFilters; @@ -49,16 +49,16 @@ public function getBase(RequestInterface $request): string $base = ''; if ($uri->getScheme()) { - $base .= $uri->getScheme() . '://'; + $base .= $uri->getScheme().'://'; } if ($uri->getUserInfo()) { - $base .= $uri->getUserInfo() . '@'; + $base .= $uri->getUserInfo().'@'; } if ($uri->getHost()) { $base .= $uri->getHost(); } if ($uri->getPort()) { - $base .= ':' . $uri->getPort(); + $base .= ':'.$uri->getPort(); } return $base; @@ -87,7 +87,9 @@ public function getHeaders(MessageInterface $message): array * 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 @@ -112,7 +114,7 @@ public function getContent(MessageInterface $message): string $body = json_encode($this->getJson($message)); } else { $body = $message->getBody()->__toString(); - foreach($this->values as $value) { + foreach ($this->values as $value) { $body = str_replace($value, $this->replace, $body); } } @@ -139,9 +141,9 @@ protected function replaceParameters(array $array, array $parameters, array $val } } - array_walk_recursive( $array, function (&$item) use ($values, $replace, $strict) { + array_walk_recursive($array, function (&$item) use ($values, $replace, $strict) { foreach ($values as $value) { - if (! $strict && str_contains($item, $value)) { + if (!$strict && str_contains($item, $value)) { $item = str_replace($value, $replace, $item); } elseif ($strict && $value === $item) { $item = $replace; diff --git a/tests/MessageAccessorTest.php b/tests/MessageAccessorTest.php index 32a98ea..42fc704 100644 --- a/tests/MessageAccessorTest.php +++ b/tests/MessageAccessorTest.php @@ -30,8 +30,8 @@ public function setUp(): void 'POST', 'https://user:secret@secret.example.com: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', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', 'Authorization' => 'Bearer 1234567890', ], json_encode([ @@ -39,12 +39,12 @@ public function setUp(): void 'foo' => 'bar', 'baz' => [ [ - 'field_1' => 'value1', - 'field_2' => 'value2', + 'field_1' => 'value1', + 'field_2' => 'value2', 'password' => '123456', - 'secret' => 'this is not for everyone', - ] - ] + 'secret' => 'this is not for everyone', + ], + ], ], ]) ); @@ -66,8 +66,10 @@ public function test_get_uri() public function test_get_base() { - $this->assertEquals('https://user:********@********.example.com:9000', - urldecode($this->messageAccessor->getBase($this->request))); + $this->assertEquals( + 'https://user:********@********.example.com:9000', + urldecode($this->messageAccessor->getBase($this->request)) + ); } public function test_get_query() @@ -76,7 +78,7 @@ public function test_get_query() $this->assertIsArray($query); $this->assertEquals([ - 'test' => 'true', + 'test' => 'true', 'search' => '********', 'filter' => [ 'field1' => 'A', @@ -91,10 +93,10 @@ public function test_get_headers() $this->assertIsArray($headers); $this->assertEquals([ - 'Accept' => ['application/json'], - 'Content-Type' => ['application/json'], + 'Accept' => ['application/json'], + 'Content-Type' => ['application/json'], 'Authorization' => ['********'], - 'Host' => ['********.example.com:9000'], + 'Host' => ['********.example.com:9000'], ], $headers); } @@ -114,12 +116,12 @@ public function test_get_json() 'foo' => 'bar', 'baz' => [ [ - 'field_1' => 'value1', - 'field_2' => 'value2', + 'field_1' => 'value1', + 'field_2' => 'value2', 'password' => '********', - 'secret' => 'this is not for everyone', // Note that keys are NOT filtered - ] - ] + 'secret' => 'this is not for everyone', // Note that keys are NOT filtered + ], + ], ], ], $json); } @@ -133,12 +135,12 @@ public function test_get_content() 'foo' => 'bar', 'baz' => [ [ - 'field_1' => 'value1', - 'field_2' => 'value2', + 'field_1' => 'value1', + 'field_2' => 'value2', 'password' => '********', - 'secret' => 'this is not for everyone', // Note that keys are NOT filtered - ] - ] + 'secret' => 'this is not for everyone', // Note that keys are NOT filtered + ], + ], ], ]), $content); } From 392c6f7175f24adc4344b6223605e5384dd07062 Mon Sep 17 00:00:00 2001 From: Anders Bilfeldt Date: Sat, 18 Dec 2021 19:52:27 +0100 Subject: [PATCH 4/5] Fix tests --- tests/MessageAccessorTest.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/MessageAccessorTest.php b/tests/MessageAccessorTest.php index 42fc704..267ae44 100644 --- a/tests/MessageAccessorTest.php +++ b/tests/MessageAccessorTest.php @@ -149,14 +149,8 @@ public function test_filter() { $request = $this->messageAccessor->filter($this->request); - // Note that this is require to use double quotes for the Carriage Return (\r) to work - $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 -Host: ********.example.com:9000\r -Accept: application/json\r -Content-Type: application/json\r -Authorization: ********\r -\r -{\"data\":{\"foo\":\"bar\",\"baz\":[{\"field_1\":\"value1\",\"field_2\":\"value2\",\"password\":\"********\",\"secret\":\"this is not for everyone\"}]}}"; + // 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)); } From 67bb851730ffa78fdc8a0f58e24696f6f54676ec Mon Sep 17 00:00:00 2001 From: Anders Bilfeldt Date: Sat, 18 Dec 2021 23:02:28 +0100 Subject: [PATCH 5/5] Change order of arguments --- src/MessageAccessor.php | 4 ++-- tests/MessageAccessorTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MessageAccessor.php b/src/MessageAccessor.php index 338791b..7ca8a4d 100644 --- a/src/MessageAccessor.php +++ b/src/MessageAccessor.php @@ -18,10 +18,10 @@ class MessageAccessor private string $replace; public function __construct( - array $values = [], + array $jsonFilers = [], array $queryFilters = [], array $headersFilters = [], - array $jsonFilers = [], + array $values = [], string $replace = '********' ) { $this->values = $values; diff --git a/tests/MessageAccessorTest.php b/tests/MessageAccessorTest.php index 267ae44..cb11819 100644 --- a/tests/MessageAccessorTest.php +++ b/tests/MessageAccessorTest.php @@ -20,10 +20,10 @@ public function setUp(): void parent::setUp(); $this->messageAccessor = new MessageAccessor( - ['secret'], + ['data.baz.*.password'], ['search', 'filter.field2'], ['Authorization'], - ['data.baz.*.password'] + ['secret'], ); $this->request = new Request(