TraceMiddleware::__invoke()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 38
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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