1: | <?php |
2: | |
3: | declare(strict_types=1); |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | namespace OpenSearch; |
23: | |
24: | use Aws\Credentials\CredentialProvider; |
25: | use Aws\Credentials\Credentials; |
26: | use Aws\Credentials\CredentialsInterface; |
27: | use GuzzleHttp\Ring\Client\CurlHandler; |
28: | use GuzzleHttp\Ring\Client\CurlMultiHandler; |
29: | use GuzzleHttp\Ring\Client\Middleware; |
30: | use OpenSearch\Common\Exceptions\AuthenticationConfigException; |
31: | use OpenSearch\Common\Exceptions\InvalidArgumentException; |
32: | use OpenSearch\Common\Exceptions\RuntimeException; |
33: | use OpenSearch\ConnectionPool\AbstractConnectionPool; |
34: | use OpenSearch\ConnectionPool\Selectors\RoundRobinSelector; |
35: | use OpenSearch\ConnectionPool\Selectors\SelectorInterface; |
36: | use OpenSearch\ConnectionPool\StaticNoPingConnectionPool; |
37: | use OpenSearch\Connections\ConnectionFactory; |
38: | use OpenSearch\Connections\ConnectionFactoryInterface; |
39: | use OpenSearch\Connections\ConnectionInterface; |
40: | use OpenSearch\Handlers\SigV4Handler; |
41: | use OpenSearch\Namespaces\NamespaceBuilderInterface; |
42: | use OpenSearch\Serializers\SerializerInterface; |
43: | use OpenSearch\Serializers\SmartSerializer; |
44: | use Psr\Log\LoggerInterface; |
45: | use Psr\Log\NullLogger; |
46: | use ReflectionClass; |
47: | |
48: | class ClientBuilder |
49: | { |
50: | public const ALLOWED_METHODS_FROM_CONFIG = ['includePortInHostHeader']; |
51: | |
52: | |
53: | |
54: | |
55: | private $transport; |
56: | |
57: | private ?EndpointFactoryInterface $endpointFactory = null; |
58: | |
59: | |
60: | |
61: | |
62: | private $registeredNamespacesBuilders = []; |
63: | |
64: | |
65: | |
66: | |
67: | private $connectionFactory; |
68: | |
69: | |
70: | |
71: | |
72: | private $handler; |
73: | |
74: | |
75: | |
76: | |
77: | private $logger; |
78: | |
79: | |
80: | |
81: | |
82: | private $tracer; |
83: | |
84: | |
85: | |
86: | |
87: | private $connectionPool = StaticNoPingConnectionPool::class; |
88: | |
89: | |
90: | |
91: | |
92: | private $serializer = SmartSerializer::class; |
93: | |
94: | |
95: | |
96: | |
97: | private $selector = RoundRobinSelector::class; |
98: | |
99: | |
100: | |
101: | |
102: | private $connectionPoolArgs = [ |
103: | 'randomizeHosts' => true |
104: | ]; |
105: | |
106: | |
107: | |
108: | |
109: | private $hosts; |
110: | |
111: | |
112: | |
113: | |
114: | private $connectionParams; |
115: | |
116: | |
117: | |
118: | |
119: | private $retries; |
120: | |
121: | |
122: | |
123: | |
124: | private $sigV4CredentialProvider; |
125: | |
126: | |
127: | |
128: | |
129: | private $sigV4Region; |
130: | |
131: | |
132: | |
133: | |
134: | private $sigV4Service; |
135: | |
136: | |
137: | |
138: | |
139: | private $sniffOnStart = false; |
140: | |
141: | |
142: | |
143: | |
144: | private $sslCert; |
145: | |
146: | |
147: | |
148: | |
149: | private $sslKey; |
150: | |
151: | |
152: | |
153: | |
154: | private $sslVerification; |
155: | |
156: | |
157: | |
158: | |
159: | private $includePortInHostHeader = false; |
160: | |
161: | |
162: | |
163: | |
164: | private $basicAuthentication = null; |
165: | |
166: | |
167: | |
168: | |
169: | public static function create(): ClientBuilder |
170: | { |
171: | return new self(); |
172: | } |
173: | |
174: | |
175: | |
176: | |
177: | public function getTransport(): Transport |
178: | { |
179: | return $this->transport; |
180: | } |
181: | |
182: | |
183: | |
184: | |
185: | |
186: | |
187: | public function getEndpoint(): callable |
188: | { |
189: | @trigger_error(__METHOD__ . '() is deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\ClientBuilder::getEndpointFactory() instead.', E_USER_DEPRECATED); |
190: | return fn ($c) => $this->endpointFactory->getEndpoint('OpenSearch\\Endpoints\\' . $c); |
191: | } |
192: | |
193: | |
194: | |
195: | |
196: | |
197: | |
198: | public function getRegisteredNamespacesBuilders(): array |
199: | { |
200: | return $this->registeredNamespacesBuilders; |
201: | } |
202: | |
203: | |
204: | |
205: | |
206: | |
207: | |
208: | |
209: | |
210: | |
211: | |
212: | |
213: | |
214: | |
215: | |
216: | |
217: | |
218: | public static function fromConfig(array $config, bool $quiet = false): Client |
219: | { |
220: | $builder = new self(); |
221: | foreach ($config as $key => $value) { |
222: | $method = in_array($key, self::ALLOWED_METHODS_FROM_CONFIG, true) ? $key : "set$key"; |
223: | $reflection = new ReflectionClass($builder); |
224: | if ($reflection->hasMethod($method)) { |
225: | $func = $reflection->getMethod($method); |
226: | if ($func->getNumberOfParameters() > 1) { |
227: | $builder->$method(...$value); |
228: | } else { |
229: | $builder->$method($value); |
230: | } |
231: | unset($config[$key]); |
232: | } |
233: | } |
234: | |
235: | if ($quiet === false && count($config) > 0) { |
236: | $unknown = implode(array_keys($config)); |
237: | throw new RuntimeException("Unknown parameters provided: $unknown"); |
238: | } |
239: | return $builder->build(); |
240: | } |
241: | |
242: | |
243: | |
244: | |
245: | |
246: | |
247: | |
248: | |
249: | public static function defaultHandler(array $multiParams = [], array $singleParams = []): callable |
250: | { |
251: | $future = null; |
252: | if (extension_loaded('curl')) { |
253: | $config = array_merge([ 'mh' => curl_multi_init() ], $multiParams); |
254: | if (function_exists('curl_reset')) { |
255: | $default = new CurlHandler($singleParams); |
256: | $future = new CurlMultiHandler($config); |
257: | } else { |
258: | $default = new CurlMultiHandler($config); |
259: | } |
260: | } else { |
261: | throw new \RuntimeException('OpenSearch-PHP requires cURL, or a custom HTTP handler.'); |
262: | } |
263: | |
264: | return $future ? Middleware::wrapFuture($default, $future) : $default; |
265: | } |
266: | |
267: | |
268: | |
269: | |
270: | |
271: | |
272: | public static function multiHandler(array $params = []): CurlMultiHandler |
273: | { |
274: | if (function_exists('curl_multi_init')) { |
275: | return new CurlMultiHandler(array_merge([ 'mh' => curl_multi_init() ], $params)); |
276: | } |
277: | |
278: | throw new \RuntimeException('CurlMulti handler requires cURL.'); |
279: | } |
280: | |
281: | |
282: | |
283: | |
284: | |
285: | |
286: | public static function singleHandler(): CurlHandler |
287: | { |
288: | if (function_exists('curl_reset')) { |
289: | return new CurlHandler(); |
290: | } |
291: | |
292: | throw new \RuntimeException('CurlSingle handler requires cURL.'); |
293: | } |
294: | |
295: | |
296: | |
297: | |
298: | |
299: | |
300: | public function setConnectionFactory(ConnectionFactoryInterface $connectionFactory): ClientBuilder |
301: | { |
302: | $this->connectionFactory = $connectionFactory; |
303: | |
304: | return $this; |
305: | } |
306: | |
307: | |
308: | |
309: | |
310: | |
311: | |
312: | |
313: | |
314: | public function setConnectionPool($connectionPool, array $args = []): ClientBuilder |
315: | { |
316: | if (is_string($connectionPool)) { |
317: | $this->connectionPool = $connectionPool; |
318: | $this->connectionPoolArgs = $args; |
319: | } elseif (is_object($connectionPool)) { |
320: | $this->connectionPool = $connectionPool; |
321: | } else { |
322: | throw new InvalidArgumentException("Serializer must be a class path or instantiated object extending AbstractConnectionPool"); |
323: | } |
324: | |
325: | return $this; |
326: | } |
327: | |
328: | |
329: | |
330: | |
331: | |
332: | |
333: | |
334: | |
335: | public function setEndpoint(callable $endpoint): ClientBuilder |
336: | { |
337: | @trigger_error(__METHOD__ . '() is deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\ClientBuilder::setEndpointFactory() instead.', E_USER_DEPRECATED); |
338: | $this->endpointFactory = new LegacyEndpointFactory($endpoint); |
339: | |
340: | return $this; |
341: | } |
342: | |
343: | public function setEndpointFactory(EndpointFactoryInterface $endpointFactory): ClientBuilder |
344: | { |
345: | $this->endpointFactory = $endpointFactory; |
346: | return $this; |
347: | } |
348: | |
349: | |
350: | |
351: | |
352: | |
353: | |
354: | public function registerNamespace(NamespaceBuilderInterface $namespaceBuilder): ClientBuilder |
355: | { |
356: | $this->registeredNamespacesBuilders[] = $namespaceBuilder; |
357: | |
358: | return $this; |
359: | } |
360: | |
361: | |
362: | |
363: | |
364: | |
365: | |
366: | public function setTransport(Transport $transport): ClientBuilder |
367: | { |
368: | $this->transport = $transport; |
369: | |
370: | return $this; |
371: | } |
372: | |
373: | |
374: | |
375: | |
376: | |
377: | |
378: | public function setHandler($handler): ClientBuilder |
379: | { |
380: | $this->handler = $handler; |
381: | |
382: | return $this; |
383: | } |
384: | |
385: | |
386: | |
387: | |
388: | |
389: | |
390: | public function setLogger(LoggerInterface $logger): ClientBuilder |
391: | { |
392: | $this->logger = $logger; |
393: | |
394: | return $this; |
395: | } |
396: | |
397: | |
398: | |
399: | |
400: | |
401: | |
402: | public function setTracer(LoggerInterface $tracer): ClientBuilder |
403: | { |
404: | $this->tracer = $tracer; |
405: | |
406: | return $this; |
407: | } |
408: | |
409: | |
410: | |
411: | |
412: | |
413: | |
414: | public function setSerializer($serializer): ClientBuilder |
415: | { |
416: | $this->parseStringOrObject($serializer, $this->serializer, 'SerializerInterface'); |
417: | |
418: | return $this; |
419: | } |
420: | |
421: | |
422: | |
423: | |
424: | |
425: | |
426: | public function setHosts(array $hosts): ClientBuilder |
427: | { |
428: | $this->hosts = $hosts; |
429: | |
430: | return $this; |
431: | } |
432: | |
433: | |
434: | |
435: | |
436: | |
437: | |
438: | |
439: | |
440: | |
441: | |
442: | public function setBasicAuthentication(string $username, string $password): ClientBuilder |
443: | { |
444: | $this->basicAuthentication = $username.':'.$password; |
445: | |
446: | return $this; |
447: | } |
448: | |
449: | |
450: | |
451: | |
452: | |
453: | |
454: | public function setConnectionParams(array $params): ClientBuilder |
455: | { |
456: | $this->connectionParams = $params; |
457: | |
458: | return $this; |
459: | } |
460: | |
461: | |
462: | |
463: | |
464: | |
465: | |
466: | public function setRetries(int $retries): ClientBuilder |
467: | { |
468: | $this->retries = $retries; |
469: | |
470: | return $this; |
471: | } |
472: | |
473: | |
474: | |
475: | |
476: | |
477: | |
478: | public function setSelector($selector): ClientBuilder |
479: | { |
480: | $this->parseStringOrObject($selector, $this->selector, 'SelectorInterface'); |
481: | |
482: | return $this; |
483: | } |
484: | |
485: | |
486: | |
487: | |
488: | |
489: | |
490: | |
491: | public function setSigV4CredentialProvider($credentialProvider): ClientBuilder |
492: | { |
493: | if ($credentialProvider !== null && $credentialProvider !== false) { |
494: | $this->sigV4CredentialProvider = $this->normalizeCredentialProvider($credentialProvider); |
495: | } |
496: | |
497: | return $this; |
498: | } |
499: | |
500: | |
501: | |
502: | |
503: | |
504: | |
505: | public function setSigV4Region($region): ClientBuilder |
506: | { |
507: | $this->sigV4Region = $region; |
508: | |
509: | return $this; |
510: | } |
511: | |
512: | |
513: | |
514: | |
515: | |
516: | |
517: | public function setSigV4Service($service): ClientBuilder |
518: | { |
519: | $this->sigV4Service = $service; |
520: | |
521: | return $this; |
522: | } |
523: | |
524: | |
525: | |
526: | |
527: | |
528: | |
529: | |
530: | public function setSniffOnStart(bool $sniffOnStart): ClientBuilder |
531: | { |
532: | $this->sniffOnStart = $sniffOnStart; |
533: | |
534: | return $this; |
535: | } |
536: | |
537: | |
538: | |
539: | |
540: | |
541: | |
542: | |
543: | public function setSSLCert(string $cert, ?string $password = null): ClientBuilder |
544: | { |
545: | $this->sslCert = [$cert, $password]; |
546: | |
547: | return $this; |
548: | } |
549: | |
550: | |
551: | |
552: | |
553: | |
554: | |
555: | |
556: | public function setSSLKey(string $key, ?string $password = null): ClientBuilder |
557: | { |
558: | $this->sslKey = [$key, $password]; |
559: | |
560: | return $this; |
561: | } |
562: | |
563: | |
564: | |
565: | |
566: | |
567: | |
568: | public function setSSLVerification($value = true): ClientBuilder |
569: | { |
570: | $this->sslVerification = $value; |
571: | |
572: | return $this; |
573: | } |
574: | |
575: | |
576: | |
577: | |
578: | |
579: | |
580: | public function includePortInHostHeader(bool $enable): ClientBuilder |
581: | { |
582: | $this->includePortInHostHeader = $enable; |
583: | |
584: | return $this; |
585: | } |
586: | |
587: | |
588: | |
589: | |
590: | public function build(): Client |
591: | { |
592: | $this->buildLoggers(); |
593: | |
594: | if (is_null($this->handler)) { |
595: | $this->handler = ClientBuilder::defaultHandler(); |
596: | } |
597: | |
598: | if (!is_null($this->sigV4CredentialProvider)) { |
599: | if (is_null($this->sigV4Region)) { |
600: | throw new RuntimeException("A region must be supplied for SigV4 request signing."); |
601: | } |
602: | |
603: | if (is_null($this->sigV4Service)) { |
604: | $this->setSigV4Service("es"); |
605: | } |
606: | |
607: | $this->handler = new SigV4Handler($this->sigV4Region, $this->sigV4Service, $this->sigV4CredentialProvider, $this->handler); |
608: | } |
609: | |
610: | $sslOptions = null; |
611: | if (isset($this->sslKey)) { |
612: | $sslOptions['ssl_key'] = $this->sslKey; |
613: | } |
614: | if (isset($this->sslCert)) { |
615: | $sslOptions['cert'] = $this->sslCert; |
616: | } |
617: | if (isset($this->sslVerification)) { |
618: | $sslOptions['verify'] = $this->sslVerification; |
619: | } |
620: | |
621: | if (!is_null($sslOptions)) { |
622: | $sslHandler = function (callable $handler, array $sslOptions) { |
623: | return function (array $request) use ($handler, $sslOptions) { |
624: | |
625: | foreach ($sslOptions as $key => $value) { |
626: | $request['client'][$key] = $value; |
627: | } |
628: | |
629: | |
630: | return $handler($request); |
631: | }; |
632: | }; |
633: | $this->handler = $sslHandler($this->handler, $sslOptions); |
634: | } |
635: | |
636: | if (is_null($this->serializer)) { |
637: | $this->serializer = new SmartSerializer(); |
638: | } elseif (is_string($this->serializer)) { |
639: | $this->serializer = new $this->serializer(); |
640: | } |
641: | |
642: | $this->connectionParams['client']['port_in_header'] = $this->includePortInHostHeader; |
643: | |
644: | if (! is_null($this->basicAuthentication)) { |
645: | if (isset($this->connectionParams['client']['curl']) === false) { |
646: | $this->connectionParams['client']['curl'] = []; |
647: | } |
648: | |
649: | $this->connectionParams['client']['curl'] += [ |
650: | CURLOPT_HTTPAUTH => CURLAUTH_BASIC, |
651: | CURLOPT_USERPWD => $this->basicAuthentication |
652: | ]; |
653: | } |
654: | |
655: | if (is_null($this->connectionFactory)) { |
656: | |
657: | |
658: | if (! isset($this->connectionParams['client']['headers'])) { |
659: | $this->connectionParams['client']['headers'] = []; |
660: | } |
661: | if (! isset($this->connectionParams['client']['headers']['Content-Type'])) { |
662: | $this->connectionParams['client']['headers']['Content-Type'] = ['application/json']; |
663: | } |
664: | if (! isset($this->connectionParams['client']['headers']['Accept'])) { |
665: | $this->connectionParams['client']['headers']['Accept'] = ['application/json']; |
666: | } |
667: | |
668: | $this->connectionFactory = new ConnectionFactory($this->handler, $this->connectionParams, $this->serializer, $this->logger, $this->tracer); |
669: | } |
670: | |
671: | if (is_null($this->hosts)) { |
672: | $this->hosts = $this->getDefaultHost(); |
673: | } |
674: | |
675: | if (is_null($this->selector)) { |
676: | $this->selector = new RoundRobinSelector(); |
677: | } elseif (is_string($this->selector)) { |
678: | $this->selector = new $this->selector(); |
679: | } |
680: | |
681: | $this->buildTransport(); |
682: | |
683: | if (is_null($this->endpointFactory)) { |
684: | $this->endpointFactory = new EndpointFactory($this->serializer); |
685: | } |
686: | |
687: | $registeredNamespaces = []; |
688: | foreach ($this->registeredNamespacesBuilders as $builder) { |
689: | |
690: | |
691: | |
692: | $registeredNamespaces[$builder->getName()] = $builder->getObject($this->transport, $this->serializer); |
693: | } |
694: | |
695: | return $this->instantiate($this->transport, $this->endpointFactory, $registeredNamespaces); |
696: | } |
697: | |
698: | protected function instantiate(Transport $transport, EndpointFactoryInterface $endpointFactory, array $registeredNamespaces): Client |
699: | { |
700: | return new Client($transport, $endpointFactory, $registeredNamespaces); |
701: | } |
702: | |
703: | private function buildLoggers(): void |
704: | { |
705: | if (is_null($this->logger)) { |
706: | $this->logger = new NullLogger(); |
707: | } |
708: | |
709: | if (is_null($this->tracer)) { |
710: | $this->tracer = new NullLogger(); |
711: | } |
712: | } |
713: | |
714: | private function buildTransport(): void |
715: | { |
716: | $connections = $this->buildConnectionsFromHosts($this->hosts); |
717: | |
718: | if (is_string($this->connectionPool)) { |
719: | $this->connectionPool = new $this->connectionPool( |
720: | $connections, |
721: | $this->selector, |
722: | $this->connectionFactory, |
723: | $this->connectionPoolArgs |
724: | ); |
725: | } |
726: | |
727: | if (is_null($this->retries)) { |
728: | $this->retries = count($connections); |
729: | } |
730: | |
731: | if (is_null($this->transport)) { |
732: | $this->transport = new Transport($this->retries, $this->connectionPool, $this->logger, $this->sniffOnStart); |
733: | } |
734: | } |
735: | |
736: | private function parseStringOrObject($arg, &$destination, $interface): void |
737: | { |
738: | if (is_string($arg)) { |
739: | $destination = new $arg(); |
740: | } elseif (is_object($arg)) { |
741: | $destination = $arg; |
742: | } else { |
743: | throw new InvalidArgumentException("Serializer must be a class path or instantiated object implementing $interface"); |
744: | } |
745: | } |
746: | |
747: | private function getDefaultHost(): array |
748: | { |
749: | return ['localhost:9200']; |
750: | } |
751: | |
752: | |
753: | |
754: | |
755: | |
756: | private function buildConnectionsFromHosts(array $hosts): array |
757: | { |
758: | $connections = []; |
759: | foreach ($hosts as $host) { |
760: | if (is_string($host)) { |
761: | $host = $this->prependMissingScheme($host); |
762: | $host = $this->extractURIParts($host); |
763: | } elseif (is_array($host)) { |
764: | $host = $this->normalizeExtendedHost($host); |
765: | } else { |
766: | $this->logger->error("Could not parse host: ".print_r($host, true)); |
767: | throw new RuntimeException("Could not parse host: ".print_r($host, true)); |
768: | } |
769: | |
770: | $connections[] = $this->connectionFactory->create($host); |
771: | } |
772: | |
773: | return $connections; |
774: | } |
775: | |
776: | |
777: | |
778: | |
779: | private function normalizeExtendedHost(array $host): array |
780: | { |
781: | if (isset($host['host']) === false) { |
782: | $this->logger->error("Required 'host' was not defined in extended format: ".print_r($host, true)); |
783: | throw new RuntimeException("Required 'host' was not defined in extended format: ".print_r($host, true)); |
784: | } |
785: | |
786: | if (isset($host['scheme']) === false) { |
787: | $host['scheme'] = 'http'; |
788: | } |
789: | if (isset($host['port']) === false) { |
790: | $host['port'] = 9200; |
791: | } |
792: | return $host; |
793: | } |
794: | |
795: | |
796: | |
797: | |
798: | private function extractURIParts(string $host): array |
799: | { |
800: | $parts = parse_url($host); |
801: | |
802: | if ($parts === false) { |
803: | throw new InvalidArgumentException(sprintf('Could not parse URI: "%s"', $host)); |
804: | } |
805: | |
806: | if (isset($parts['port']) !== true) { |
807: | $parts['port'] = 9200; |
808: | } |
809: | |
810: | return $parts; |
811: | } |
812: | |
813: | private function prependMissingScheme(string $host): string |
814: | { |
815: | if (!preg_match("/^https?:\/\//", $host)) { |
816: | $host = 'http://' . $host; |
817: | } |
818: | |
819: | return $host; |
820: | } |
821: | |
822: | private function normalizeCredentialProvider($provider): ?callable |
823: | { |
824: | if ($provider === null || $provider === false) { |
825: | return null; |
826: | } |
827: | |
828: | if (is_callable($provider)) { |
829: | return $provider; |
830: | } |
831: | |
832: | SigV4Handler::assertDependenciesInstalled(); |
833: | |
834: | if ($provider === true) { |
835: | return CredentialProvider::defaultProvider(); |
836: | } |
837: | |
838: | if ($provider instanceof CredentialsInterface) { |
839: | return CredentialProvider::fromCredentials($provider); |
840: | } elseif (is_array($provider) && isset($provider['key']) && isset($provider['secret'])) { |
841: | return CredentialProvider::fromCredentials( |
842: | new Credentials( |
843: | $provider['key'], |
844: | $provider['secret'], |
845: | isset($provider['token']) ? $provider['token'] : null, |
846: | isset($provider['expires']) ? $provider['expires'] : null |
847: | ) |
848: | ); |
849: | } |
850: | |
851: | throw new InvalidArgumentException('Credentials must be an instance of Aws\Credentials\CredentialsInterface, an' |
852: | . ' associative array that contains "key", "secret", and an optional "token" key-value pairs, a credentials' |
853: | . ' provider function, or true.'); |
854: | } |
855: | } |
856: | |