diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index dea8204ec..543f2f717 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 1.1 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= Let's Encrypt client PLUGIN_MAINTAINER= opnsense@moov.de 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 fcba67fa2..913afdc4c 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"), + array("enabled", "name", "altNames", "description", "lastUpdate", "statusCode", "statusLastUpdate"), "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 ea5083510..52e60e9b7 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 @@ -125,7 +125,7 @@ class SettingsController extends ApiMutableModelControllerBase $mdlAcme = $this->getModel(); // Check if the required plugin is installed - if ((string)$mdlAcme->isPluginInstalled('os-haproxy') != "1") { + if ((string)$mdlAcme->isPluginInstalled('haproxy') != "1") { $this->getLogger()->error("LE check: HAProxy plugin is NOT installed, skipping integration"); return($result); } @@ -322,6 +322,11 @@ class SettingsController extends ApiMutableModelControllerBase (string)$validation->method == "http01" and (string)$validation->http_service == "haproxy") { //$this->getLogger()->error("LE HAProxy DEBUG: checking validation method: " . (string)$validation->name); + // Check if HAProxy frontends were specified. + if (empty((string)$validation->http_haproxyFrontends)) { + // Skip item, no HAProxy frontends were specified. + continue; + } $_frontends = explode(',', $validation->http_haproxyFrontends); // Walk through all linked frontends. foreach ($_frontends as $_frontend) { 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 da015154c..2056ed88f 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 @@ -28,9 +28,9 @@ certificate.account - + dropdown - + certificate.validationMethod diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml index 81f703af4..4109e2c0d 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml @@ -26,6 +26,7 @@ header + validation.http_service @@ -36,6 +37,7 @@ header + validation.http_opn_autodiscovery @@ -61,6 +63,7 @@ header + validation.http_haproxyInject @@ -93,6 +96,7 @@ header + validation.dns_service @@ -109,6 +113,7 @@ header + validation.dns_ad_key @@ -119,6 +124,7 @@ header + validation.dns_ali_key @@ -135,6 +141,7 @@ header + validation.dns_aws_id @@ -151,6 +158,7 @@ header + validation.dns_cf_email @@ -167,6 +175,7 @@ header + validation.dns_cx_key @@ -183,6 +192,7 @@ header + validation.dns_dp_id @@ -199,6 +209,7 @@ header + validation.dns_gd_key @@ -215,6 +226,7 @@ header + validation.dns_ispconfig_user @@ -243,6 +255,7 @@ header + validation.dns_lexicon_provider @@ -265,6 +278,7 @@ header + validation.dns_lua_email @@ -281,6 +295,7 @@ header + validation.dns_me_key @@ -297,6 +312,7 @@ header + validation.dns_nsupdate_server @@ -313,6 +329,7 @@ header + validation.dns_ovh_app_key @@ -341,6 +358,7 @@ header + validation.dns_pdns_url diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php index 09a07e485..70427ab5c 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php @@ -96,45 +96,7 @@ class AcmeClient extends BaseModel */ public function isPluginInstalled($name) { - // NOTE: Based on infoAction() from Core/Api/FirmwareController.php - // FIXME: Should be replaced by a Core function sooner or later. - $backend = new Backend(); - $keys = array('name', 'version', 'comment', 'flatsize', 'locked', 'license'); - $plugins = array(); - - // Only check local package data for performance reasons - $current = $backend->configdRun("firmware local"); - $current = explode("\n", trim($current)); - - foreach ($current as $line) { - /* package infos are flat lists with 3 pipes as delimiter */ - $expanded = explode('|||', $line); - $translated = array(); - $index = 0; - if (count($expanded) != count($keys)) { - continue; - } - foreach ($keys as $key) { - $translated[$key] = $expanded[$index++]; - } - - /* mark local packages as "installed" */ - $translated['installed'] = "1"; - - /* figure out local and remote plugins */ - $plugin = explode('-', $translated['name']); - if (count($plugin)) { - if ($plugin[0] == 'os' || $plugin[0] == 'ospriv') { - $plugins[$translated['name']] = $translated; - } - } - } - - if (isset($plugins[$name]) and $plugins[$name]['installed'] == "1") { - return 1; // TRUE, is installed - } else { - return 0; // FALSE, is not installed - } + return trim($backend->configdRun('firmware plugin ' . escapeshellarg($name))); } } diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml index dd011e54a..50f6823de 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml @@ -224,6 +224,18 @@ N + + + N + + + 100 + 1000 + + + + N + diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt index 8f3ac324f..051f3e7af 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt @@ -69,6 +69,52 @@ POSSIBILITY OF SUCH DAMAGE. } else { return ""; } + }, + "certdate": function (column, row) { + if (row.lastUpdate == "" || row.lastUpdate == undefined) { + return "{{ lang._('pending') }}"; + } else { + var certdate = new Date(row.lastUpdate*1000); + return certdate.toLocaleString(); + } + }, + "acmestatus": function (column, row) { + if (row.statusCode == "" || row.statusCode == undefined) { + // fallback to lastUpdate value (unset if cert was never issued/imported) + if (row.lastUpdate == "" || row.lastUpdate == undefined) { + return "{{ lang._('unknown') }}"; + } else { + return "{{ lang._('OK') }}"; + } + } else if (row.statusCode == "100") { + return "{{ lang._('unknown') }}"; + } else if (row.statusCode == "200") { + return "{{ lang._('OK') }}"; + } else if (row.statusCode == "250") { + return "{{ lang._('cert revoked') }}"; + } else if (row.statusCode == "300") { + return "{{ lang._('configuration error') }}"; + } else if (row.statusCode == "400") { + return "{{ lang._('validation failed') }}"; + } else if (row.statusCode == "500") { + return "{{ lang._('internal error') }}"; + } else { + return "{{ lang._('unknown') }}"; + } + }, + "acmestatusdate": function (column, row) { + if (row.statusLastUpdate == "" || row.statusCode == undefined) { + // fallback to lastUpdate value + if (row.lastUpdate == "" || row.lastUpdate == undefined) { + return "{{ lang._('unknown') }}"; + } else { + var legacydate = new Date(row.lastUpdate*1000); + return legacydate.toLocaleString(); + } + } else { + var statusdate = new Date(row.statusLastUpdate*1000); + return statusdate.toLocaleString(); + } } }, }; @@ -339,6 +385,9 @@ POSSIBILITY OF SUCH DAMAGE. {{ lang._('Certificate Name') }} {{ lang._('Multi-Domain (SAN)') }} {{ lang._('Description') }} + {{ lang._('Issue/Renewal Date') }} + {{ lang._('Last Acme Status') }} + {{ lang._('Last Acme Run') }} {{ lang._('Commands') }} {{ lang._('ID') }} diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt index a1eca2613..96660070d 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt @@ -175,7 +175,7 @@ POSSIBILITY OF SUCH DAMAGE. }); } else { BootstrapDialog.show({ - type: BootstrapDialog.TYPE_WARNING, + type: BootstrapDialog.TYPE_INFO, title: "{{ lang._('acme-client config test result') }}", message: "{{ lang._('Your acme-client config contains no errors.') }}", draggable: true diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt index 260d583f3..b192c9477 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt @@ -48,6 +48,22 @@ POSSIBILITY OF SUCH DAMAGE. } ); + // hook into on-show event for dialog to extend layout. + $('#DialogValidation').on('shown.bs.modal', function (e) { + $("#validation\\.dns_service").change(function(){ + var service_id = 'table_' + $(this).val() ; + $(".table_dns").hide(); + if ($("#validation\\.method").val() == 'dns01') { + $("."+service_id).show(); + } + }); + $("#validation\\.method").change(function(){ + $(".method_table").hide(); + $(".method_table_"+$(this).val()).show(); + $("#validation\\.dns_service").change(); + }); + $("#validation\\.method").change(); + }) }); diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php index f6c417c4e..058004bd4 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php @@ -158,6 +158,7 @@ function cert_action_validator($opt_cert_id) } else { //echo "DEBUG: account registration failed\n"; log_error("AcmeClient: account registration failed"); + log_cert_acme_status($certObj, $modelObj, '400'); if (isset($options["A"])) { continue; // skip to next item } @@ -166,6 +167,7 @@ function cert_action_validator($opt_cert_id) } else { //echo "DEBUG: account not found\n"; log_error("AcmeClient: account not found"); + log_cert_acme_status($certObj, $modelObj, '300'); if (isset($options["A"])) { continue; // skip to next item } @@ -193,10 +195,12 @@ function cert_action_validator($opt_cert_id) // Start acme client to revoke the certificate $rev_result = revoke_cert($certObj, $valObj, $acctObj); if (!$rev_result) { + log_cert_acme_status($certObj, $modelObj, '250'); return(0); // Success! } else { // Revocation failure log_error("AcmeClient: revocation for certificate failed"); + log_cert_acme_status($certObj, $modelObj, '400'); if (isset($options["A"])) { continue; // skip to next item } @@ -215,8 +219,10 @@ function cert_action_validator($opt_cert_id) //echo "DEBUG: cert import done\n"; // Prepare certificate for restart action $restart_certs[] = $certObj; + log_cert_acme_status($certObj, $modelObj, '200'); } else { log_error("AcmeClient: unable to import certificate: " . (string)$certObj->name); + log_cert_acme_status($certObj, $modelObj, '500'); if (isset($options["A"])) { continue; // skip to next item } @@ -227,6 +233,7 @@ function cert_action_validator($opt_cert_id) } else { // validation failure log_error("AcmeClient: validation for certificate failed: " . (string)$certObj->name); + log_cert_acme_status($certObj, $modelObj, '400'); if (isset($options["A"])) { continue; // skip to next item } @@ -234,6 +241,7 @@ function cert_action_validator($opt_cert_id) } } else { log_error("AcmeClient: invalid validation method specified: " . (string)$valObj->method); + log_cert_acme_status($certObj, $modelObj, '300'); if (isset($options["A"])) { continue; // skip to next item } @@ -241,6 +249,7 @@ function cert_action_validator($opt_cert_id) } } else { log_error("AcmeClient: validation method not found for cert " . $certObj->name); + log_cert_acme_status($certObj, $modelObj, '300'); if (isset($options["A"])) { continue; // skip to next item } @@ -887,8 +896,11 @@ function run_restart_actions($certlist, $modelObj) } // Extract restart actions $_actions = explode(',', $certObj->restartActions); + if (empty($_actions)) { + // No restart actions configured. + continue; + } // Walk through all linked restart actions. - $_actions = explode(',', $certObj->restartActions); foreach ($_actions as $_action) { // Extract restart action $action = $modelObj->getByActionID($_action); @@ -1007,6 +1019,32 @@ function run_restart_actions($certlist, $modelObj) return($return); } +/* Update certificate object to log the status of the current acme run. + * Supported status codes are: + * 100 pending + * 200 issue/renew OK + * 250 certificate revoked + * 300 configuration error (validation method, account, ...) + * 400 issue/renew failed + * 500 internal error (code issues, bad luck, unexpected errors, ...) + * Feel free to add more status codes to make it more useful. +*/ +function log_cert_acme_status($certObj, $modelObj, $statusCode) +{ + $uuid = $certObj->attributes()->uuid; + $node = $modelObj->getNodeByReference('certificates.certificate.' . $uuid); + if ($node != null) { + $node->statusCode = $statusCode; + $node->statusLastUpdate = time(); + // serialize to config and save + $modelObj->serializeToConfig(); + Config::getInstance()->save(); + } else { + log_error("AcmeClient: unable to update acme status for certificate " . (string)$certObj->name); + return(1); + } +} + // taken from certs.inc function local_cert_get_subject_array($str_crt, $decode = true) {