Issue with SSL Self-Signed Certificate Verification during Elasticsearch Test cURL error 60: SSL certificate problem:

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:

  1. Configure Elasticsearch with a signed SSL certificate.
  2. Modify the ClientBuilder.php file to disable SSL verification.
  3. 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.