diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 46ee93a16..955b5b79f 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -7,7 +7,7 @@ their needs, but it is possible to completely replace much of the internals if r Custom configuration is accomplished before the client is instantiated, through the ClientBuilder helper object. We'll walk through all the configuration options and show sample code to replace the various components. -=== Host Configuration +=== Inline Host Configuration The most common configuration is telling the client about your cluster: how many nodes, their addresses and ports. If no hosts are specified, the client will attempt to connect to `localhost:9200`. @@ -49,6 +49,44 @@ $clientBuilder->setHosts($hosts); // Set the hosts $client = $clientBuilder->build(); // Build the client object ---- +=== Extended Host Configuration + +The client also supports an _extended_ host configuration syntax. The inline configuration method relies on PHP's +`filter_var()` and `parse_url()` methods to validate and extract the components of a URL. Unfortunately, these built-in +methods run into problems with certain edge-cases. For example, `filter_var()` will not accept URL's that have underscores +(which are questionably legal, depending on how you interpret the RFCs). Similarly, `parse_url()` will choke if a +Basic Auth's password contains special characters such as a pound sign (`#`) or question-marks (`?`). + +For this reason, the client supports an extended host syntax which provides greater control over host initialization. +None of the components are validated, so edge-cases like underscores domain names will not cause problems. + +The extended syntax is an array of parameters for each host: + +[source,php] +---- +$hosts = [ + // This is effectively equal to: "https://username:password!#$?*abc@foo.com:9200/" + [ + 'host' => 'foo.com', + 'port' => '9200', + 'scheme' => 'https', + 'user' => 'username', + 'password' => 'password!#$?*abc' + ], + + // This is equal to "http://localhost:9200/" + [ + 'host' => 'localhost', // Only host is required + ] +]; +$client = ClientBuilder::create() // Instantiate a new ClientBuilder + ->setHosts($hosts) // Set the hosts + ->build(); // Build the client object +---- + +Only the `host` parameter is required for each configured host. If not provided, the default port is `9200`. The default +scheme is `http`. + === Authorization and Encryption For details about HTTP Authorization and SSL encryption, please see link:_security.html[Authorization and SSL]. diff --git a/src/Elasticsearch/ClientBuilder.php b/src/Elasticsearch/ClientBuilder.php index 24299eaf4..29a9c7588 100644 --- a/src/Elasticsearch/ClientBuilder.php +++ b/src/Elasticsearch/ClientBuilder.php @@ -518,19 +518,46 @@ private function getDefaultHost() private function buildConnectionsFromHosts($hosts) { if (is_array($hosts) === false) { - throw new InvalidArgumentException('Hosts parameter must be an array of strings'); + $this->logger->error("Hosts parameter must be an array of strings, or an array of Connection hashes."); + throw new InvalidArgumentException('Hosts parameter must be an array of strings, or an array of Connection hashes.'); } $connections = []; foreach ($hosts as $host) { - $host = $this->prependMissingScheme($host); - $host = $this->extractURIParts($host); + if (is_string($host)) { + $host = $this->prependMissingScheme($host); + $host = $this->extractURIParts($host); + } else if (is_array($host)) { + $host = $this->normalizeExtendedHost($host); + } else { + $this->logger->error("Could not parse host: ".print_r($host, true)); + throw new RuntimeException("Could not parse host: ".print_r($host, true)); + } $connections[] = $this->connectionFactory->create($host); } return $connections; } + /** + * @param $host + * @return array + */ + private function normalizeExtendedHost($host) { + if (isset($host['host']) === false) { + $this->logger->error("Required 'host' was not defined in extended format: ".print_r($host, true)); + throw new RuntimeException("Required 'host' was not defined in extended format: ".print_r($host, true)); + } + + if (isset($host['scheme']) === false) { + $host['scheme'] = 'http'; + } + if (isset($host['port']) === false) { + $host['port'] = '9200'; + } + return $host; + } + /** * @param array $host * diff --git a/tests/Elasticsearch/Tests/ClientTest.php b/tests/Elasticsearch/Tests/ClientTest.php index 01d203fe7..f89778d61 100644 --- a/tests/Elasticsearch/Tests/ClientTest.php +++ b/tests/Elasticsearch/Tests/ClientTest.php @@ -4,6 +4,7 @@ use Elasticsearch; use Elasticsearch\ClientBuilder; +use Elasticsearch\Connections\Connection; use Mockery as m; /** @@ -168,7 +169,7 @@ public function testArrayOfEmptyStringDelete() try { $client->delete([ - 'index' => ['','',''], + 'index' => ['', '', ''], 'type' => 'test', 'id' => 'test' ]); @@ -180,7 +181,7 @@ public function testArrayOfEmptyStringDelete() try { $client->delete([ 'index' => 'test', - 'type' => ['','',''], + 'type' => ['', '', ''], 'id' => 'test' ]); $this->fail("InvalidArgumentException was not thrown"); @@ -267,5 +268,171 @@ public function testMaxRetriesException() } } + public function testInlineHosts() + { + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + 'localhost:9200' + ])->build(); + + // We're casting to Connection here, instead of ConnectionInterface + // so we can access getHost() on Connection + + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("localhost:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + 'http://localhost:9200' + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("localhost:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + 'http://foo.com:9200' + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + 'https://foo.com:9200' + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("https", $host->getTransportSchema()); + + + // Note: we can't test user/pass themselves yet, need to introduce + // breaking change to interface in master to do that + // But we can confirm it doesn't break anything + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + 'https://user:pass@foo.com:9200' + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("https", $host->getTransportSchema()); + } + + public function testExtendedHosts() + { + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'localhost', + 'port' => 9200, + 'scheme' => 'http' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("localhost:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'foo.com', + 'port' => 9200, + 'scheme' => 'http' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'foo.com', + 'port' => 9200, + 'scheme' => 'https' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("https", $host->getTransportSchema()); + + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'foo.com', + 'scheme' => 'http' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'foo.com' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'foo.com', + 'port' => 9500, + 'scheme' => 'https' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9500", $host->getHost()); + $this->assertEquals("https", $host->getTransportSchema()); + + + try { + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'port' => 9200, + 'scheme' => 'http' + ] + ])->build(); + $this->fail("Expected RuntimeException from missing host, none thrown"); + } catch (Elasticsearch\Common\Exceptions\RuntimeException $e) { + // good + } + + // Underscore host, questionably legal, but inline method would break + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'the_foo.com' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("the_foo.com:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + + // Special characters in user/pass, would break inline + $client = Elasticsearch\ClientBuilder::create()->setHosts([ + [ + 'host' => 'foo.com', + 'user' => 'user', + 'pass' => 'abc#$%!abc' + ] + ])->build(); + /** @var Connection $host */ + $host = $client->transport->getConnection(); + $this->assertEquals("foo.com:9200", $host->getHost()); + $this->assertEquals("http", $host->getTransportSchema()); + + } }