From 3cbea5226719a09faedd2b15a183c7c1fad4e130 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Tue, 24 Dec 2024 13:54:59 +0100 Subject: [PATCH] System: High Availability - XMLRPC Client / replace file_get_contents() with curl implementation, closes https://github.com/opnsense/core/issues/7561 While here, also offer optional peer tls verification as this is/was disabled by default. In most cases verification isn't very relevant when using a direct attached neighbor, but if someone has infrastructure in between, extra safeguards are now possible. With this inplace, allow_url_fopen can safely be disabled on our end (which was the primary goal here). --- src/etc/inc/XMLRPC_Client.inc | 53 +++++++------- src/etc/rc.subr.d/recover | 2 +- .../OPNsense/Core/forms/hasyncSettings.xml | 6 ++ .../mvc/app/models/OPNsense/Core/Hasync.xml | 6 +- .../scripts/system/ha_xmlrpc_exec.php | 70 ++++++++++--------- .../service/templates/OPNsense/WebGui/php.ini | 2 +- 6 files changed, 79 insertions(+), 60 deletions(-) diff --git a/src/etc/inc/XMLRPC_Client.inc b/src/etc/inc/XMLRPC_Client.inc index 54ce5d169ff..d6711216470 100644 --- a/src/etc/inc/XMLRPC_Client.inc +++ b/src/etc/inc/XMLRPC_Client.inc @@ -72,6 +72,8 @@ function xmlrpc_execute($method, $params = [], $debug = false) $client->setCredentials($username, $hasync['password']); if ($client->query($method, $params)) { return $client->getResponse(); + } else { + throw new Exception($client->error); } } return false; @@ -159,11 +161,15 @@ class SimpleXMLRPC_Client */ public function setCredentials($username, $password) { - $this->authHeader = 'Authorization: Basic ' . base64_encode("$username:$password") . "\r\n"; + $this->authHeader = 'Authorization: Basic ' . base64_encode("$username:$password"); } public function query() { + /* XXX: xmlrpc is legacy, callers always import legacy config */ + global $config; + $tls_verify = !empty($config['hasync']) && !empty($config['hasync']['verifypeer']); + // create xmlrpc request object $args = func_get_args(); $method = array_shift($args); @@ -171,38 +177,37 @@ class SimpleXMLRPC_Client $request_xml = $request->getXml(); // setup http headers - $headers = 'Host: ' . $this->server . "\r\n"; - $headers .= "User-Agent: XML_RPC\r\n"; - $headers .= "Content-Type: text/xml\r\n"; - $headers .= 'Content-Length: ' . $request->getLength() . "\r\n"; + $headers = ['Host: ' . $this->server]; + $headers[] = "User-Agent: XML_RPC"; + $headers[] = "Content-Type: text/xml"; if ($this->authHeader != null) { - $headers .= $this->authHeader; + $headers[] = $this->authHeader; } - $this->request_send = $headers . $request_xml; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->url); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); + if (!$tls_verify) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + } + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLINFO_HEADER_OUT, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $request_xml); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $this->response_received = curl_exec($ch); if ($this->debug) { - echo sprintf(">>> send %d bytes: \n%s\n", strlen($this->request_send), $this->request_send); + print_r(curl_getinfo($ch)); } - - // setup a stream context - $context = stream_context_create(array('http' => array( - 'method' => "POST", - 'header' => $headers, - 'content' => $request_xml, - 'timeout' => $this->timeout - ), - "ssl" => array( - "verify_peer" => false, - "verify_peer_name" => false, - ) - )); - - $this->response_received = @file_get_contents($this->url, false, $context); if ($this->response_received === false) { - $this->error = 'fetch error. remote host down?'; + $this->error = 'fetch error. remote host down? (' . curl_error($ch) . ')'; + curl_close($ch); return false; } + curl_close($ch); + if ($this->debug) { echo sprintf(">>> received %d bytes: \n%s\n", strlen($this->response_received), $this->response_received); } diff --git a/src/etc/rc.subr.d/recover b/src/etc/rc.subr.d/recover index 6e16be56914..24ed13ae812 100755 --- a/src/etc/rc.subr.d/recover +++ b/src/etc/rc.subr.d/recover @@ -185,7 +185,7 @@ EOF; $php_ini = << + + hasync.verifypeer + + checkbox + In most cases the target host will be a directly attached neighbor in which case TLS verification can be ignored. + hasync.username diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Hasync.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Hasync.xml index 0fd524a8335..a0ac8461f32 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Hasync.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Hasync.xml @@ -1,7 +1,7 @@ //hasync MHA - 1.0.1 + 1.0.2 HA sync @@ -31,6 +31,10 @@ + + 0 + Y + diff --git a/src/opnsense/scripts/system/ha_xmlrpc_exec.php b/src/opnsense/scripts/system/ha_xmlrpc_exec.php index edab03bde29..a205a7885b8 100755 --- a/src/opnsense/scripts/system/ha_xmlrpc_exec.php +++ b/src/opnsense/scripts/system/ha_xmlrpc_exec.php @@ -35,37 +35,41 @@ $service = $argv[2] ?? ''; $service_id = $argv[3] ?? ''; -switch ($action) { - case 'stop': - $result = xmlrpc_execute('opnsense.stop_service', ['service' => $service, 'id' => $service_id]); - echo json_encode(['response' => $result, 'status' => 'ok']); - break; - case 'start': - $result = xmlrpc_execute('opnsense.start_service', ['service' => $service, 'id' => $service_id]); - echo json_encode(['response' => $result, 'status' => 'ok']); - break; - case 'restart': - $result = xmlrpc_execute('opnsense.restart_service', ['service' => $service, 'id' => $service_id]); - echo json_encode(['response' => $result, 'status' => 'ok']); - break; - case 'reload_templates': - xmlrpc_execute('opnsense.configd_reload_all_templates'); - echo json_encode(['status' => 'done']); - break; - case 'exec_sync': - configd_run('filter sync'); - echo json_encode(['status' => 'done']); - break; - case 'version': - $payload = xmlrpc_execute('opnsense.firmware_version'); - if (isset($payload['firmware'])) { - $payload['firmware']['_my_version'] = shell_safe('opnsense-version -v core'); - } - echo json_encode(['response' => $payload]); - break; - case 'services': - echo json_encode(['response' => xmlrpc_execute('opnsense.list_services')]); - break; - default: - echo json_encode(['status' => 'error', 'message' => 'usage ha_xmlrpc_exec.php action [service_id]']); +try { + switch ($action) { + case 'stop': + $result = xmlrpc_execute('opnsense.stop_service', ['service' => $service, 'id' => $service_id]); + echo json_encode(['response' => $result, 'status' => 'ok']); + break; + case 'start': + $result = xmlrpc_execute('opnsense.start_service', ['service' => $service, 'id' => $service_id]); + echo json_encode(['response' => $result, 'status' => 'ok']); + break; + case 'restart': + $result = xmlrpc_execute('opnsense.restart_service', ['service' => $service, 'id' => $service_id]); + echo json_encode(['response' => $result, 'status' => 'ok']); + break; + case 'reload_templates': + xmlrpc_execute('opnsense.configd_reload_all_templates'); + echo json_encode(['status' => 'done']); + break; + case 'exec_sync': + configd_run('filter sync'); + echo json_encode(['status' => 'done']); + break; + case 'version': + $payload = xmlrpc_execute('opnsense.firmware_version'); + if (isset($payload['firmware'])) { + $payload['firmware']['_my_version'] = shell_safe('opnsense-version -v core'); + } + echo json_encode(['response' => $payload]); + break; + case 'services': + echo json_encode(['response' => xmlrpc_execute('opnsense.list_services')]); + break; + default: + echo json_encode(['status' => 'error', 'message' => 'usage ha_xmlrpc_exec.php action [service_id]']); + } +} catch (Exception $e) { + echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); } diff --git a/src/opnsense/service/templates/OPNsense/WebGui/php.ini b/src/opnsense/service/templates/OPNsense/WebGui/php.ini index b50564b2d96..051432cc3aa 100644 --- a/src/opnsense/service/templates/OPNsense/WebGui/php.ini +++ b/src/opnsense/service/templates/OPNsense/WebGui/php.ini @@ -1,6 +1,6 @@ ; File generated via configd output_buffering = "0" -; allow_url_fopen = "0" +allow_url_fopen = "0" expose_php = Off enable_dl = Off implicit_flush = true