diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index 59485d2d6..dea8204ec 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 1.0 +PLUGIN_VERSION= 1.1 PLUGIN_COMMENT= Let's Encrypt client PLUGIN_MAINTAINER= opnsense@moov.de diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/ActionsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/ActionsController.php new file mode 100644 index 000000000..512accd09 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/ActionsController.php @@ -0,0 +1,46 @@ +view->title = "Let's Encrypt Restart Actions"; + // include form definitions + $this->view->formDialogAction = $this->getForm("dialogAction"); + // choose template + $this->view->pick('OPNsense/AcmeClient/actions'); + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php index 8eaf12334..a47695edc 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php @@ -202,7 +202,7 @@ class AccountsController extends ApiControllerBase $grid = new UIModelGrid($mdlAcme->accounts->account); return $grid->fetchBindRequest( $this->request, - array("enabled", "name", "email","accountid"), + array("enabled", "name", "email"), "name" ); } diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ActionsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ActionsController.php new file mode 100644 index 000000000..2c9c666ea --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ActionsController.php @@ -0,0 +1,209 @@ +"failed","validations" => array()); + // perform validation + $valMsgs = $mdl->performValidation(); + foreach ($valMsgs as $field => $msg) { + // replace absolute path to attribute for relative one at uuid. + if ($node != null) { + $fieldnm = str_replace($node->__reference, $reference, $msg->getField()); + $result["validations"][$fieldnm] = $msg->getMessage(); + } else { + $result["validations"][$msg->getField()] = $msg->getMessage(); + } + } + + // serialize model to config and save when there are no validation errors + if (count($result['validations']) == 0) { + // save config if validated correctly + $mdl->serializeToConfig(); + + Config::getInstance()->save(); + $result = array("result" => "saved"); + } + + return $result; + } + + /** + * retrieve action settings or return defaults + * @param $uuid item unique id + * @return array + */ + public function getAction($uuid = null) + { + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('actions.action.'.$uuid); + if ($node != null) { + // return node + return array("action" => $node->getNodes()); + } + } else { + // generate new node, but don't save to disc + $node = $mdlAcme->actions->action->add(); + return array("action" => $node->getNodes()); + } + return array(); + } + + /** + * update action with given properties + * @param $uuid item unique id + * @return array + */ + public function setAction($uuid) + { + if ($this->request->isPost() && $this->request->hasPost("action")) { + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('actions.action.'.$uuid); + if ($node != null) { + $node->setNodes($this->request->getPost("action")); + return $this->save($mdlAcme, $node, "action"); + } + } + } + return array("result"=>"failed"); + } + + /** + * add new action and set with attributes from post + * @return array + */ + public function addAction() + { + $result = array("result"=>"failed"); + if ($this->request->isPost() && $this->request->hasPost("action")) { + $mdlAcme = new AcmeClient(); + $node = $mdlAcme->actions->action->Add(); + $node->setNodes($this->request->getPost("action")); + return $this->save($mdlAcme, $node, "action"); + } + return $result; + } + + /** + * delete action by uuid + * @param $uuid item unique id + * @return array status + */ + public function delAction($uuid) + { + $result = array("result"=>"failed"); + if ($this->request->isPost()) { + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + if ($mdlAcme->actions->action->del($uuid)) { + // if item is removed, serialize to config and save + $mdlAcme->serializeToConfig(); + Config::getInstance()->save(); + $result['result'] = 'deleted'; + } else { + $result['result'] = 'not found'; + } + } + } + return $result; + } + + /** + * toggle action by uuid (enable/disable) + * @param $uuid item unique id + * @param $enabled desired state enabled(1)/disabled(0), leave empty for toggle + * @return array status + */ + public function toggleAction($uuid, $enabled = null) + { + + $result = array("result" => "failed"); + if ($this->request->isPost()) { + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('actions.action.' . $uuid); + if ($node != null) { + if ($enabled == "0" || $enabled == "1") { + $node->enabled = (string)$enabled; + } elseif ((string)$node->enabled == "1") { + $node->enabled = "0"; + } else { + $node->enabled = "1"; + } + $result['result'] = $node->enabled; + // if item has toggled, serialize to config and save + $mdlAcme->serializeToConfig(); + Config::getInstance()->save(); + } + } + } + return $result; + } + + /** + * search actions + * @return array + */ + public function searchAction() + { + $this->sessionClose(); + $mdlAcme = new AcmeClient(); + $grid = new UIModelGrid($mdlAcme->actions->action); + return $grid->fetchBindRequest( + $this->request, + array("enabled", "name", "description"), + "name" + ); + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php index c326ed77a..fcba67fa2 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php @@ -203,7 +203,7 @@ class CertificatesController extends ApiControllerBase $grid = new UIModelGrid($mdlAcme->certificates->certificate); return $grid->fetchBindRequest( $this->request, - array("enabled", "name", "altNames", "description","certificateid"), + array("enabled", "name", "altNames", "description"), "name" ); } diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php index 30f6b7d30..b62ce4f80 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php @@ -34,6 +34,7 @@ use \OPNsense\Core\Backend; use \OPNsense\Cron\Cron; use \OPNsense\Core\Config; use \OPNsense\Base\UIModelGrid; +use \OPNsense\AcmeClient\AcmeClient; /** * Class SettingsController @@ -48,12 +49,10 @@ class SettingsController extends ApiMutableModelControllerBase * create new cron job or return already available one * @return array status action */ - public function fetchRBCronAction() + public function fetchCronIntegrationAction() { $result = array("result" => "no change"); - // TODO: How to force the system to write-out the cronjob? - if ($this->request->isPost()) { $mdlAcme = $this->getModel(); $backend = new Backend(); @@ -81,7 +80,7 @@ class SettingsController extends ApiMutableModelControllerBase // cron item just created. $mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true); Config::getInstance()->save(); - // Regenerate the crontab + // Refresh the crontab $backend->configdRun('template reload OPNsense/Cron'); $result['result'] = "new"; $result['uuid'] = $cron_uuid; @@ -92,11 +91,13 @@ class SettingsController extends ApiMutableModelControllerBase } elseif ((string)$mdlAcme->settings->UpdateCron != "" and ((string)$mdlAcme->settings->autoRenewal == "0" or (string)$mdlAcme->settings->enabled == "0")) { + // Get UUID, clean existin entry $cron_uuid = (string)$mdlAcme->settings->UpdateCron; $mdlAcme->settings->UpdateCron = null; $mdlCron = new Cron(); + // Delete the cronjob item if ($mdlCron->jobs->job->del($cron_uuid)) { - // if item is removed, serialize to config and save + // If item is removed, serialize to config and save $mdlCron->serializeToConfig(); $mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true); Config::getInstance()->save(); @@ -111,4 +112,269 @@ class SettingsController extends ApiMutableModelControllerBase return $result; } + + /** + * integrate with HAProxy plugin or return if already done + * @return array status action + */ + public function fetchHAProxyIntegrationAction() + { + $result = array("result" => "no change"); + + if ($this->request->isPost()) { + $mdlAcme = $this->getModel(); + + // Check if the required plugin is installed + if ((string)$mdlAcme->isPluginInstalled('os-haproxy') != "1") { + $this->getLogger()->error("LE check: HAProxy plugin is NOT installed, skipping integration"); + return($result); + } + + // Setup only if AcmeClient and HAProxy integration is enabled. + // NOTE: We provide HAProxy integration no matter if the HAProxy plugin + // is actually enabled or not. This should avoid confusion. + if ((string)$mdlAcme->settings->haproxyIntegration == "1" and + (string)$mdlAcme->settings->enabled == "1") { + $mdlHAProxy = new \OPNsense\HAProxy\HAProxy(); + $backend = new Backend(); + + // Get current status of HAProxy integration by running various checks. + $integration_found = false; // Switch to TRUE if something is found. + $integration_complete = true; // Switch to FALSE if anything is missing. + $integration_changes = false; // Switch to TRUE if config was changes. + + // Check: HAProxy ACL + $acl_ref = (string)$mdlAcme->settings->haproxyAclRef; + if (!empty($acl_ref)) { + $integration_found = true; // We found something. + // Make sure the item was not deleted. + if ($mdlHAProxy->getByAclID($acl_ref) === null) { + $this->getLogger()->error("LE check: HAProxy integration is incomplete: ACL item not found"); + $integration_complete = false; // Item is broken. + } + } else { + $integration_complete = false; // Item is missing. + } + + // Check: HAProxy action + $action_ref = (string)$mdlAcme->settings->haproxyActionRef; + if (!empty($action_ref)) { + $integration_found = true; // We found something. + // Make sure the item was not deleted. + if ($mdlHAProxy->getByActionID($action_ref) === null) { + $this->getLogger()->error("LE check: HAProxy integration is incomplete: action item not found"); + $integration_complete = false; // Item is broken. + } + } else { + $integration_complete = false; // Item is missing. + } + + // Check: HAProxy server + $server_ref = (string)$mdlAcme->settings->haproxyServerRef; + if (!empty($server_ref)) { + $integration_found = true; // We found something. + // Make sure the item was not deleted. + if ($mdlHAProxy->getByServerID($server_ref) === null) { + $this->getLogger()->error("LE check: HAProxy integration is incomplete: server item not found"); + $integration_complete = false; // Item is broken. + } + } else { + $integration_complete = false; // Item is missing. + } + + // Check: HAProxy backend + $backend_ref = (string)$mdlAcme->settings->haproxyBackendRef; + if (!empty($backend_ref)) { + $integration_found = true; // We found something. + // Make sure the item was not deleted. + if ($mdlHAProxy->getByBackendID($backend_ref) === null) { + $this->getLogger()->error("LE check: HAProxy integration is incomplete: backend item not found"); + $integration_complete = false; // Item is broken. + } + } else { + $integration_complete = false; // Item is missing. + } + + // Check if HAProxy integration is already complete. + if ($integration_found and $integration_complete) { + $this->getLogger()->error("LE check: HAProxy integration is complete"); + } else { + $integration_changes = true; + // Check if we need to remove relics of incomplete HAProxy integration. + // NOTE: We try to automatically repair a broken HAProxy integration, + // although the user may have deleted some items intentionally. + // As long as the HAProxy integration is enabled we assume that + // this is an error that should *automatically* be fixed. + if ($integration_found and !$integration_complete) { + // NOTE: We ignore the return value of the del() calls + // too keep this as simple as possible. + $this->getLogger()->error("LE check: HAProxy integration is incomplete, removing relics"); + // Remove obsolete backend item + if (!empty($backend_ref)) { + if ($mdlHAProxy->backends->backend->del($backend_ref)) { + $this->getLogger()->error("LE HAProxy integration: deleted obsolete backend item"); + } + } + // Remove obsolete server item + if (!empty($server_ref)) { + if ($mdlHAProxy->servers->server->del($server_ref)) { + $this->getLogger()->error("LE HAProxy integration: deleted obsolete server item"); + } + } + // Remove obsolete action item + if (!empty($action_ref)) { + if ($mdlHAProxy->actions->action->del($action_ref)) { + $this->getLogger()->error("LE HAProxy integration: deleted obsolete action item"); + } + } + // Remove obsolete ACL item + if (!empty($acl_ref)) { + if ($mdlHAProxy->acls->acl->del($acl_ref)) { + $this->getLogger()->error("LE HAProxy integration: deleted obsolete ACL item"); + } + } + // TODO: Remove obsolete ACL link from frontends + + // NOTE: We don't clear the settings refs here, because they + // will be overwritten later anyway. + $result['result'] = "repaired"; + } else { + $this->getLogger()->error("LE check: HAProxy integration initializing"); + $result['result'] = "new"; + } + + // Get TCP port for internal acme webserver from config. + $acme_port = (string)$mdlAcme->settings->challengePort; + + // Add a new HAProxy ACL + $acl_uuid = $mdlHAProxy->newAcl( + "find_acme_challenge", + "Added by Let's Encrypt plugin", + "path_starts_with", + "0", + "/.well-known/acme-challenge/" + ); + //$this->getLogger()->error("LE acl: ${acl_uuid}"); + + // Add a new HAProxy backend + $backend_uuid = $mdlHAProxy->newBackend( + "1", + "acme_challenge_backend", + "Added by Let's Encrypt plugin", + "http", + "source", + "", + "" + ); + //$this->getLogger()->error("LE backend: ${backend_uuid}"); + + // Add a new HAProxy action + $action_uuid = $mdlHAProxy->newAction( + "redirect_acme_challenges", + "Added by Let's Encrypt plugin", + "if", + "", + "and", + "use_backend", + // Use the new backend uuid in field "useBackend" + $backend_uuid, + "", + "", + "", + "" + ); + //$this->getLogger()->error("LE action: ${action_uuid}"); + // NOTE: This action is linked to frontends. + $action_ref = $action_uuid; + + // Add a new HAProxy server + $server_uuid = $mdlHAProxy->newServer( + "acme_challenge_host", + "Added by Let's Encrypt plugin", + "127.0.0.1", + $acme_port, + "active", + "0", + "0", + "" + ); + //$this->getLogger()->error("LE server: ${server_uuid}"); + + // Update hidden fields to signal that HAProxy integration is complete. + $mdlAcme->settings->haproxyAclRef = $acl_uuid; + $mdlAcme->settings->haproxyActionRef = $action_uuid; + $mdlAcme->settings->haproxyServerRef = $server_uuid; + $mdlAcme->settings->haproxyBackendRef = $backend_uuid; + + // Link new ACL to HAProxy action + $link_acl_result = $mdlHAProxy->linkAclToAction($acl_uuid,$action_uuid); + //$this->getLogger()->error("LE link acl result: ${link_acl_result}"); + + // Link new server to HAProxy backend + $link_server_result = $mdlHAProxy->linkServerToBackend($server_uuid,$backend_uuid); + //$this->getLogger()->error("LE link server result: ${link_server_result}"); + } + + // Ensure HAProxy frontend additions have been applied. + foreach ($mdlAcme->getNodeByReference('validations.validation')->__items as $validation) { + // Find all (enabled) validation methods with HAProxy integration. + if ((string)$validation->enabled == "1" and + (string)$validation->method == "http01" and + (string)$validation->http_service == "haproxy") { + //$this->getLogger()->error("LE HAProxy DEBUG: checking validation method: " . (string)$validation->name); + $_frontends = explode(',', $validation->http_haproxyFrontends); + // Walk through all linked frontends. + foreach ($_frontends as $_frontend) { + //$this->getLogger()->error("LE HAProxy DEBUG: checking frontend: ${_frontend}"); + $frontend = $mdlHAProxy->getByFrontendID($_frontend); + // Make sure the frontend was found in config. + if (!empty((string)$frontend->id)) { + // Check if the HAProxy ACME Action is linked to this frontend. + $_actions = $frontend->linkedActions; + if (strpos($_actions,$action_ref) !== false) { + // Match! Nothing to do. + } else { + // Link to ACME Action is currently missing: add it! + if (!empty((string)$_actions)) { + // Extend existing string. + $_actions .= ",${action_ref}"; + } else { + // First linked Action for this frontend. + $_actions = $action_ref; + } + // Add modified list of linked Actions to frontend. + $frontend->linkedActions = $_actions; + $this->getLogger()->error("LE HAProxy integration: updating frontend ${_frontend}"); + // We need to write changes to config. + $integration_changes = true; + } + } + } + } + } + + // Changes made to configuration? + if ($integration_changes === true) { + $this->getLogger()->error("LE HAProxy integration: saving updated configuration"); + // Save updated configuration. + // Do NOT validate because the current in-memory model doesn't know about the + // HAProxy items just created. + // FIXME: works, but still leads to "Related item not found" errors in the log file + $mdlHAProxy->serializeToConfig($validateFullModel = false, $disable_validation = true); + $mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true); + Config::getInstance()->save(); + + // Reconfigure HAProxy + $backend->configdRun('template reload OPNsense/HAProxy'); + $response = $backend->configdRun("haproxy restart"); + } + + } else { + // NOTE: HAProxy integration is NOT removed if the user disables it, because + // we might destroy changes made by the user when doing so. + } + } + + return $result; + } } diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ValidationsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ValidationsController.php index fbe9f18a7..70a3cafc0 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ValidationsController.php +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ValidationsController.php @@ -202,7 +202,7 @@ class ValidationsController extends ApiControllerBase $grid = new UIModelGrid($mdlAcme->validations->validation); return $grid->fetchBindRequest( $this->request, - array("enabled", "name", "description","validationid"), + array("enabled", "name", "description"), "name" ); } diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml new file mode 100644 index 000000000..51254439c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml @@ -0,0 +1,42 @@ +
diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml index 9dee719d4..da015154c 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml @@ -23,7 +23,7 @@| {{ lang._('Enabled') }} | +{{ lang._('Name') }} | +{{ lang._('Description') }} | +{{ lang._('Commands') }} | +{{ lang._('ID') }} | +
|---|---|---|---|---|
| + | + + + | +