TraceMiddleware.php 10 KB

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