Description:
I’m encountering an issue while running the php occ fulltextsearch:test
command involving an Elasticsearch instance with a signed SSL certificate ( Common Name (CN):GoGetSSL RSA DV CA
Organization (O):GoGetSSL).
The error I’m receiving is as follows:
Testing your current setup:
Creating mocked content provider. ok
Testing mocked provider: get indexable documents. (2 items) ok
Loading search platform. (Elasticsearch) ok
Testing search platform. fail
In CurlFactory.php line 146:
cURL error 60: SSL certificate problem: unable to get local issuer certificate (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)
I modified the ClientBuilder.php
file to disable SSL verification by setting sslVerification
to false
, but the error persists.
Steps to Reproduce the Issue:
- Configure Elasticsearch with a signed SSL certificate.
- Modify the
ClientBuilder.php
file to disable SSL verification. - Run the
php occ fulltextsearch:test
command.
Expected Result:
I expect the connection test to Elasticsearch to pass without SSL errors when SSL verification is disabled.
Actual Result:
The test fails with a cURL error related to the SSL certificate.
System Configuration:
- Elasticsearch Version: 8.6.1
fulltextsearch
app version: 28.0.3
Modified Code:
I modified the apps/fulltextsearch_elasticsearch/lib/Vendor/Elastic/Elasticsearch/ClientBuilder.php
file as follows:
<?php
declare(strict_types=1);
namespace OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch\Exception\AuthenticationException;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch\Exception\ConfigException;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch\Exception\HttpClientException;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch\Exception\InvalidArgumentException;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch\Transport\Adapter\AdapterInterface;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch\Transport\Adapter\AdapterOptions;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Elasticsearch\Transport\RequestOptions;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Transport\Exception\NoAsyncClientException;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Transport\NodePool\NodePoolInterface;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Transport\Transport;
use OCA\FullTextSearch_Elasticsearch\Vendor\Elastic\Transport\TransportBuilder;
use OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Client as GuzzleHttpClient;
use OCA\FullTextSearch_Elasticsearch\Vendor\Http\Client\HttpAsyncClient;
use OCA\FullTextSearch_Elasticsearch\Vendor\Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use ReflectionClass;
class ClientBuilder
{
const DEFAULT_HOST = 'localhost:9200';
private ClientInterface $httpClient;
private HttpAsyncClient $asyncHttpClient;
private LoggerInterface $logger;
private NodePoolInterface $nodePool;
private array $hosts;
private string $apiKey;
private string $username;
private string $password;
private string $cloudId;
private int $retries;
private array $sslCert;
private array $sslKey;
private bool $sslVerification = false;
private string $sslCA;
private bool $elasticMetaHeader = true;
private array $httpClientOptions = [];
public final function __construct()
{
}
public static function create() : ClientBuilder
{
return new static();
}
public static function fromConfig(array $config, bool $quiet = false) : Client
{
$builder = new static();
foreach ($config as $key => $value) {
$method = "set{$key}";
$reflection = new ReflectionClass($builder);
if ($reflection->hasMethod($method)) {
$func = $reflection->getMethod($method);
if ($func->getNumberOfParameters() > 1) {
$builder->{$method}(...$value);
} else {
$builder->{$method}($value);
}
unset($config[$key]);
}
}
if ($quiet === false && count($config) > 0) {
$unknown = implode(array_keys($config));
throw new ConfigException("Unknown parameters provided: {$unknown}");
}
return $builder->build();
}
public function setHttpClient(ClientInterface $httpClient) : ClientBuilder
{
$this->httpClient = $httpClient;
return $this;
}
public function setAsyncHttpClient(HttpAsyncClient $asyncHttpClient) : ClientBuilder
{
$this->asyncHttpClient = $asyncHttpClient;
return $this;
}
public function setLogger(LoggerInterface $logger) : ClientBuilder
{
$this->logger = $logger;
return $this;
}
public function setNodePool(NodePoolInterface $nodePool) : ClientBuilder
{
$this->nodePool = $nodePool;
return $this;
}
public function setHosts(array $hosts) : ClientBuilder
{
$this->hosts = $hosts;
return $this;
}
public function setApiKey(string $apiKey, string $id = null) : ClientBuilder
{
if (empty($id)) {
$this->apiKey = $apiKey;
} else {
$this->apiKey = base64_encode($id . ':' . $apiKey);
}
return $this;
}
public function setBasicAuthentication(string $username, string $password) : ClientBuilder
{
$this->username = $username;
$this->password = $password;
return $this;
}
public function setElasticCloudId(string $cloudId)
{
$this->cloudId = $cloudId;
return $this;
}
public function setRetries(int $retries) : ClientBuilder
{
if ($retries < 0) {
throw new InvalidArgumentException('The retries number must be >= 0');
}
$this->retries = $retries;
return $this;
}
public function setSSLCert(string $cert, string $password = null) : ClientBuilder
{
$this->sslCert = [$cert, $password];
return $this;
}
public function setCABundle(string $cert) : ClientBuilder
{
$this->sslCA = $cert;
return $this;
}
public function setSSLKey(string $key, string $password = null) : ClientBuilder
{
$this->sslKey = [$key, $password];
return $this;
}
public function setSSLVerification(bool $value = false) : ClientBuilder
{
$this->sslVerification = $value;
return $this;
}
public function setElasticMetaHeader(bool $value = true) : ClientBuilder
{
$this->elasticMetaHeader = $value;
return $this;
}
public function setHttpClientOptions(array $options) : ClientBuilder
{
$this->httpClientOptions = $options;
return $this;
}
public function build() : Client
{
$builder = TransportBuilder::create();
if (empty($this->hosts)) {
$this->hosts = [self::DEFAULT_HOST];
}
$builder->setHosts($this->hosts);
if (!empty($this->logger)) {
$builder->setLogger($this->logger);
}
if (!empty($this->httpClient)) {
$builder->setClient($this->httpClient);
}
$builder->setClient($this->setOptions($builder->getClient(), $this->getConfig(), $this->httpClientOptions));
if (!empty($this->cloudId)) {
$builder->setCloudId($this->cloudId);
}
if (!empty($this->nodePool)) {
$builder->setNodePool($this->nodePool);
}
$transport = $builder->build();
if (empty($this->retries)) {
$this->retries = count($this->hosts);
}
$transport->setRetries($this->retries);
if (!empty($this->asyncHttpClient)) {
$transport->setAsyncClient($this->asyncHttpClient);
}
if (!empty($this->username) && !empty($this->password)) {
$transport->setUserInfo($this->username, $this->password);
}
if (!empty($this->apiKey)) {
if (!empty($this->username)) {
throw new AuthenticationException('You cannot use APIKey and Basic Authentication together');
}
$transport->setHeader('Authorization', sprintf("ApiKey %s", $this->apiKey));
}
if (!empty($this
->cloudId) && !$this->isSymfonyHttpClient($transport)) {
$transport->setHeader('Accept-Encoding', 'gzip');
}
$client = new Client($transport, $transport->getLogger());
$client->setElasticMetaHeader($this->elasticMetaHeader);
return $client;
}
protected function isSymfonyHttpClient(Transport $transport) : bool
{
if (false !== strpos(get_class($transport->getClient()), 'OCA\\FullTextSearch_Elasticsearch\\Vendor\\Symfony\\Component\\HttpClient')) {
return true;
}
try {
if (false !== strpos(get_class($transport->getAsyncClient()), 'OCA\\FullTextSearch_Elasticsearch\\Vendor\\Symfony\\Component\\HttpClient')) {
return true;
}
} catch (NoAsyncClientException $e) {
return false;
}
return false;
}
protected function getConfig() : array
{
$config = [];
if (!empty($this->sslCert)) {
$config[RequestOptions::SSL_CERT] = $this->sslCert;
}
if (!empty($this->sslKey)) {
$config[RequestOptions::SSL_KEY] = $this->sslKey;
}
if (!$this->sslVerification) {
$config[RequestOptions::SSL_VERIFY] = true;
}
if (!empty($this->sslCA)) {
$config[RequestOptions::SSL_CA] = $this->sslCA;
}
return $config;
}
protected function setOptions(ClientInterface $client, array $config, array $clientOptions = []) : ClientInterface
{
if (empty($config) && empty($clientOptions)) {
return $client;
}
$class = get_class($client);
if (!isset(AdapterOptions::HTTP_ADAPTERS[$class])) {
throw new HttpClientException(sprintf("The HTTP client %s is not supported for custom options", $class));
}
$adapterClass = AdapterOptions::HTTP_ADAPTERS[$class];
if (!class_exists($adapterClass) || !in_array(AdapterInterface::class, class_implements($adapterClass))) {
throw new HttpClientException(sprintf("The class %s does not exists or does not implement %s", $adapterClass, AdapterInterface::class));
}
$adapter = new $adapterClass();
return $adapter->setConfig($client, $config, $clientOptions);
}
}
If possible, providing support for using signed SSL certificates in development environments would be very helpful.