| 1 |
<?php |
| 2 |
|
| 3 |
namespace Dudlewebs\WPMCS\s3\Aws; |
| 4 |
|
| 5 |
use Dudlewebs\WPMCS\s3\Aws\Api\Service; |
| 6 |
use Dudlewebs\WPMCS\s3\Aws\Api\Validator; |
| 7 |
use Dudlewebs\WPMCS\s3\Aws\Credentials\CredentialsInterface; |
| 8 |
use Dudlewebs\WPMCS\s3\Aws\EndpointV2\EndpointProviderV2; |
| 9 |
use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException; |
| 10 |
use Dudlewebs\WPMCS\s3\Aws\Signature\S3ExpressSignature; |
| 11 |
use Dudlewebs\WPMCS\s3\Aws\Token\TokenAuthorization; |
| 12 |
use Dudlewebs\WPMCS\s3\Aws\Token\TokenInterface; |
| 13 |
use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise; |
| 14 |
use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7; |
| 15 |
use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7\LazyOpenStream; |
| 16 |
use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface; |
| 17 |
final class Middleware |
| 18 |
{ |
| 19 |
/** |
| 20 |
* Middleware used to allow a command parameter (e.g., "SourceFile") to |
| 21 |
* be used to specify the source of data for an upload operation. |
| 22 |
* |
| 23 |
* @param Service $api |
| 24 |
* @param string $bodyParameter |
| 25 |
* @param string $sourceParameter |
| 26 |
* |
| 27 |
* @return callable |
| 28 |
*/ |
| 29 |
public static function sourceFile(Service $api, $bodyParameter = 'Body', $sourceParameter = 'SourceFile') |
| 30 |
{ |
| 31 |
return function (callable $handler) use($api, $bodyParameter, $sourceParameter) { |
| 32 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, $api, $bodyParameter, $sourceParameter) { |
| 33 |
$operation = $api->getOperation($command->getName()); |
| 34 |
$source = $command[$sourceParameter]; |
| 35 |
if ($source !== null && $operation->getInput()->hasMember($bodyParameter)) { |
| 36 |
$lazyOpenStream = new LazyOpenStream($source, 'r'); |
| 37 |
$command[$bodyParameter] = $lazyOpenStream; |
| 38 |
unset($command[$sourceParameter]); |
| 39 |
$next = $handler($command, $request); |
| 40 |
// To avoid failures in some tests cases |
| 41 |
if ($next !== null && \method_exists($next, 'then')) { |
| 42 |
return $next->then(function ($result) use($lazyOpenStream) { |
| 43 |
// To make sure the resource is closed. |
| 44 |
$lazyOpenStream->close(); |
| 45 |
return $result; |
| 46 |
})->otherwise(function (\Throwable $e) use($lazyOpenStream) { |
| 47 |
$lazyOpenStream->close(); |
| 48 |
throw $e; |
| 49 |
}); |
| 50 |
} |
| 51 |
return $next; |
| 52 |
} |
| 53 |
return $handler($command, $request); |
| 54 |
}; |
| 55 |
}; |
| 56 |
} |
| 57 |
/** |
| 58 |
* Adds a middleware that uses client-side validation. |
| 59 |
* |
| 60 |
* @param Service $api API being accessed. |
| 61 |
* |
| 62 |
* @return callable |
| 63 |
*/ |
| 64 |
public static function validation(Service $api, ?Validator $validator = null) |
| 65 |
{ |
| 66 |
$validator = $validator ?: new Validator(); |
| 67 |
return function (callable $handler) use($api, $validator) { |
| 68 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($api, $validator, $handler) { |
| 69 |
if ($api->isModifiedModel()) { |
| 70 |
$api = new Service($api->getDefinition(), $api->getProvider()); |
| 71 |
} |
| 72 |
$operation = $api->getOperation($command->getName()); |
| 73 |
$validator->validate($command->getName(), $operation->getInput(), $command->toArray()); |
| 74 |
return $handler($command, $request); |
| 75 |
}; |
| 76 |
}; |
| 77 |
} |
| 78 |
/** |
| 79 |
* Builds an HTTP request for a command. |
| 80 |
* |
| 81 |
* @param callable $serializer Function used to serialize a request for a |
| 82 |
* command. |
| 83 |
* @param EndpointProviderV2 | null $endpointProvider |
| 84 |
* @param array $providerArgs |
| 85 |
* @return callable |
| 86 |
*/ |
| 87 |
public static function requestBuilder($serializer) |
| 88 |
{ |
| 89 |
return function (callable $handler) use($serializer) { |
| 90 |
return function (CommandInterface $command, $endpoint = null) use($serializer, $handler) { |
| 91 |
return $handler($command, $serializer($command, $endpoint)); |
| 92 |
}; |
| 93 |
}; |
| 94 |
} |
| 95 |
/** |
| 96 |
* Creates a middleware that signs requests for a command. |
| 97 |
* |
| 98 |
* @param callable $credProvider Credentials provider function that |
| 99 |
* returns a promise that is resolved |
| 100 |
* with a CredentialsInterface object. |
| 101 |
* @param callable $signatureFunction Function that accepts a Command |
| 102 |
* object and returns a |
| 103 |
* SignatureInterface. |
| 104 |
* |
| 105 |
* @return callable |
| 106 |
*/ |
| 107 |
public static function signer(callable $credProvider, callable $signatureFunction, $tokenProvider = null, $config = []) |
| 108 |
{ |
| 109 |
return function (callable $handler) use($signatureFunction, $credProvider, $tokenProvider, $config) { |
| 110 |
return function (CommandInterface $command, RequestInterface $request) use($handler, $signatureFunction, $credProvider, $tokenProvider, $config) { |
| 111 |
$signer = $signatureFunction($command); |
| 112 |
if ($signer instanceof TokenAuthorization) { |
| 113 |
return $tokenProvider()->then(function (TokenInterface $token) use($handler, $command, $signer, $request) { |
| 114 |
$command->getMetricsBuilder()->identifyMetricByValueAndAppend('token', $token); |
| 115 |
return $handler($command, $signer->authorizeRequest($request, $token)); |
| 116 |
}); |
| 117 |
} |
| 118 |
if ($signer instanceof S3ExpressSignature) { |
| 119 |
$credentialPromise = $config['s3_express_identity_provider']($command); |
| 120 |
} else { |
| 121 |
$credentialPromise = $credProvider(); |
| 122 |
} |
| 123 |
return $credentialPromise->then(function (CredentialsInterface $creds) use($handler, $command, $signer, $request) { |
| 124 |
// Capture credentials metric |
| 125 |
$command->getMetricsBuilder()->identifyMetricByValueAndAppend('credentials', $creds); |
| 126 |
return $handler($command, $signer->signRequest($request, $creds)); |
| 127 |
}); |
| 128 |
}; |
| 129 |
}; |
| 130 |
} |
| 131 |
/** |
| 132 |
* Creates a middleware that invokes a callback at a given step. |
| 133 |
* |
| 134 |
* The tap callback accepts a CommandInterface and RequestInterface as |
| 135 |
* arguments but is not expected to return a new value or proxy to |
| 136 |
* downstream middleware. It's simply a way to "tap" into the handler chain |
| 137 |
* to debug or get an intermediate value. |
| 138 |
* |
| 139 |
* @param callable $fn Tap function |
| 140 |
* |
| 141 |
* @return callable |
| 142 |
*/ |
| 143 |
public static function tap(callable $fn) |
| 144 |
{ |
| 145 |
return function (callable $handler) use($fn) { |
| 146 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, $fn) { |
| 147 |
$fn($command, $request); |
| 148 |
return $handler($command, $request); |
| 149 |
}; |
| 150 |
}; |
| 151 |
} |
| 152 |
/** |
| 153 |
* Middleware wrapper function that retries requests based on the boolean |
| 154 |
* result of invoking the provided "decider" function. |
| 155 |
* |
| 156 |
* If no delay function is provided, a simple implementation of exponential |
| 157 |
* backoff will be utilized. |
| 158 |
* |
| 159 |
* @param callable $decider Function that accepts the number of retries, |
| 160 |
* a request, [result], and [exception] and |
| 161 |
* returns true if the command is to be retried. |
| 162 |
* @param callable $delay Function that accepts the number of retries and |
| 163 |
* returns the number of milliseconds to delay. |
| 164 |
* @param bool $stats Whether to collect statistics on retries and the |
| 165 |
* associated delay. |
| 166 |
* |
| 167 |
* @return callable |
| 168 |
*/ |
| 169 |
public static function retry(?callable $decider = null, ?callable $delay = null, $stats = \false) |
| 170 |
{ |
| 171 |
$decider = $decider ?: RetryMiddleware::createDefaultDecider(); |
| 172 |
$delay = $delay ?: [RetryMiddleware::class, 'exponentialDelay']; |
| 173 |
return function (callable $handler) use($decider, $delay, $stats) { |
| 174 |
return new RetryMiddleware($decider, $delay, $handler, $stats); |
| 175 |
}; |
| 176 |
} |
| 177 |
/** |
| 178 |
* Middleware wrapper function that adds an invocation id header to |
| 179 |
* requests, which is only applied after the build step. |
| 180 |
* |
| 181 |
* This is a uniquely generated UUID to identify initial and subsequent |
| 182 |
* retries as part of a complete request lifecycle. |
| 183 |
* |
| 184 |
* @return callable |
| 185 |
*/ |
| 186 |
public static function invocationId() |
| 187 |
{ |
| 188 |
return function (callable $handler) { |
| 189 |
return function (CommandInterface $command, RequestInterface $request) use($handler) { |
| 190 |
return $handler($command, $request->withHeader('aws-sdk-invocation-id', \md5(\uniqid(\gethostname(), \true)))); |
| 191 |
}; |
| 192 |
}; |
| 193 |
} |
| 194 |
/** |
| 195 |
* Middleware wrapper function that adds a Content-Type header to requests. |
| 196 |
* This is only done when the Content-Type has not already been set, and the |
| 197 |
* request body's URI is available. It then checks the file extension of the |
| 198 |
* URI to determine the mime-type. |
| 199 |
* |
| 200 |
* @param array $operations Operations that Content-Type should be added to. |
| 201 |
* |
| 202 |
* @return callable |
| 203 |
*/ |
| 204 |
public static function contentType(array $operations) |
| 205 |
{ |
| 206 |
return function (callable $handler) use($operations) { |
| 207 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, $operations) { |
| 208 |
if (!$request->hasHeader('Content-Type') && \in_array($command->getName(), $operations, \true) && ($uri = $request->getBody()->getMetadata('uri'))) { |
| 209 |
$request = $request->withHeader('Content-Type', Psr7\MimeType::fromFilename($uri) ?: 'application/octet-stream'); |
| 210 |
} |
| 211 |
return $handler($command, $request); |
| 212 |
}; |
| 213 |
}; |
| 214 |
} |
| 215 |
/** |
| 216 |
* Middleware wrapper function that adds a trace id header to requests |
| 217 |
* from clients instantiated in supported Lambda runtime environments. |
| 218 |
* |
| 219 |
* The purpose for this header is to track and stop Lambda functions |
| 220 |
* from being recursively invoked due to misconfigured resources. |
| 221 |
* |
| 222 |
* @return callable |
| 223 |
*/ |
| 224 |
public static function recursionDetection() |
| 225 |
{ |
| 226 |
return function (callable $handler) { |
| 227 |
return function (CommandInterface $command, RequestInterface $request) use($handler) { |
| 228 |
$isLambda = \getenv('AWS_LAMBDA_FUNCTION_NAME'); |
| 229 |
$traceId = \str_replace('\\e', '\\x1b', \getenv('_X_AMZN_TRACE_ID')); |
| 230 |
if ($isLambda && $traceId) { |
| 231 |
if (!$request->hasHeader('X-Amzn-Trace-Id')) { |
| 232 |
$ignoreChars = ['=', ';', ':', '+', '&', '[', ']', '{', '}', '"', '\'', ',']; |
| 233 |
$traceIdEncoded = \rawurlencode(\stripcslashes($traceId)); |
| 234 |
foreach ($ignoreChars as $char) { |
| 235 |
$encodedChar = \rawurlencode($char); |
| 236 |
$traceIdEncoded = \str_replace($encodedChar, $char, $traceIdEncoded); |
| 237 |
} |
| 238 |
return $handler($command, $request->withHeader('X-Amzn-Trace-Id', $traceIdEncoded)); |
| 239 |
} |
| 240 |
} |
| 241 |
return $handler($command, $request); |
| 242 |
}; |
| 243 |
}; |
| 244 |
} |
| 245 |
/** |
| 246 |
* Tracks command and request history using a history container. |
| 247 |
* |
| 248 |
* This is useful for testing. |
| 249 |
* |
| 250 |
* @param History $history History container to store entries. |
| 251 |
* |
| 252 |
* @return callable |
| 253 |
*/ |
| 254 |
public static function history(History $history) |
| 255 |
{ |
| 256 |
return function (callable $handler) use($history) { |
| 257 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, $history) { |
| 258 |
$ticket = $history->start($command, $request); |
| 259 |
return $handler($command, $request)->then(function ($result) use($history, $ticket) { |
| 260 |
$history->finish($ticket, $result); |
| 261 |
return $result; |
| 262 |
}, function ($reason) use($history, $ticket) { |
| 263 |
$history->finish($ticket, $reason); |
| 264 |
return Promise\Create::rejectionFor($reason); |
| 265 |
}); |
| 266 |
}; |
| 267 |
}; |
| 268 |
} |
| 269 |
/** |
| 270 |
* Creates a middleware that applies a map function to requests as they |
| 271 |
* pass through the middleware. |
| 272 |
* |
| 273 |
* @param callable $f Map function that accepts a RequestInterface and |
| 274 |
* returns a RequestInterface. |
| 275 |
* |
| 276 |
* @return callable |
| 277 |
*/ |
| 278 |
public static function mapRequest(callable $f) |
| 279 |
{ |
| 280 |
return function (callable $handler) use($f) { |
| 281 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, $f) { |
| 282 |
return $handler($command, $f($request)); |
| 283 |
}; |
| 284 |
}; |
| 285 |
} |
| 286 |
/** |
| 287 |
* Creates a middleware that applies a map function to commands as they |
| 288 |
* pass through the middleware. |
| 289 |
* |
| 290 |
* @param callable $f Map function that accepts a command and returns a |
| 291 |
* command. |
| 292 |
* |
| 293 |
* @return callable |
| 294 |
*/ |
| 295 |
public static function mapCommand(callable $f) |
| 296 |
{ |
| 297 |
return function (callable $handler) use($f) { |
| 298 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, $f) { |
| 299 |
return $handler($f($command), $request); |
| 300 |
}; |
| 301 |
}; |
| 302 |
} |
| 303 |
/** |
| 304 |
* Creates a middleware that applies a map function to results. |
| 305 |
* |
| 306 |
* @param callable $f Map function that accepts an Aws\ResultInterface and |
| 307 |
* returns an Aws\ResultInterface. |
| 308 |
* |
| 309 |
* @return callable |
| 310 |
*/ |
| 311 |
public static function mapResult(callable $f) |
| 312 |
{ |
| 313 |
return function (callable $handler) use($f) { |
| 314 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, $f) { |
| 315 |
return $handler($command, $request)->then($f); |
| 316 |
}; |
| 317 |
}; |
| 318 |
} |
| 319 |
public static function timer() |
| 320 |
{ |
| 321 |
return function (callable $handler) { |
| 322 |
return function (CommandInterface $command, ?RequestInterface $request = null) use($handler) { |
| 323 |
$start = \microtime(\true); |
| 324 |
return $handler($command, $request)->then(function (ResultInterface $res) use($start) { |
| 325 |
if (!isset($res['@metadata'])) { |
| 326 |
$res['@metadata'] = []; |
| 327 |
} |
| 328 |
if (!isset($res['@metadata']['transferStats'])) { |
| 329 |
$res['@metadata']['transferStats'] = []; |
| 330 |
} |
| 331 |
$res['@metadata']['transferStats']['total_time'] = \microtime(\true) - $start; |
| 332 |
return $res; |
| 333 |
}, function ($err) use($start) { |
| 334 |
if ($err instanceof AwsException) { |
| 335 |
$err->setTransferInfo(['total_time' => \microtime(\true) - $start] + $err->getTransferInfo()); |
| 336 |
} |
| 337 |
return Promise\Create::rejectionFor($err); |
| 338 |
}); |
| 339 |
}; |
| 340 |
}; |
| 341 |
} |
| 342 |
} |
| 343 |
|