1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace OpenSearch\Handlers;
6:
7: use Aws\Credentials\CredentialProvider;
8: use Aws\Signature\SignatureV4;
9: use GuzzleHttp\Psr7\Request;
10: use GuzzleHttp\Psr7\Uri;
11: use GuzzleHttp\Psr7\Utils;
12: use OpenSearch\ClientBuilder;
13: use Psr\Http\Message\RequestInterface;
14: use RuntimeException;
15:
16: /**
17: * @phpstan-type RingPhpRequest array{http_method: string, scheme: string, uri: string, query_string?: string, version?: string, headers: array<string, list<string>>, body: string|resource|null, client?: array<string, mixed>}
18: */
19: class SigV4Handler
20: {
21: /**
22: * @var SignatureV4
23: */
24: private $signer;
25: /**
26: * @var callable
27: */
28: private $credentialProvider;
29: /**
30: * @var callable
31: */
32: private $wrappedHandler;
33:
34: /**
35: * A handler that applies an AWS V4 signature before dispatching requests.
36: *
37: * @param string $region The region of your Amazon
38: * OpenSearch Service domain
39: * @param string $service The Service of your Amazon
40: * OpenSearch Service domain
41: * @param callable|null $credentialProvider A callable that returns a
42: * promise that is fulfilled
43: * with an instance of
44: * Aws\Credentials\Credentials
45: * @param callable|null $wrappedHandler A RingPHP handler
46: */
47: public function __construct(
48: string $region,
49: string $service,
50: ?callable $credentialProvider = null,
51: ?callable $wrappedHandler = null
52: ) {
53: self::assertDependenciesInstalled();
54: $this->signer = new SignatureV4($service, $region);
55: $this->wrappedHandler = $wrappedHandler
56: ?: ClientBuilder::defaultHandler();
57: $this->credentialProvider = $credentialProvider
58: ?: CredentialProvider::defaultProvider();
59: }
60:
61: /**
62: * @phpstan-param RingPhpRequest $request
63: */
64: public function __invoke(array $request)
65: {
66: $creds = call_user_func($this->credentialProvider)->wait();
67:
68: $psr7Request = $this->createPsr7Request($request);
69: $psr7Request = $psr7Request->withHeader('x-amz-content-sha256', Utils::hash($psr7Request->getBody(), 'sha256'));
70: $signedRequest = $this->signer
71: ->signRequest($psr7Request, $creds);
72: return call_user_func($this->wrappedHandler, $this->createRingRequest($signedRequest, $request));
73: }
74:
75: public static function assertDependenciesInstalled(): void
76: {
77: if (!class_exists(SignatureV4::class)) {
78: throw new RuntimeException(
79: 'The AWS SDK for PHP must be installed in order to use the SigV4 signing handler'
80: );
81: }
82: }
83:
84: /**
85: * @phpstan-param RingPhpRequest $ringPhpRequest
86: */
87: private function createPsr7Request(array $ringPhpRequest): Request
88: {
89: // fix for uppercase 'Host' array key in elasticsearch-php 5.3.1 and backward compatible
90: // https://github.com/aws/aws-sdk-php/issues/1225
91: $hostKey = isset($ringPhpRequest['headers']['Host']) ? 'Host' : 'host';
92:
93: // Amazon ES/OS listens on standard ports (443 for HTTPS, 80 for HTTP).
94: // Consequently, the port should be stripped from the host header.
95: $parsedUrl = parse_url($ringPhpRequest['headers'][$hostKey][0]);
96: if (isset($parsedUrl['host'])) {
97: $ringPhpRequest['headers'][$hostKey][0] = $parsedUrl['host'];
98: }
99:
100: // Create a PSR-7 URI from the array passed to the handler
101: $uri = (new Uri($ringPhpRequest['uri']))
102: ->withScheme($ringPhpRequest['scheme'])
103: ->withHost($ringPhpRequest['headers'][$hostKey][0]);
104: if (isset($ringPhpRequest['query_string'])) {
105: $uri = $uri->withQuery($ringPhpRequest['query_string']);
106: }
107:
108: // Create a PSR-7 request from the array passed to the handler
109: return new Request(
110: $ringPhpRequest['http_method'],
111: $uri,
112: $ringPhpRequest['headers'],
113: $ringPhpRequest['body']
114: );
115: }
116:
117: /**
118: * @phpstan-param RingPhpRequest $originalRequest
119: *
120: * @phpstan-return RingPhpRequest
121: */
122: private function createRingRequest(RequestInterface $request, array $originalRequest): array
123: {
124: $uri = $request->getUri();
125: $body = (string) $request->getBody();
126:
127: // RingPHP currently expects empty message bodies to be null:
128: // https://github.com/guzzle/RingPHP/blob/4c8fe4c48a0fb7cc5e41ef529e43fecd6da4d539/src/Client/CurlFactory.php#L202
129: if (empty($body)) {
130: $body = null;
131: }
132:
133: // Reset the explicit port in the URL
134: $client = $originalRequest['client'];
135: unset($client['curl'][CURLOPT_PORT]);
136:
137: $ringRequest = [
138: 'http_method' => $request->getMethod(),
139: 'scheme' => $uri->getScheme(),
140: 'uri' => $uri->getPath(),
141: 'body' => $body,
142: 'headers' => $request->getHeaders(),
143: 'client' => $client
144: ];
145: if ($uri->getQuery()) {
146: $ringRequest['query_string'] = $uri->getQuery();
147: }
148:
149: return $ringRequest;
150: }
151: }
152: