diff --git a/application/forms/ApiTransportForm.php b/application/forms/ApiTransportForm.php index 27c147baf..d588e63b9 100644 --- a/application/forms/ApiTransportForm.php +++ b/application/forms/ApiTransportForm.php @@ -8,8 +8,11 @@ use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; use Icinga\Web\Session; +use ipl\Validator\CallbackValidator; +use ipl\Validator\X509CertValidator; use ipl\Web\Common\CsrfCounterMeasure; use ipl\Web\Compat\CompatForm; +use Throwable; class ApiTransportForm extends CompatForm { @@ -34,6 +37,41 @@ protected function assemble() 'description' => t('Hostname or address of the Icinga master') ]); + $this->addElement('text', 'caPath', [ + 'required' => false, + 'label' => t('Verify Peer'), + 'description' => t('Path to a certificate file to verify the Icinga master\'s api certificate'), + 'placeholder' => t('Leave empty to disable peer verification'), + 'validators' => [new CallbackValidator(function (?string $value, CallbackValidator $validator) { + if (empty($value)) { + return true; + } + + if (! file_exists($value) || ! is_readable($value)) { + $validator->addMessage(t('The specified certificate file does not exist or is not readable')); + + return false; + } + + try { + $cert = file_get_contents($value); + } catch (Throwable $e) { + $validator->addMessage(t('Failed to read certificate file: %s', $e->getMessage())); + + return false; + } + + $x509Validator = new X509CertValidator(); + if (! $x509Validator->isValid($cert)) { + $validator->addMessages($x509Validator->getMessages()); + + return false; + } + + return true; + })] + ]); + // TODO: Don't rely only on browser validation $this->addElement('number', 'port', [ 'required' => true, diff --git a/library/Icingadb/Command/Transport/ApiCommandTransport.php b/library/Icingadb/Command/Transport/ApiCommandTransport.php index 6d9433d6f..964891e50 100644 --- a/library/Icingadb/Command/Transport/ApiCommandTransport.php +++ b/library/Icingadb/Command/Transport/ApiCommandTransport.php @@ -34,6 +34,13 @@ class ApiCommandTransport implements CommandTransportInterface */ protected $host; + /** + * API Ca Path + * + * @var ?string + */ + protected $caPath; + /** * API password * @@ -114,6 +121,30 @@ public function setHost(string $host): self return $this; } + /** + * Get the API Ca Path + * + * @return ?string + */ + public function getCaPath(): ?string + { + return $this->caPath; + } + + /** + * Set the API Ca Path + * + * @param ?string $caPath + * + * @return $this + */ + public function setCaPath(?string $caPath): self + { + $this->caPath = $caPath; + + return $this; + } + /** * Get the API password * @@ -243,10 +274,10 @@ protected function sendCommand(IcingaApiCommand $command) $response = (new Client(['timeout' => static::SEND_TIMEOUT])) ->post($this->getUriFor($command->getEndpoint()), [ 'auth' => [$this->getUsername(), $this->getPassword()], + 'verify' => $this->getCaPath() ?: false, 'headers' => $headers, 'json' => $command->getData(), - 'http_errors' => false, - 'verify' => false + 'http_errors' => false ]); } catch (GuzzleException $e) { if (str_starts_with(ltrim($e->getMessage()), 'cURL error 28:')) { @@ -324,9 +355,9 @@ public function probe() $response = (new Client(['timeout' => static::SEND_TIMEOUT])) ->get($this->getUriFor(''), [ 'auth' => [$this->getUsername(), $this->getPassword()], + 'verify' => $this->getCaPath() ?: false, 'headers' => ['Accept' => 'application/json'], - 'http_errors' => false, - 'verify' => false + 'http_errors' => false ]); } catch (GuzzleException $e) { throw new CommandTransportException( diff --git a/library/Icingadb/Command/Transport/CommandTransportConfig.php b/library/Icingadb/Command/Transport/CommandTransportConfig.php index e17fa0413..8a41cef0b 100644 --- a/library/Icingadb/Command/Transport/CommandTransportConfig.php +++ b/library/Icingadb/Command/Transport/CommandTransportConfig.php @@ -24,6 +24,7 @@ class CommandTransportConfig extends IniRepository // API options 'host', 'port', + 'caPath', 'username', 'password' ]