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