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