| 1 |
<?php |
| 2 |
|
| 3 |
namespace Dudlewebs\WPMCS\s3\Aws; |
| 4 |
|
| 5 |
use Dudlewebs\WPMCS\s3\Aws\Api\Service; |
| 6 |
use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException; |
| 7 |
use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\RejectedPromise; |
| 8 |
use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface; |
| 9 |
use Dudlewebs\WPMCS\s3\Psr\Http\Message\ResponseInterface; |
| 10 |
use Dudlewebs\WPMCS\s3\Psr\Http\Message\StreamInterface; |
| 11 |
use RecursiveArrayIterator; |
| 12 |
use RecursiveIteratorIterator; |
| 13 |
/** |
| 14 |
* Traces state changes between middlewares. |
| 15 |
*/ |
| 16 |
class TraceMiddleware |
| 17 |
{ |
| 18 |
private $prevOutput; |
| 19 |
private $prevInput; |
| 20 |
private $config; |
| 21 |
/** @var Service */ |
| 22 |
private $service; |
| 23 |
private static $authHeaders = ['X-Amz-Security-Token' => '[TOKEN]']; |
| 24 |
private static $authStrings = [ |
| 25 |
// S3Signature |
| 26 |
'/AWSAccessKeyId=[A-Z0-9]{20}&/i' => 'AWSAccessKeyId=[KEY]&', |
| 27 |
// SignatureV4 Signature and S3Signature |
| 28 |
'/Signature=.+/i' => 'Signature=[SIGNATURE]', |
| 29 |
// SignatureV4 access key ID |
| 30 |
'/Credential=[A-Z0-9]{20}\\//i' => 'Credential=[KEY]/', |
| 31 |
// S3 signatures |
| 32 |
'/AWS [A-Z0-9]{20}:.+/' => 'AWS AKI[KEY]:[SIGNATURE]', |
| 33 |
// STS Presigned URLs |
| 34 |
'/X-Amz-Security-Token=[^&]+/i' => 'X-Amz-Security-Token=[TOKEN]', |
| 35 |
// Crypto *Stream Keys |
| 36 |
'/\\["key.{27,36}Stream.{9}\\]=>\\s+.{7}\\d{2}\\) "\\X{16,64}"/U' => '["key":[CONTENT KEY]]', |
| 37 |
]; |
| 38 |
/** |
| 39 |
* Configuration array can contain the following key value pairs. |
| 40 |
* |
| 41 |
* - logfn: (callable) Function that is invoked with log messages. By |
| 42 |
* default, PHP's "echo" function will be utilized. |
| 43 |
* - stream_size: (int) When the size of a stream is greater than this |
| 44 |
* number, the stream data will not be logged. Set to "0" to not log any |
| 45 |
* stream data. |
| 46 |
* - scrub_auth: (bool) Set to false to disable the scrubbing of auth data |
| 47 |
* from the logged messages. |
| 48 |
* - http: (bool) Set to false to disable the "debug" feature of lower |
| 49 |
* level HTTP adapters (e.g., verbose curl output). |
| 50 |
* - auth_strings: (array) A mapping of authentication string regular |
| 51 |
* expressions to scrubbed strings. These mappings are passed directly to |
| 52 |
* preg_replace (e.g., preg_replace($key, $value, $debugOutput) if |
| 53 |
* "scrub_auth" is set to true. |
| 54 |
* - auth_headers: (array) A mapping of header names known to contain |
| 55 |
* sensitive data to what the scrubbed value should be. The value of any |
| 56 |
* headers contained in this array will be replaced with the if |
| 57 |
* "scrub_auth" is set to true. |
| 58 |
*/ |
| 59 |
public function __construct(array $config = [], ?Service $service = null) |
| 60 |
{ |
| 61 |
$this->config = $config + ['logfn' => function ($value) { |
| 62 |
echo $value; |
| 63 |
}, 'stream_size' => 524288, 'scrub_auth' => \true, 'http' => \true, 'auth_strings' => [], 'auth_headers' => []]; |
| 64 |
$this->config['auth_strings'] += self::$authStrings; |
| 65 |
$this->config['auth_headers'] += self::$authHeaders; |
| 66 |
$this->service = $service; |
| 67 |
} |
| 68 |
public function __invoke($step, $name) |
| 69 |
{ |
| 70 |
$this->prevOutput = $this->prevInput = []; |
| 71 |
return function (callable $next) use($step, $name) { |
| 72 |
return function (CommandInterface $command, $request = null) use($next, $step, $name) { |
| 73 |
$this->createHttpDebug($command); |
| 74 |
$start = \microtime(\true); |
| 75 |
$this->stepInput(['step' => $step, 'name' => $name, 'request' => $this->requestArray($request), 'command' => $this->commandArray($command)]); |
| 76 |
return $next($command, $request)->then(function ($value) use($step, $name, $command, $start) { |
| 77 |
$this->flushHttpDebug($command); |
| 78 |
$this->stepOutput($start, ['step' => $step, 'name' => $name, 'result' => $this->resultArray($value), 'error' => null]); |
| 79 |
return $value; |
| 80 |
}, function ($reason) use($step, $name, $start, $command) { |
| 81 |
$this->flushHttpDebug($command); |
| 82 |
$this->stepOutput($start, ['step' => $step, 'name' => $name, 'result' => null, 'error' => $this->exceptionArray($reason)]); |
| 83 |
return new RejectedPromise($reason); |
| 84 |
}); |
| 85 |
}; |
| 86 |
}; |
| 87 |
} |
| 88 |
private function stepInput($entry) |
| 89 |
{ |
| 90 |
static $keys = ['command', 'request']; |
| 91 |
$this->compareStep($this->prevInput, $entry, '-> Entering', $keys); |
| 92 |
$this->write("\n"); |
| 93 |
$this->prevInput = $entry; |
| 94 |
} |
| 95 |
private function stepOutput($start, $entry) |
| 96 |
{ |
| 97 |
static $keys = ['result', 'error']; |
| 98 |
$this->compareStep($this->prevOutput, $entry, '<- Leaving', $keys); |
| 99 |
$totalTime = \microtime(\true) - $start; |
| 100 |
$this->write(" Inclusive step time: " . $totalTime . "\n\n"); |
| 101 |
$this->prevOutput = $entry; |
| 102 |
} |
| 103 |
private function compareStep(array $a, array $b, $title, array $keys) |
| 104 |
{ |
| 105 |
$changes = []; |
| 106 |
foreach ($keys as $key) { |
| 107 |
$av = isset($a[$key]) ? $a[$key] : null; |
| 108 |
$bv = isset($b[$key]) ? $b[$key] : null; |
| 109 |
$this->compareArray($av, $bv, $key, $changes); |
| 110 |
} |
| 111 |
$str = "\n{$title} step {$b['step']}, name '{$b['name']}'"; |
| 112 |
$str .= "\n" . \str_repeat('-', \strlen($str) - 1) . "\n\n "; |
| 113 |
$str .= $changes ? \implode("\n ", \str_replace("\n", "\n ", $changes)) : 'no changes'; |
| 114 |
$this->write($str . "\n"); |
| 115 |
} |
| 116 |
private function commandArray(CommandInterface $cmd) |
| 117 |
{ |
| 118 |
return ['instance' => \spl_object_hash($cmd), 'name' => $cmd->getName(), 'params' => $this->getRedactedArray($cmd)]; |
| 119 |
} |
| 120 |
private function requestArray($request = null) |
| 121 |
{ |
| 122 |
return !$request instanceof RequestInterface ? [] : \array_filter(['instance' => \spl_object_hash($request), 'method' => $request->getMethod(), 'headers' => $this->redactHeaders($request->getHeaders()), 'body' => $this->streamStr($request->getBody()), 'scheme' => $request->getUri()->getScheme(), 'port' => $request->getUri()->getPort(), 'path' => $request->getUri()->getPath(), 'query' => $request->getUri()->getQuery()]); |
| 123 |
} |
| 124 |
private function responseArray(?ResponseInterface $response = null) |
| 125 |
{ |
| 126 |
return !$response ? [] : ['instance' => \spl_object_hash($response), 'statusCode' => $response->getStatusCode(), 'headers' => $this->redactHeaders($response->getHeaders()), 'body' => $this->streamStr($response->getBody())]; |
| 127 |
} |
| 128 |
private function resultArray($value) |
| 129 |
{ |
| 130 |
return $value instanceof ResultInterface ? ['instance' => \spl_object_hash($value), 'data' => $value->toArray()] : $value; |
| 131 |
} |
| 132 |
private function exceptionArray($e) |
| 133 |
{ |
| 134 |
if (!$e instanceof \Exception) { |
| 135 |
return $e; |
| 136 |
} |
| 137 |
$result = ['instance' => \spl_object_hash($e), 'class' => \get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()]; |
| 138 |
if ($e instanceof AwsException) { |
| 139 |
$result += ['type' => $e->getAwsErrorType(), 'code' => $e->getAwsErrorCode(), 'requestId' => $e->getAwsRequestId(), 'statusCode' => $e->getStatusCode(), 'result' => $this->resultArray($e->getResult()), 'request' => $this->requestArray($e->getRequest()), 'response' => $this->responseArray($e->getResponse())]; |
| 140 |
} |
| 141 |
return $result; |
| 142 |
} |
| 143 |
private function compareArray($a, $b, $path, array &$diff) |
| 144 |
{ |
| 145 |
if ($a === $b) { |
| 146 |
return; |
| 147 |
} |
| 148 |
if (\is_array($a)) { |
| 149 |
$b = (array) $b; |
| 150 |
$keys = \array_unique(\array_merge(\array_keys($a), \array_keys($b))); |
| 151 |
foreach ($keys as $k) { |
| 152 |
if (!\array_key_exists($k, $a)) { |
| 153 |
$this->compareArray(null, $b[$k], "{$path}.{$k}", $diff); |
| 154 |
} elseif (!\array_key_exists($k, $b)) { |
| 155 |
$this->compareArray($a[$k], null, "{$path}.{$k}", $diff); |
| 156 |
} else { |
| 157 |
$this->compareArray($a[$k], $b[$k], "{$path}.{$k}", $diff); |
| 158 |
} |
| 159 |
} |
| 160 |
} elseif ($a !== null && $b === null) { |
| 161 |
$diff[] = "{$path} was unset"; |
| 162 |
} elseif ($a === null && $b !== null) { |
| 163 |
$diff[] = \sprintf("%s was set to %s", $path, $this->str($b)); |
| 164 |
} else { |
| 165 |
$diff[] = \sprintf("%s changed from %s to %s", $path, $this->str($a), $this->str($b)); |
| 166 |
} |
| 167 |
} |
| 168 |
private function str($value) |
| 169 |
{ |
| 170 |
if (\is_scalar($value)) { |
| 171 |
return (string) $value; |
| 172 |
} |
| 173 |
if ($value instanceof \Exception) { |
| 174 |
$value = $this->exceptionArray($value); |
| 175 |
} |
| 176 |
\ob_start(); |
| 177 |
\var_dump($value); |
| 178 |
return \ob_get_clean(); |
| 179 |
} |
| 180 |
private function streamStr(StreamInterface $body) |
| 181 |
{ |
| 182 |
return $body->getSize() < $this->config['stream_size'] ? (string) $body : 'stream(size=' . $body->getSize() . ')'; |
| 183 |
} |
| 184 |
private function createHttpDebug(CommandInterface $command) |
| 185 |
{ |
| 186 |
if ($this->config['http'] && !isset($command['@http']['debug'])) { |
| 187 |
$command['@http']['debug'] = \fopen('php://temp', 'w+'); |
| 188 |
} |
| 189 |
} |
| 190 |
private function flushHttpDebug(CommandInterface $command) |
| 191 |
{ |
| 192 |
if ($res = $command['@http']['debug']) { |
| 193 |
if (\is_resource($res)) { |
| 194 |
\rewind($res); |
| 195 |
$this->write(\stream_get_contents($res)); |
| 196 |
\fclose($res); |
| 197 |
} |
| 198 |
$command['@http']['debug'] = null; |
| 199 |
} |
| 200 |
} |
| 201 |
private function write($value) |
| 202 |
{ |
| 203 |
if ($this->config['scrub_auth']) { |
| 204 |
foreach ($this->config['auth_strings'] as $pattern => $replacement) { |
| 205 |
$value = \preg_replace_callback($pattern, function ($matches) use($replacement) { |
| 206 |
return $replacement; |
| 207 |
}, $value); |
| 208 |
} |
| 209 |
} |
| 210 |
\call_user_func($this->config['logfn'], $value); |
| 211 |
} |
| 212 |
private function redactHeaders(array $headers) |
| 213 |
{ |
| 214 |
if ($this->config['scrub_auth']) { |
| 215 |
$headers = $this->config['auth_headers'] + $headers; |
| 216 |
} |
| 217 |
return $headers; |
| 218 |
} |
| 219 |
/** |
| 220 |
* @param CommandInterface $cmd |
| 221 |
* @return array |
| 222 |
*/ |
| 223 |
private function getRedactedArray(CommandInterface $cmd) |
| 224 |
{ |
| 225 |
if (!isset($this->service["shapes"])) { |
| 226 |
return $cmd->toArray(); |
| 227 |
} |
| 228 |
$shapes = $this->service["shapes"]; |
| 229 |
$cmdArray = $cmd->toArray(); |
| 230 |
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($cmdArray), RecursiveIteratorIterator::SELF_FIRST); |
| 231 |
foreach ($iterator as $parameter => $value) { |
| 232 |
if (isset($shapes[$parameter]['sensitive']) && $shapes[$parameter]['sensitive'] === \true) { |
| 233 |
$redactedValue = \is_string($value) ? "[{$parameter}]" : ["[{$parameter}]"]; |
| 234 |
$currentDepth = $iterator->getDepth(); |
| 235 |
for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) { |
| 236 |
$subIterator = $iterator->getSubIterator($subDepth); |
| 237 |
$subIterator->offsetSet($subIterator->key(), $subDepth === $currentDepth ? $redactedValue : $iterator->getSubIterator($subDepth + 1)->getArrayCopy()); |
| 238 |
} |
| 239 |
} |
| 240 |
} |
| 241 |
return $iterator->getArrayCopy(); |
| 242 |
} |
| 243 |
} |
| 244 |
|