From dd4853d09f50ef68940f88e152ec84f319197fe4 Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Thu, 5 Jan 2017 22:18:12 +0100 Subject: [PATCH] security/acme-client: add acme.sh to plugins, closes #6 --- security/acme-client/Makefile | 7 + .../src/etc/inc/plugins.inc.d/acmeclient.inc | 95 + .../AcmeClient/AccountsController.php | 46 + .../AcmeClient/Api/AccountsController.php | 209 + .../AcmeClient/Api/CertificatesController.php | 259 + .../AcmeClient/Api/ServiceController.php | 191 + .../AcmeClient/Api/SettingsController.php | 116 + .../AcmeClient/Api/ValidationsController.php | 209 + .../AcmeClient/CertificatesController.php | 46 + .../OPNsense/AcmeClient/IndexController.php | 51 + .../AcmeClient/ValidationsController.php | 46 + .../AcmeClient/forms/dialogAccount.xml | 33 + .../AcmeClient/forms/dialogCertificate.xml | 53 + .../AcmeClient/forms/dialogValidation.xml | 357 ++ .../OPNsense/AcmeClient/forms/settings.xml | 27 + .../models/OPNsense/AcmeClient/ACL/ACL.xml | 10 + .../models/OPNsense/AcmeClient/AcmeClient.php | 68 + .../models/OPNsense/AcmeClient/AcmeClient.xml | 373 ++ .../models/OPNsense/AcmeClient/Menu/Menu.xml | 17 + .../views/OPNsense/AcmeClient/accounts.volt | 87 + .../OPNsense/AcmeClient/certificates.volt | 366 ++ .../views/OPNsense/AcmeClient/settings.volt | 207 + .../OPNsense/AcmeClient/validations.volt | 87 + .../scripts/OPNsense/AcmeClient/acme.sh | 4649 +++++++++++++++++ .../OPNsense/AcmeClient/certhelper.php | 872 ++++ .../OPNsense/AcmeClient/dnsapi/dns_ad.sh | 147 + .../OPNsense/AcmeClient/dnsapi/dns_ali.sh | 187 + .../OPNsense/AcmeClient/dnsapi/dns_aws.sh | 227 + .../OPNsense/AcmeClient/dnsapi/dns_cf.sh | 183 + .../OPNsense/AcmeClient/dnsapi/dns_cx.sh | 216 + .../OPNsense/AcmeClient/dnsapi/dns_dp.sh | 223 + .../OPNsense/AcmeClient/dnsapi/dns_gd.sh | 117 + .../AcmeClient/dnsapi/dns_ispconfig.sh | 177 + .../OPNsense/AcmeClient/dnsapi/dns_lexicon.sh | 78 + .../OPNsense/AcmeClient/dnsapi/dns_lua.sh | 143 + .../OPNsense/AcmeClient/dnsapi/dns_me.sh | 146 + .../OPNsense/AcmeClient/dnsapi/dns_myapi.sh | 35 + .../AcmeClient/dnsapi/dns_nsupdate.sh | 58 + .../OPNsense/AcmeClient/dnsapi/dns_ovh.sh | 295 ++ .../OPNsense/AcmeClient/dnsapi/dns_pdns.sh | 184 + .../scripts/OPNsense/AcmeClient/setup.sh | 14 + .../conf/actions.d/actions_acmeclient.conf | 67 + .../templates/OPNsense/AcmeClient/+TARGETS | 3 + .../AcmeClient/lighttpd-acme-challenge.conf | 86 + .../OPNsense/AcmeClient/lighttpd-rc-script | 67 + .../templates/OPNsense/AcmeClient/rc.conf.d | 7 + .../src/www/diag_logs_acmeclient.php | 6 + 47 files changed, 11147 insertions(+) create mode 100644 security/acme-client/Makefile create mode 100644 security/acme-client/src/etc/inc/plugins.inc.d/acmeclient.inc create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/AccountsController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ValidationsController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/CertificatesController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/IndexController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/ValidationsController.php create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAccount.xml create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml create mode 100644 security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml create mode 100644 security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/ACL/ACL.xml create mode 100644 security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php create mode 100644 security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml create mode 100644 security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml create mode 100644 security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt create mode 100644 security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt create mode 100644 security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt create mode 100644 security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php create mode 100644 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ad.sh create mode 100644 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh create mode 100644 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_aws.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cf.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_dp.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ispconfig.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_myapi.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_nsupdate.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ovh.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_pdns.sh create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh create mode 100644 security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf create mode 100644 security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS create mode 100644 security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf create mode 100755 security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-rc-script create mode 100644 security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d create mode 100644 security/acme-client/src/www/diag_logs_acmeclient.php diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile new file mode 100644 index 000000000..1b607b5e4 --- /dev/null +++ b/security/acme-client/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= acme-client +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= Lets Encrypt client +#PLUGIN_DEPENDS= acme.sh +PLUGIN_MAINTAINER= opnsense@moov.de + +.include "../../Mk/plugins.mk" diff --git a/security/acme-client/src/etc/inc/plugins.inc.d/acmeclient.inc b/security/acme-client/src/etc/inc/plugins.inc.d/acmeclient.inc new file mode 100644 index 000000000..de08064e8 --- /dev/null +++ b/security/acme-client/src/etc/inc/plugins.inc.d/acmeclient.inc @@ -0,0 +1,95 @@ +registerAnchor('acme-client/*', 'nat'); + $fw->registerAnchor('acme-client/*', 'rdr'); + $fw->registerAnchor('acme-client/*', 'fw'); +} + +/** + * register legacy service + * @return array + */ +function acmeclient_services() +{ + if (!acmeclient_enabled()) { + return; + } + + global $config; + $services = array(); + + $services[] = array( + 'description' => gettext('Secure Lets Encrypt client'), + 'configd' => array( + 'restart' => array('acme-http-challenge restart'), + 'start' => array('acme-http-challenge start'), + 'stop' => array('acme-http-challenge stop'), + ), + 'name' => 'acmeclient', + ); + + return $services; +} + +/** + * sync configuration via xmlrpc + * @return array + */ + +/** +XXX: needs investigation, auto-renewal must be disabled on secondary node(s) +function acmeclient_xmlrpc_sync() +{ + $result = array(); + $result['id'] = 'acmeclient'; + $result['section'] = 'OPNsense.acmeclient'; + $result['description'] = gettext('Lets Encrypt Client'); + return array($result); +} + */ diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/AccountsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/AccountsController.php new file mode 100644 index 000000000..f4ccf8a65 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/AccountsController.php @@ -0,0 +1,46 @@ +view->title = "Let's Encrypt Accounts"; + // include form definitions + $this->view->formDialogAccount = $this->getForm("dialogAccount"); + // choose template + $this->view->pick('OPNsense/AcmeClient/accounts'); + } +} 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 new file mode 100644 index 000000000..b0fc401e6 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/AccountsController.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 account 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('accounts.account.'.$uuid); + if ($node != null) { + // return node + return array("account" => $node->getNodes()); + } + } else { + // generate new node, but don't save to disc + $node = $mdlAcme->accounts->account->add() ; + return array("account" => $node->getNodes()); + } + return array(); + } + + /** + * update account with given properties + * @param $uuid item unique id + * @return array + */ + public function setAction($uuid) + { + if ($this->request->isPost() && $this->request->hasPost("account")) { + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('accounts.account.'.$uuid); + if ($node != null) { + $node->setNodes($this->request->getPost("account")); + return $this->save($mdlAcme, $node, "account"); + } + } + } + return array("result"=>"failed"); + } + + /** + * add new account and set with attributes from post + * @return array + */ + public function addAction() + { + $result = array("result"=>"failed"); + if ($this->request->isPost() && $this->request->hasPost("account")) { + $mdlAcme = new AcmeClient(); + $node = $mdlAcme->accounts->account->Add(); + $node->setNodes($this->request->getPost("account")); + return $this->save($mdlAcme, $node, "account"); + } + return $result; + } + + /** + * delete account 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->accounts->account->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 account 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('accounts.account.' . $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 accounts + * @return array + */ + public function searchAction() + { + $this->sessionClose(); + $mdlAcme = new AcmeClient(); + $grid = new UIModelGrid($mdlAcme->accounts->account); + return $grid->fetchBindRequest( + $this->request, + array("enabled", "name", "email","accountid"), + "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 new file mode 100644 index 000000000..d7819cec0 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/CertificatesController.php @@ -0,0 +1,259 @@ +"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 certificate 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('certificates.certificate.'.$uuid); + if ($node != null) { + // return node + return array("certificate" => $node->getNodes()); + } + } else { + // generate new node, but don't save to disc + $node = $mdlAcme->certificates->certificate->add() ; + return array("certificate" => $node->getNodes()); + } + return array(); + } + + /** + * update certificate with given properties + * @param $uuid item unique id + * @return array + */ + public function setAction($uuid) + { + if ($this->request->isPost() && $this->request->hasPost("certificate")) { + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('certificates.certificate.'.$uuid); + if ($node != null) { + $node->setNodes($this->request->getPost("certificate")); + return $this->save($mdlAcme, $node, "certificate"); + } + } + } + return array("result"=>"failed"); + } + + /** + * add new certificate and set with attributes from post + * @return array + */ + public function addAction() + { + $result = array("result"=>"failed"); + if ($this->request->isPost() && $this->request->hasPost("certificate")) { + $mdlAcme = new AcmeClient(); + $node = $mdlAcme->certificates->certificate->Add(); + $node->setNodes($this->request->getPost("certificate")); + return $this->save($mdlAcme, $node, "certificate"); + } + return $result; + } + + /** + * delete certificate 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->certificates->certificate->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 certificate 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('certificates.certificate.' . $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 certificates + * @return array + */ + public function searchAction() + { + $this->sessionClose(); + $mdlAcme = new AcmeClient(); + $grid = new UIModelGrid($mdlAcme->certificates->certificate); + return $grid->fetchBindRequest( + $this->request, + array("enabled", "name", "altNames", "description","certificateid"), + "name" + ); + } + + /** + * sign certificate by uuid + * @param $uuid item unique id + * @return array status + */ + public function signAction($uuid) + { + $result = array("result"=>"failed"); + if ($this->request->isPost()) { + $mdlAcme = new AcmeClient(); + + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid); + if ($node != null) { + $cert_id = $node->id; + $backend = new Backend(); + $response = $backend->configdRun("acmeclient sign-cert {$cert_id}"); + return array("response" => $response); + } + } + } + return $result; + } + + /** + * revoke certificate by uuid + * @param $uuid item unique id + * @return array status + */ + public function revokeAction($uuid) + { + $result = array("result"=>"failed"); + if ($this->request->isPost()) { + $mdlAcme = new AcmeClient(); + + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('certificates.certificate.' . $uuid); + if ($node != null) { + $cert_id = $node->id; + $backend = new Backend(); + $response = $backend->configdRun("acmeclient revoke-cert {$cert_id}"); + return array("response" => $response); + } + } + } + return $result; + } + +} diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php new file mode 100644 index 000000000..419d5d9eb --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ServiceController.php @@ -0,0 +1,191 @@ +request->isPost()) { + $backend = new Backend(); + $response = $backend->configdRun("acmeclient http-start", true); + return array("response" => $response); + } else { + return array("response" => array()); + } + } + + /** + * stop acmeclient service + * @return array + */ + public function stopAction() + { + if ($this->request->isPost()) { + $backend = new Backend(); + $response = $backend->configdRun("acmeclient http-stop"); + return array("response" => $response); + } else { + return array("response" => array()); + } + } + + /** + * restart acme_http_challenge service + * @return array + */ + public function restartAction() + { + if ($this->request->isPost()) { + $backend = new Backend(); + $response = $backend->configdRun("acmeclient http-restart"); + return array("response" => $response); + } else { + return array("response" => array()); + } + } + + /** + * retrieve status of acme_http_challenge service + * @return array + * @throws \Exception + */ + public function statusAction() + { + $backend = new Backend(); + $model = new AcmeClient(); + $response = $backend->configdRun("acmeclient http-status"); + + if (strpos($response, "not running") > 0) { + if ($model->settings->enabled->__toString() == 1) { + $status = "stopped"; + } else { + $status = "disabled"; + } + } elseif (strpos($response, "is running") > 0) { + $status = "running"; + } elseif ($model->settings->enabled->__toString() == 0) { + $status = "disabled"; + } else { + $status = "unkown"; + } + + return array("status" => $status); + } + + /** + * reconfigure acmeclient, generate config and reload + */ + public function reconfigureAction() + { + if ($this->request->isPost()) { + // close session for long running action + $this->sessionClose(); + + $force_restart = false; + + $mdlAcme = new AcmeClient(); + $backend = new Backend(); + $runStatus = $this->statusAction(); + + // stop acmeclient when disabled + if ($runStatus['status'] == "running" && + ($mdlAcme->settings->enabled->__toString() == 0 || $force_restart)) { + $this->stopAction(); + } + + // generate template + $backend->configdRun('template reload OPNsense/AcmeClient'); + + // now setup the environment + $backend->configdRun("acmeclient setup"); + + // (res)start daemon + if ($mdlAcme->settings->enabled->__toString() == 1) { + if ($runStatus['status'] == "running" && !$force_restart) { + $backend->configdRun("acmeclient http-restart"); + } else { + $this->startAction(); + } + } + + return array("status" => "ok"); + } else { + return array("status" => "failed"); + } + } + + /** + * run syntax check for our custom lighttpd configuration + * @return array + * @throws \Exception + */ + public function configtestAction() + { + $backend = new Backend(); + // first generate template based on current configuration + $backend->configdRun('template reload OPNsense/AcmeClient'); + // now setup the environment + $backend->configdRun("acmeclient setup"); + // finally run the syntax check + $response = $backend->configdRun("acmeclient configtest"); + return array("result" => $response); + // TODO: We may also want to check for duplicate cert names, etc. + } + + /** + * Run sign or renew (if required) command for ALL certificates + * @return array + * @throws \Exception + */ + public function signallcertsAction() + { + $backend = new Backend(); + // first setup the environment + $backend->configdRun("acmeclient setup"); + // run the command + $response = $backend->configdRun("acmeclient sign-all-certs"); + return array("result" => $response); + } +} 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 new file mode 100644 index 000000000..17e203ff1 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/SettingsController.php @@ -0,0 +1,116 @@ + "no change"); + + // TODO: How to force the system to write-out the cronjob? + + if ($this->request->isPost()) { + $mdlAcme = $this->getModel(); + $backend = new Backend(); + + // Setup cronjob if AcmeClient and AutoRenewal is enabled. + if ((string)$mdlAcme->settings->UpdateCron == "" and + (string)$mdlAcme->settings->autoRenewal == "1" and + (string)$mdlAcme->settings->enabled == "1") { + + $mdlCron = new Cron(); + // NOTE: Only configd actions are valid commands for cronjobs + // and they *must* provide a description that is not empty. + $cron_uuid = $mdlCron->newDailyJob( + "AcmeClient", + "acmeclient cron-auto-renew", + "AcmeClient Cronjob for Certificate AutoRenewal", + "*", + "1" + ); + $mdlAcme->settings->UpdateCron = $cron_uuid; + + // Save updated configuration. + if ($mdlCron->performValidation()->count() == 0) { + $mdlCron->serializeToConfig(); + // save data to config, do not validate because the current in memory model doesn't know about the + // cron item just created. + $mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true); + Config::getInstance()->save(); + // Regenerate the crontab + $backend->configdRun('template reload OPNsense/Cron'); + $result['result'] = "new"; + $result['uuid'] = $cron_uuid; + } else { + $result['result'] = "unable to add cron"; + } + // Delete cronjob if AcmeClient or AutoRenewal is disabled. + } elseif ((string)$mdlAcme->settings->UpdateCron != "" and + ((string)$mdlAcme->settings->autoRenewal == "0" or + (string)$mdlAcme->settings->enabled == "0")) { + + $cron_uuid = (string)$mdlAcme->settings->UpdateCron; + $mdlAcme->settings->UpdateCron = null; + $mdlCron = new Cron(); + if ($mdlCron->jobs->job->del($cron_uuid)) { + // if item is removed, serialize to config and save + $mdlCron->serializeToConfig(); + $mdlAcme->serializeToConfig($validateFullModel = false, $disable_validation = true); + Config::getInstance()->save(); + // Regenerate the crontab + $backend->configdRun('template reload OPNsense/Cron'); + $result['result'] = "deleted"; + } else { + $result['result'] = "unable to delete cron"; + } + } + } + + 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 new file mode 100644 index 000000000..5deb63343 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/Api/ValidationsController.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 validation 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('validations.validation.'.$uuid); + if ($node != null) { + // return node + return array("validation" => $node->getNodes()); + } + } else { + // generate new node, but don't save to disc + $node = $mdlAcme->validations->validation->add() ; + return array("validation" => $node->getNodes()); + } + return array(); + } + + /** + * update validation with given properties + * @param $uuid item unique id + * @return array + */ + public function setAction($uuid) + { + if ($this->request->isPost() && $this->request->hasPost("validation")) { + $mdlAcme = new AcmeClient(); + if ($uuid != null) { + $node = $mdlAcme->getNodeByReference('validations.validation.'.$uuid); + if ($node != null) { + $node->setNodes($this->request->getPost("validation")); + return $this->save($mdlAcme, $node, "validation"); + } + } + } + return array("result"=>"failed"); + } + + /** + * add new validation and set with attributes from post + * @return array + */ + public function addAction() + { + $result = array("result"=>"failed"); + if ($this->request->isPost() && $this->request->hasPost("validation")) { + $mdlAcme = new AcmeClient(); + $node = $mdlAcme->validations->validation->Add(); + $node->setNodes($this->request->getPost("validation")); + return $this->save($mdlAcme, $node, "validation"); + } + return $result; + } + + /** + * delete validation 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->validations->validation->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 validation 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('validations.validation.' . $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 validations + * @return array + */ + public function searchAction() + { + $this->sessionClose(); + $mdlAcme = new AcmeClient(); + $grid = new UIModelGrid($mdlAcme->validations->validation); + return $grid->fetchBindRequest( + $this->request, + array("enabled", "name", "description","validationid"), + "name" + ); + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/CertificatesController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/CertificatesController.php new file mode 100644 index 000000000..86640f1d1 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/CertificatesController.php @@ -0,0 +1,46 @@ +view->title = "Let's Encrypt Certificates"; + // include form definitions + $this->view->formDialogCertificate = $this->getForm("dialogCertificate"); + // choose template + $this->view->pick('OPNsense/AcmeClient/certificates'); + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/IndexController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/IndexController.php new file mode 100644 index 000000000..d6de7cf4a --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/IndexController.php @@ -0,0 +1,51 @@ +view->title = "Let's Encrypt Settings"; + // include form definitions + $this->view->settingsForm = $this->getForm("settings"); + // pick the template to serve + $this->view->pick('OPNsense/AcmeClient/settings'); + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/ValidationsController.php b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/ValidationsController.php new file mode 100644 index 000000000..bf1cb3d96 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/ValidationsController.php @@ -0,0 +1,46 @@ +view->title = "Let's Encrypt Domain Validation Methods"; + // include form definitions + $this->view->formDialogValidation = $this->getForm("dialogValidation"); + // choose template + $this->view->pick('OPNsense/AcmeClient/validations'); + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAccount.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAccount.xml new file mode 100644 index 000000000..2800717bd --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAccount.xml @@ -0,0 +1,33 @@ +
+ + account.enabled + + checkbox + Enable this account + + + account.name + + text + Name to identify this account. + + + account.description + + text + Description for this account. + + + account.email + + text + Optional e-mail address for this account. + + + account.certificateAuthority + + dropdown + + true + +
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 new file mode 100644 index 000000000..9dee719d4 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml @@ -0,0 +1,53 @@ +
+ + certificate.enabled + + checkbox + Enable this certificate + + + certificate.name + + text + Name to identify this certificate. + + + certificate.description + + text + Description for this certificate. + + + certificate.altNames + + select_multiple + + true +
NOTE:Cannot be altered once the certificate was signed by the Let's Encrypt Authority! You need to create a new certificate to add additional names.
]]>
+ Enter FQDN here. Finish with TAB. +
+ + certificate.account + + dropdown + + + + certificate.validationMethod + + dropdown + + + + certificate.autoRenewal + + checkbox + Enable automatic renewal for this certificate to prevent expiration. + + + certificate.renewInterval + + text + + +
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 new file mode 100644 index 000000000..532125434 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml @@ -0,0 +1,357 @@ +
+ + validation.enabled + + checkbox + Enable this validation + + + validation.name + + text + Name to identify this validation. + + + validation.description + + text + Description for this validation. + + + validation.method + + dropdown + + + + + header + + + validation.http_service + + dropdown + + + + + header + + + validation.http_opn_autodiscovery + + checkbox +
NOTE:This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.
]]>
+
+ + validation.http_opn_interface + + dropdown +
NOTE:This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.
]]>
+
+ + validation.http_opn_ipaddresses + + select_multiple + + true +
NOTE:This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.
]]>
+ Enter IP addresses here. Finish each with TAB. +
+ + + + header + + + validation.dns_service + + dropdown + + + + validation.dns_sleep + + text + + + + + header + + + validation.dns_ad_key + + text + + + + + header + + + validation.dns_ali_key + + text + + + + validation.dns_ali_secret + + text + + + + + header + + + validation.dns_aws_id + + text + + + + validation.dns_aws_secret + + text + + + + + header + + + validation.dns_cf_email + + text + + + + validation.dns_cf_key + + text + + + + + header + + + validation.dns_cx_key + + text + + + + validation.dns_cx_secret + + text + + + + + header + + + validation.dns_dp_id + + text + + + + validation.dns_dp_key + + text + + + + + header + + + validation.dns_gd_key + + text + + + + validation.dns_gd_secret + + text + + + + + header + + + validation.dns_ispconfig_user + + text + + + + validation.dns_ispconfig_password + + text + + + + validation.dns_ispconfig_api + + text + + + + validation.dns_ispconfig_insecure + + text + + + + + header + + + validation.dns_lexicon_provider + + dropdown + + + + validation.dns_lexicon_user + + text + + + + validation.dns_lexicon_token + + text + + + + + header + + + validation.dns_lua_email + + text + + + + validation.dns_lua_key + + text + + + + + header + + + validation.dns_me_key + + text + + + + validation.dns_me_secret + + text + + + + + header + + + validation.dns_nsupdate_server + + text + + + + validation.dns_nsupdate_key + + textbox + + + + + header + + + validation.dns_ovh_app_key + + text + + + + validation.dns_ovh_app_secret + + text + + + + validation.dns_ovh_consumer_key + + text + + + + validation.dns_ovh_endpoint + + text + acme.sh documentation for further information.]]> + + + + header + + + validation.dns_pdns_url + + text + + + + validation.dns_pdns_serverid + + text + + + + validation.dns_pdns_token + + text + + +
diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml new file mode 100644 index 000000000..87871c156 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml @@ -0,0 +1,27 @@ +
+ + acmeclient.settings.enabled + + checkbox + + + + acmeclient.settings.autoRenewal + + checkbox + + + + acmeclient.settings.environment + + dropdown + relaxed rate limits.
NOTE:Certificates signed by the staging environment are NOT valid. You need to forcefully re-sign (or delete and re-create) them after switching from staging to production environment.
]]>
+
+ + acmeclient.settings.challengePort + + text + + true + +
diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/ACL/ACL.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/ACL/ACL.xml new file mode 100644 index 000000000..11a53cc6e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/ACL/ACL.xml @@ -0,0 +1,10 @@ + + + Services: Let's Encrypt + + ui/acmeclient/* + api/acmeclient/* + diag_logs_acmeclient.php + + + 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 new file mode 100644 index 000000000..de4a4af9a --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php @@ -0,0 +1,68 @@ +certificates->certificate->__items as $certificate) { + if ((string)$certificateid === (string)$certificate->certificateid) { + return $certificate; + } + } + return null; + } + + /** + * check if module is enabled + * @return bool is the AcmeClient enabled (1 or more active certificates) + */ + public function isEnabled() + { + foreach ($this->certificates->certificate->__items as $certificate) { + if ((string)$certificate->enabled == "1") { + return true; + } + } + return false; + } +} 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 new file mode 100644 index 000000000..c7be58403 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml @@ -0,0 +1,373 @@ + + + //OPNsense/AcmeClient + 1.0.0 + + a secure Let's Encrypt plugin + + + + + 0 + Y + + + 1 + Y + + + + + OPNsense.Cron.Cron + jobs.job + description + + /AcmeClient/ + + + + Related cron not found. + N + + + Y + prod + + Production Environment [default] + Staging Environment + + + + 43580 + 1024 + 65535 + Y + + + + + + N + + + 1 + Y + + + Y + /^([0-9a-zA-Z._]){1,255}$/u + Should be a string between 1 and 255 characters. + + + N + /^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u + Should be a string between 1 and 255 characters. + + + N + + + Y + letsencrypt + + Let's Encrypt CA + + + + + N + + + + N + + + + + + + N + + + 1 + Y + + + Y + /^([0-9a-zA-Z._]){1,255}$/u + Should be a string between 1 and 255 characters. + + + N + /^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u + Should be a string between 1 and 255 characters. + + + N + Y + + /^((([0-9a-zA-Z._\-\*]+\.[0-9a-zA-Z._\-\*]+(-[0-9]+)?)([,]){0,1}))*/u + lower + Please provide a valid FQDN, i.e. www.example.com or mail.example.com. + + + + + + Related item not found + N + Y + + + + + + Related item not found + N + Y + + + 1 + Y + + + Y + 1 + 60 + 60 + + + + N + + + + N + + + + + + + N + + + 1 + Y + + + Y + /^([0-9a-zA-Z._]){1,255}$/u + Should be a string between 1 and 255 characters. + + + N + /^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u + Should be a string between 1 and 255 characters. + + + Y + http01 + + HTTP-01 + DNS-01 + + + + Y + opnsense + + OPNsense port forward (specify Interface or IP) + + + + + 1 + N + + + + N + wan + + /^(?!0).*$/ + + + + N + Y + + + + Y + dns_nsupdate + + Alwaysdata.com API + aliyun.com API + AWS Route 53 + CloudFlare.com API + CloudXNS.com API + DNSPod.cn API + GoDaddy.com API + ISPConfig 3.1+ API + lexicon DNS API + LuaDNS.com API + DNSMadeEasy.com API + nsupdate (RFC 2136) + OVH, kimsufi, soyoustart and runabove API + PowerDNS.com API + + + + 1 + 10000 + 120 + Please specify a value between 1 and 10000. + Y + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + 1 + + + N + cloudflare + + Cloudflare API + Namesilo API + + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + + + diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml new file mode 100644 index 000000000..d030891d5 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt new file mode 100644 index 000000000..7d1d54bb5 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt @@ -0,0 +1,87 @@ +{# + +Copyright (C) 2017 Frank Wall +OPNsense® is Copyright © 2014-2015 by Deciso B.V. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +#} + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
{{ lang._('Enabled') }}{{ lang._('Name') }}{{ lang._('E-Mail') }}{{ lang._('Commands') }}{{ lang._('ID') }}
+ + +
+
+
+ +{# include dialogs #} +{{ partial("layout_partials/base_dialog",['fields':formDialogAccount,'id':'DialogAccount','label':'Edit Account'])}} 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 new file mode 100644 index 000000000..07391082f --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt @@ -0,0 +1,366 @@ +{# + +(Partially duplicates code from opnsense_bootgrid_plugin.js.) + +Copyright (C) 2017 Frank Wall +Copyright (C) 2015 Deciso B.V. +OPNsense® is Copyright © 2014-2015 by Deciso B.V. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +#} + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('Enabled') }}{{ lang._('Certificate Name') }}{{ lang._('Multi-Domain (SAN)') }}{{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('ID') }}
+ + +
+
+
+
+ +
+
+
+ {{ lang._("Use the Issue/Renew button to let the acme client automatically issue any new certificate and renew existing certificates (only if required). If you want to only issue/renew or revoke a single certificate, use the buttons in the Commands column. This will forcefully issue/renew the certificate, even if it's not required.") }} {{ lang._("The process may take some time and thus will run in the background, you will not get any notification in the GUI. Use the log file to monitor the progress and to see error messages.") }} +

+
+
+ +{# include dialogs #} +{{ partial("layout_partials/base_dialog",['fields':formDialogCertificate,'id':'DialogCertificate','label':'Edit Certificate'])}} 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 new file mode 100644 index 000000000..011da5daa --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt @@ -0,0 +1,207 @@ +{# + +Copyright (C) 2017 Frank Wall +OPNsense® is Copyright © 2014-2015 by Deciso B.V. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +#} + + + + + +
+
+{{ partial("layout_partials/base_form",['fields':settingsForm,'id':'frm_settings'])}} +
+
+
+ + +
+
+
+ {{ lang._("Please read the official ") }}{{ lang._("Let's Encrypt documentation") }}{{ lang._(" before using this plugin. Otherwise you will easily hit it's ") }}{{ lang._("rate limits") }}{{ lang._(" and thus all your attempts to issue a certificate will fail. ") }}{{ lang._("Please use Let's Encrypts ") }}{{ lang._("Staging servers") }}{{ lang._(" when using this plugin for the first time or while testing a new validation method. You'll have to re-issue your certificates when switching from staging to production servers to get valid certificates.") }} +
+ {{ lang._("Please use the ") }}{{ lang._("GitHub Issue Tracker ") }}{{ lang._("to report bugs or request new features.") }} +
+
+

Includes code from the Neilpang/acme.sh project. Licensed under GPLv3.
Let's Encrypt™ is a trademark of the Internet Security Research Group. All rights reserved.

+
+
+
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 new file mode 100644 index 000000000..260d583f3 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt @@ -0,0 +1,87 @@ +{# + +Copyright (C) 2017 Frank Wall +OPNsense® is Copyright © 2014-2015 by Deciso B.V. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +#} + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
{{ lang._('Enabled') }}{{ lang._('Name') }}{{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('ID') }}
+ + +
+
+
+ +{# include dialogs #} +{{ partial("layout_partials/base_dialog",['fields':formDialogValidation,'id':'DialogValidation','label':'Edit Validation Method'])}} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh new file mode 100755 index 000000000..7ec84e6fe --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh @@ -0,0 +1,4649 @@ +#!/usr/bin/env sh + +VER=2.6.6 + +PROJECT_NAME="acme.sh" + +PROJECT_ENTRY="acme.sh" + +PROJECT="https://github.com/Neilpang/$PROJECT_NAME" + +DEFAULT_INSTALL_HOME="$HOME/.$PROJECT_NAME" +_SCRIPT_="$0" + +_SUB_FOLDERS="dnsapi deploy" + +DEFAULT_CA="https://acme-v01.api.letsencrypt.org" +DEFAULT_AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf" + +DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" +DEFAULT_ACCOUNT_EMAIL="" + +DEFAULT_ACCOUNT_KEY_LENGTH=2048 +DEFAULT_DOMAIN_KEY_LENGTH=2048 + +DEFAULT_OPENSSL_BIN="openssl" + +STAGE_CA="https://acme-staging.api.letsencrypt.org" + +VTYPE_HTTP="http-01" +VTYPE_DNS="dns-01" +VTYPE_TLS="tls-sni-01" +#VTYPE_TLS2="tls-sni-02" + +LOCAL_ANY_ADDRESS="0.0.0.0" + +MAX_RENEW=60 + +DEFAULT_DNS_SLEEP=120 + +NO_VALUE="no" + +W_TLS="tls" + +STATE_VERIFIED="verified_ok" + +BEGIN_CSR="-----BEGIN CERTIFICATE REQUEST-----" +END_CSR="-----END CERTIFICATE REQUEST-----" + +BEGIN_CERT="-----BEGIN CERTIFICATE-----" +END_CERT="-----END CERTIFICATE-----" + +RENEW_SKIP=2 + +ECC_SEP="_" +ECC_SUFFIX="${ECC_SEP}ecc" + +LOG_LEVEL_1=1 +LOG_LEVEL_2=2 +LOG_LEVEL_3=3 +DEFAULT_LOG_LEVEL="$LOG_LEVEL_1" + +_DEBUG_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh" + +__INTERACTIVE="" +if [ -t 1 ]; then + __INTERACTIVE="1" +fi + +__green() { + if [ "$__INTERACTIVE" ]; then + printf '\033[1;31;32m' + fi + printf -- "$1" + if [ "$__INTERACTIVE" ]; then + printf '\033[0m' + fi +} + +__red() { + if [ "$__INTERACTIVE" ]; then + printf '\033[1;31;40m' + fi + printf -- "$1" + if [ "$__INTERACTIVE" ]; then + printf '\033[0m' + fi +} + +_printargs() { + if [ -z "$NO_TIMESTAMP" ] || [ "$NO_TIMESTAMP" = "0" ]; then + printf -- "%s" "[$(date)] " + fi + if [ -z "$2" ]; then + printf -- "%s" "$1" + else + printf -- "%s" "$1='$2'" + fi + printf "\n" +} + +_dlg_versions() { + echo "Diagnosis versions: " + echo "openssl:$OPENSSL_BIN" + if _exists "$OPENSSL_BIN"; then + $OPENSSL_BIN version 2>&1 + else + echo "$OPENSSL_BIN doesn't exists." + fi + + echo "apache:" + if [ "$_APACHECTL" ] && _exists "$_APACHECTL"; then + _APACHECTL -V 2>&1 + else + echo "apache doesn't exists." + fi + + echo "nc:" + if _exists "nc"; then + nc -h 2>&1 + else + _debug "nc doesn't exists." + fi +} + +_log() { + [ -z "$LOG_FILE" ] && return + _printargs "$@" >>"$LOG_FILE" +} + +_info() { + _log "$@" + _printargs "$@" +} + +_err() { + _log "$@" + if [ -z "$NO_TIMESTAMP" ] || [ "$NO_TIMESTAMP" = "0" ]; then + printf -- "%s" "[$(date)] " >&2 + fi + if [ -z "$2" ]; then + __red "$1" >&2 + else + __red "$1='$2'" >&2 + fi + printf "\n" >&2 + return 1 +} + +_usage() { + __red "$@" >&2 + printf "\n" >&2 +} + +_debug() { + if [ -z "$LOG_LEVEL" ] || [ "$LOG_LEVEL" -ge "$LOG_LEVEL_1" ]; then + _log "$@" + fi + if [ -z "$DEBUG" ]; then + return + fi + _printargs "$@" >&2 +} + +_debug2() { + if [ "$LOG_LEVEL" ] && [ "$LOG_LEVEL" -ge "$LOG_LEVEL_2" ]; then + _log "$@" + fi + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _debug "$@" + fi +} + +_debug3() { + if [ "$LOG_LEVEL" ] && [ "$LOG_LEVEL" -ge "$LOG_LEVEL_3" ]; then + _log "$@" + fi + if [ "$DEBUG" ] && [ "$DEBUG" -ge "3" ]; then + _debug "$@" + fi +} + +_startswith() { + _str="$1" + _sub="$2" + echo "$_str" | grep "^$_sub" >/dev/null 2>&1 +} + +_endswith() { + _str="$1" + _sub="$2" + echo "$_str" | grep -- "$_sub\$" >/dev/null 2>&1 +} + +_contains() { + _str="$1" + _sub="$2" + echo "$_str" | grep -- "$_sub" >/dev/null 2>&1 +} + +_hasfield() { + _str="$1" + _field="$2" + _sep="$3" + if [ -z "$_field" ]; then + _usage "Usage: str field [sep]" + return 1 + fi + + if [ -z "$_sep" ]; then + _sep="," + fi + + for f in $(echo "$_str" | tr ',' ' '); do + if [ "$f" = "$_field" ]; then + _debug2 "'$_str' contains '$_field'" + return 0 #contains ok + fi + done + _debug2 "'$_str' does not contain '$_field'" + return 1 #not contains +} + +_getfield() { + _str="$1" + _findex="$2" + _sep="$3" + + if [ -z "$_findex" ]; then + _usage "Usage: str field [sep]" + return 1 + fi + + if [ -z "$_sep" ]; then + _sep="," + fi + + _ffi="$_findex" + while [ "$_ffi" -gt "0" ]; do + _fv="$(echo "$_str" | cut -d "$_sep" -f "$_ffi")" + if [ "$_fv" ]; then + printf -- "%s" "$_fv" + return 0 + fi + _ffi="$(_math "$_ffi" - 1)" + done + + printf -- "%s" "$_str" + +} + +_exists() { + cmd="$1" + if [ -z "$cmd" ]; then + _usage "Usage: _exists cmd" + return 1 + fi + + if eval type type >/dev/null 2>&1; then + eval type "$cmd" >/dev/null 2>&1 + elif command >/dev/null 2>&1; then + command -v "$cmd" >/dev/null 2>&1 + else + which "$cmd" >/dev/null 2>&1 + fi + ret="$?" + _debug3 "$cmd exists=$ret" + return $ret +} + +#a + b +_math() { + _m_opts="$@" + printf "%s" "$(($_m_opts))" +} + +_h_char_2_dec() { + _ch=$1 + case "${_ch}" in + a | A) + printf "10" + ;; + b | B) + printf "11" + ;; + c | C) + printf "12" + ;; + d | D) + printf "13" + ;; + e | E) + printf "14" + ;; + f | F) + printf "15" + ;; + *) + printf "%s" "$_ch" + ;; + esac + +} + +_URGLY_PRINTF="" +if [ "$(printf '\x41')" != 'A' ]; then + _URGLY_PRINTF=1 +fi + +_h2b() { + hex=$(cat) + i=1 + j=2 + + _debug3 _URGLY_PRINTF "$_URGLY_PRINTF" + while true; do + if [ -z "$_URGLY_PRINTF" ]; then + h="$(printf "%s" "$hex" | cut -c $i-$j)" + if [ -z "$h" ]; then + break + fi + printf "\x$h%s" + else + ic="$(printf "%s" "$hex" | cut -c $i)" + jc="$(printf "%s" "$hex" | cut -c $j)" + if [ -z "$ic$jc" ]; then + break + fi + ic="$(_h_char_2_dec "$ic")" + jc="$(_h_char_2_dec "$jc")" + printf '\'"$(printf "%o" "$(_math "$ic" \* 16 + $jc)")""%s" + fi + + i="$(_math "$i" + 2)" + j="$(_math "$j" + 2)" + + done +} + +#hex string +_hex() { + _str="$1" + _str_len=${#_str} + _h_i=1 + while [ "$_h_i" -le "$_str_len" ]; do + _str_c="$(printf "%s" "$_str" | cut -c "$_h_i")" + printf "%02x" "'$_str_c" + _h_i="$(_math "$_h_i" + 1)" + done +} + +#options file +_sed_i() { + options="$1" + filename="$2" + if [ -z "$filename" ]; then + _usage "Usage:_sed_i options filename" + return 1 + fi + _debug2 options "$options" + if sed -h 2>&1 | grep "\-i\[SUFFIX]" >/dev/null 2>&1; then + _debug "Using sed -i" + sed -i "$options" "$filename" + else + _debug "No -i support in sed" + text="$(cat "$filename")" + echo "$text" | sed "$options" >"$filename" + fi +} + +_egrep_o() { + if ! egrep -o "$1" 2>/dev/null; then + sed -n 's/.*\('"$1"'\).*/\1/p' + fi +} + +#Usage: file startline endline +_getfile() { + filename="$1" + startline="$2" + endline="$3" + if [ -z "$endline" ]; then + _usage "Usage: file startline endline" + return 1 + fi + + i="$(grep -n -- "$startline" "$filename" | cut -d : -f 1)" + if [ -z "$i" ]; then + _err "Can not find start line: $startline" + return 1 + fi + i="$(_math "$i" + 1)" + _debug i "$i" + + j="$(grep -n -- "$endline" "$filename" | cut -d : -f 1)" + if [ -z "$j" ]; then + _err "Can not find end line: $endline" + return 1 + fi + j="$(_math "$j" - 1)" + _debug j "$j" + + sed -n "$i,${j}p" "$filename" + +} + +#Usage: multiline +_base64() { + [ "" ] #urgly + if [ "$1" ]; then + _debug3 "base64 multiline:'$1'" + $OPENSSL_BIN base64 -e + else + _debug3 "base64 single line." + $OPENSSL_BIN base64 -e | tr -d '\r\n' + fi +} + +#Usage: multiline +_dbase64() { + if [ "$1" ]; then + $OPENSSL_BIN base64 -d -A + else + $OPENSSL_BIN base64 -d + fi +} + +#Usage: hashalg [outputhex] +#Output Base64-encoded digest +_digest() { + alg="$1" + if [ -z "$alg" ]; then + _usage "Usage: _digest hashalg" + return 1 + fi + + outputhex="$2" + + if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then + if [ "$outputhex" ]; then + $OPENSSL_BIN dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' ' + else + $OPENSSL_BIN dgst -"$alg" -binary | _base64 + fi + else + _err "$alg is not supported yet" + return 1 + fi + +} + +#Usage: hashalg secret_hex [outputhex] +#Output binary hmac +_hmac() { + alg="$1" + secret_hex="$2" + outputhex="$3" + + if [ -z "$secret_hex" ]; then + _usage "Usage: _hmac hashalg secret [outputhex]" + return 1 + fi + + if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ]; then + if [ "$outputhex" ]; then + $OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" | cut -d = -f 2 | tr -d ' ' + else + $OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary + fi + else + _err "$alg is not supported yet" + return 1 + fi + +} + +#Usage: keyfile hashalg +#Output: Base64-encoded signature value +_sign() { + keyfile="$1" + alg="$2" + if [ -z "$alg" ]; then + _usage "Usage: _sign keyfile hashalg" + return 1 + fi + + _sign_openssl="$OPENSSL_BIN dgst -sign $keyfile " + if [ "$alg" = "sha256" ]; then + _sign_openssl="$_sign_openssl -$alg" + else + _err "$alg is not supported yet" + return 1 + fi + + if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + $_sign_openssl | _base64 + elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + if ! _signedECText="$($_sign_openssl | $OPENSSL_BIN asn1parse -inform DER)"; then + _err "Sign failed: $_sign_openssl" + _err "Key file: $keyfile" + _err "Key content:$(wc -l <"$keyfile") lises" + return 1 + fi + _debug3 "_signedECText" "$_signedECText" + _ec_r="$(echo "$_signedECText" | _head_n 2 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" + _debug3 "_ec_r" "$_ec_r" + _ec_s="$(echo "$_signedECText" | _head_n 3 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" + _debug3 "_ec_s" "$_ec_s" + printf "%s" "$_ec_r$_ec_s" | _h2b | _base64 + else + _err "Unknown key file format." + return 1 + fi + +} + +#keylength +_isEccKey() { + _length="$1" + + if [ -z "$_length" ]; then + return 1 + fi + + [ "$_length" != "1024" ] \ + && [ "$_length" != "2048" ] \ + && [ "$_length" != "3072" ] \ + && [ "$_length" != "4096" ] \ + && [ "$_length" != "8192" ] +} + +# _createkey 2048|ec-256 file +_createkey() { + length="$1" + f="$2" + _debug2 "_createkey for file:$f" + eccname="$length" + if _startswith "$length" "ec-"; then + length=$(printf "%s" "$length" | cut -d '-' -f 2-100) + + if [ "$length" = "256" ]; then + eccname="prime256v1" + fi + if [ "$length" = "384" ]; then + eccname="secp384r1" + fi + if [ "$length" = "521" ]; then + eccname="secp521r1" + fi + + fi + + if [ -z "$length" ]; then + length=2048 + fi + + _debug "Use length $length" + + if _isEccKey "$length"; then + _debug "Using ec name: $eccname" + $OPENSSL_BIN ecparam -name "$eccname" -genkey 2>/dev/null >"$f" + else + _debug "Using RSA: $length" + $OPENSSL_BIN genrsa "$length" 2>/dev/null >"$f" + fi + + if [ "$?" != "0" ]; then + _err "Create key error." + return 1 + fi +} + +#domain +_is_idn() { + _is_idn_d="$1" + _debug2 _is_idn_d "$_is_idn_d" + _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '.,-') + _debug2 _idn_temp "$_idn_temp" + [ "$_idn_temp" ] +} + +#aa.com +#aa.com,bb.com,cc.com +_idn() { + __idn_d="$1" + if ! _is_idn "$__idn_d"; then + printf "%s" "$__idn_d" + return 0 + fi + + if _exists idn; then + if _contains "$__idn_d" ','; then + _i_first="1" + for f in $(echo "$__idn_d" | tr ',' ' '); do + [ -z "$f" ] && continue + if [ -z "$_i_first" ]; then + printf "%s" "," + else + _i_first="" + fi + idn --quiet "$f" | tr -d "\r\n" + done + else + idn "$__idn_d" | tr -d "\r\n" + fi + else + _err "Please install idn to process IDN names." + fi +} + +#_createcsr cn san_list keyfile csrfile conf +_createcsr() { + _debug _createcsr + domain="$1" + domainlist="$2" + csrkey="$3" + csr="$4" + csrconf="$5" + _debug2 domain "$domain" + _debug2 domainlist "$domainlist" + _debug2 csrkey "$csrkey" + _debug2 csr "$csr" + _debug2 csrconf "$csrconf" + + printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]\n\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment" >"$csrconf" + + if [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then + #single domain + _info "Single domain" "$domain" + else + domainlist="$(_idn "$domainlist")" + _debug2 domainlist "$domainlist" + if _contains "$domainlist" ","; then + alt="DNS:$(echo "$domainlist" | sed "s/,/,DNS:/g")" + else + alt="DNS:$domainlist" + fi + #multi + _info "Multi domain" "$alt" + printf -- "\nsubjectAltName=$alt" >>"$csrconf" + fi + if [ "$Le_OCSP_Staple" ] || [ "$Le_OCSP_Stable" ]; then + _savedomainconf Le_OCSP_Staple "$Le_OCSP_Staple" + _cleardomainconf Le_OCSP_Stable + printf -- "\nbasicConstraints = CA:FALSE\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >>"$csrconf" + fi + + _csr_cn="$(_idn "$domain")" + _debug2 _csr_cn "$_csr_cn" + $OPENSSL_BIN req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" +} + +#_signcsr key csr conf cert +_signcsr() { + key="$1" + csr="$2" + conf="$3" + cert="$4" + _debug "_signcsr" + + _msg="$($OPENSSL_BIN x509 -req -days 365 -in "$csr" -signkey "$key" -extensions v3_req -extfile "$conf" -out "$cert" 2>&1)" + _ret="$?" + _debug "$_msg" + return $_ret +} + +#_csrfile +_readSubjectFromCSR() { + _csrfile="$1" + if [ -z "$_csrfile" ]; then + _usage "_readSubjectFromCSR mycsr.csr" + return 1 + fi + $OPENSSL_BIN req -noout -in "$_csrfile" -subject | _egrep_o "CN=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d '\n' +} + +#_csrfile +#echo comma separated domain list +_readSubjectAltNamesFromCSR() { + _csrfile="$1" + if [ -z "$_csrfile" ]; then + _usage "_readSubjectAltNamesFromCSR mycsr.csr" + return 1 + fi + + _csrsubj="$(_readSubjectFromCSR "$_csrfile")" + _debug _csrsubj "$_csrsubj" + + _dnsAltnames="$($OPENSSL_BIN req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')" + _debug _dnsAltnames "$_dnsAltnames" + + if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then + _debug "AltNames contains subject" + _dnsAltnames="$(printf "%s" "$_dnsAltnames," | sed "s/DNS:$_csrsubj,//g")" + else + _debug "AltNames doesn't contain subject" + fi + + printf "%s" "$_dnsAltnames" | sed "s/DNS://g" +} + +#_csrfile +_readKeyLengthFromCSR() { + _csrfile="$1" + if [ -z "$_csrfile" ]; then + _usage "_readKeyLengthFromCSR mycsr.csr" + return 1 + fi + + _outcsr="$($OPENSSL_BIN req -noout -text -in "$_csrfile")" + if _contains "$_outcsr" "Public Key Algorithm: id-ecPublicKey"; then + _debug "ECC CSR" + echo "$_outcsr" | _egrep_o "^ *ASN1 OID:.*" | cut -d ':' -f 2 | tr -d ' ' + else + _debug "RSA CSR" + echo "$_outcsr" | _egrep_o "^ *Public-Key:.*" | cut -d '(' -f 2 | cut -d ' ' -f 1 + fi +} + +_ss() { + _port="$1" + + if _exists "ss"; then + _debug "Using: ss" + ss -ntpl | grep ":$_port " + return 0 + fi + + if _exists "netstat"; then + _debug "Using: netstat" + if netstat -h 2>&1 | grep "\-p proto" >/dev/null; then + #for windows version netstat tool + netstat -an -p tcp | grep "LISTENING" | grep ":$_port " + else + if netstat -help 2>&1 | grep "\-p protocol" >/dev/null; then + netstat -an -p tcp | grep LISTEN | grep ":$_port " + elif netstat -help 2>&1 | grep -- '-P protocol' >/dev/null; then + #for solaris + netstat -an -P tcp | grep "\.$_port " | grep "LISTEN" + else + netstat -ntpl | grep ":$_port " + fi + fi + return 0 + fi + + return 1 +} + +#domain [password] [isEcc] +toPkcs() { + domain="$1" + pfxPassword="$2" + if [ -z "$domain" ]; then + _usage "Usage: $PROJECT_ENTRY --toPkcs -d domain [--password pfx-password]" + return 1 + fi + + _isEcc="$3" + + _initpath "$domain" "$_isEcc" + + if [ "$pfxPassword" ]; then + $OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" -password "pass:$pfxPassword" + else + $OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" + fi + + if [ "$?" = "0" ]; then + _info "Success, Pfx is exported to: $CERT_PFX_PATH" + fi + +} + +#[2048] +createAccountKey() { + _info "Creating account key" + if [ -z "$1" ]; then + _usage "Usage: $PROJECT_ENTRY --createAccountKey --accountkeylength 2048" + return + fi + + length=$1 + _create_account_key "$length" + +} + +_create_account_key() { + + length=$1 + + if [ -z "$length" ] || [ "$length" = "$NO_VALUE" ]; then + _debug "Use default length $DEFAULT_ACCOUNT_KEY_LENGTH" + length="$DEFAULT_ACCOUNT_KEY_LENGTH" + fi + + _debug length "$length" + _initpath + + mkdir -p "$CA_DIR" + if [ -f "$ACCOUNT_KEY_PATH" ]; then + _info "Account key exists, skip" + return + else + #generate account key + _createkey "$length" "$ACCOUNT_KEY_PATH" + fi + +} + +#domain [length] +createDomainKey() { + _info "Creating domain key" + if [ -z "$1" ]; then + _usage "Usage: $PROJECT_ENTRY --createDomainKey -d domain.com [ --keylength 2048 ]" + return + fi + + domain=$1 + length=$2 + + if [ -z "$length" ]; then + _debug "Use DEFAULT_DOMAIN_KEY_LENGTH=$DEFAULT_DOMAIN_KEY_LENGTH" + length="$DEFAULT_DOMAIN_KEY_LENGTH" + fi + + _initpath "$domain" "$length" + + if [ ! -f "$CERT_KEY_PATH" ] || ([ "$FORCE" ] && ! [ "$IS_RENEW" ]); then + _createkey "$length" "$CERT_KEY_PATH" + else + if [ "$IS_RENEW" ]; then + _info "Domain key exists, skip" + return 0 + else + _err "Domain key exists, do you want to overwrite the key?" + _err "Add '--force', and try again." + return 1 + fi + fi + +} + +# domain domainlist isEcc +createCSR() { + _info "Creating csr" + if [ -z "$1" ]; then + _usage "Usage: $PROJECT_ENTRY --createCSR -d domain1.com [-d domain2.com -d domain3.com ... ]" + return + fi + + domain="$1" + domainlist="$2" + _isEcc="$3" + + _initpath "$domain" "$_isEcc" + + if [ -f "$CSR_PATH" ] && [ "$IS_RENEW" ] && [ -z "$FORCE" ]; then + _info "CSR exists, skip" + return + fi + + if [ ! -f "$CERT_KEY_PATH" ]; then + _err "The key file is not found: $CERT_KEY_PATH" + _err "Please create the key file first." + return 1 + fi + _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" + +} + +_urlencode() { + tr '/+' '_-' | tr -d '= ' +} + +_time2str() { + #BSD + if date -u -d@"$1" 2>/dev/null; then + return + fi + + #Linux + if date -u -r "$1" 2>/dev/null; then + return + fi + + #Soaris + if _exists adb; then + _t_s_a=$(echo "0t${1}=Y" | adb) + echo "$_t_s_a" + fi + +} + +_normalizeJson() { + sed "s/\" *: *\([\"{\[]\)/\":\1/g" | sed "s/^ *\([^ ]\)/\1/" | tr -d "\r\n" +} + +_stat() { + #Linux + if stat -c '%U:%G' "$1" 2>/dev/null; then + return + fi + + #BSD + if stat -f '%Su:%Sg' "$1" 2>/dev/null; then + return + fi + + return 1 #error, 'stat' not found +} + +#keyfile +_calcjwk() { + keyfile="$1" + if [ -z "$keyfile" ]; then + _usage "Usage: _calcjwk keyfile" + return 1 + fi + + if [ "$JWK_HEADER" ] && [ "$__CACHED_JWK_KEY_FILE" = "$keyfile" ]; then + _debug2 "Use cached jwk for file: $__CACHED_JWK_KEY_FILE" + return 0 + fi + + if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + _debug "RSA key" + pub_exp=$($OPENSSL_BIN rsa -in "$keyfile" -noout -text | grep "^publicExponent:" | cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1) + if [ "${#pub_exp}" = "5" ]; then + pub_exp=0$pub_exp + fi + _debug3 pub_exp "$pub_exp" + + e=$(echo "$pub_exp" | _h2b | _base64) + _debug3 e "$e" + + modulus=$($OPENSSL_BIN rsa -in "$keyfile" -modulus -noout | cut -d '=' -f 2) + _debug3 modulus "$modulus" + n="$(printf "%s" "$modulus" | _h2b | _base64 | _urlencode)" + _debug3 n "$n" + + jwk='{"e": "'$e'", "kty": "RSA", "n": "'$n'"}' + _debug3 jwk "$jwk" + + JWK_HEADER='{"alg": "RS256", "jwk": '$jwk'}' + JWK_HEADERPLACE_PART1='{"nonce": "' + JWK_HEADERPLACE_PART2='", "alg": "RS256", "jwk": '$jwk'}' + elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + _debug "EC key" + crv="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" | cut -d ":" -f 2 | tr -d " \r\n")" + _debug3 crv "$crv" + + if [ -z "$crv" ]; then + _debug "Let's try ASN1 OID" + crv_oid="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^ASN1 OID:" | cut -d ":" -f 2 | tr -d " \r\n")" + _debug3 crv_oid "$crv_oid" + case "${crv_oid}" in + "prime256v1") + crv="P-256" + ;; + "secp384r1") + crv="P-384" + ;; + "secp521r1") + crv="P-521" + ;; + *) + _err "ECC oid : $crv_oid" + return 1 + ;; + esac + _debug3 crv "$crv" + fi + + pubi="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n pub: | cut -d : -f 1)" + pubi=$(_math "$pubi" + 1) + _debug3 pubi "$pubi" + + pubj="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n "ASN1 OID:" | cut -d : -f 1)" + pubj=$(_math "$pubj" - 1) + _debug3 pubj "$pubj" + + pubtext="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | sed -n "$pubi,${pubj}p" | tr -d " \n\r")" + _debug3 pubtext "$pubtext" + + xlen="$(printf "%s" "$pubtext" | tr -d ':' | wc -c)" + xlen=$(_math "$xlen" / 4) + _debug3 xlen "$xlen" + + xend=$(_math "$xlen" + 1) + x="$(printf "%s" "$pubtext" | cut -d : -f 2-"$xend")" + _debug3 x "$x" + + x64="$(printf "%s" "$x" | tr -d : | _h2b | _base64 | _urlencode)" + _debug3 x64 "$x64" + + xend=$(_math "$xend" + 1) + y="$(printf "%s" "$pubtext" | cut -d : -f "$xend"-10000)" + _debug3 y "$y" + + y64="$(printf "%s" "$y" | tr -d : | _h2b | _base64 | _urlencode)" + _debug3 y64 "$y64" + + jwk='{"crv": "'$crv'", "kty": "EC", "x": "'$x64'", "y": "'$y64'"}' + _debug3 jwk "$jwk" + + JWK_HEADER='{"alg": "ES256", "jwk": '$jwk'}' + JWK_HEADERPLACE_PART1='{"nonce": "' + JWK_HEADERPLACE_PART2='", "alg": "ES256", "jwk": '$jwk'}' + else + _err "Only RSA or EC key is supported." + return 1 + fi + + _debug3 JWK_HEADER "$JWK_HEADER" + __CACHED_JWK_KEY_FILE="$keyfile" +} + +_time() { + date -u "+%s" +} + +_mktemp() { + if _exists mktemp; then + if mktemp 2>/dev/null; then + return 0 + elif _contains "$(mktemp 2>&1)" "-t prefix" && mktemp -t "$PROJECT_NAME" 2>/dev/null; then + #for Mac osx + return 0 + fi + fi + if [ -d "/tmp" ]; then + echo "/tmp/${PROJECT_NAME}wefADf24sf.$(_time).tmp" + return 0 + elif [ "$LE_TEMP_DIR" ] && mkdir -p "$LE_TEMP_DIR"; then + echo "/$LE_TEMP_DIR/wefADf24sf.$(_time).tmp" + return 0 + fi + _err "Can not create temp file." +} + +_inithttp() { + + if [ -z "$HTTP_HEADER" ] || ! touch "$HTTP_HEADER"; then + HTTP_HEADER="$(_mktemp)" + _debug2 HTTP_HEADER "$HTTP_HEADER" + fi + + if [ "$__HTTP_INITIALIZED" ]; then + if [ "$_ACME_CURL$_ACME_WGET" ]; then + _debug2 "Http already initialized." + return 0 + fi + fi + + if [ -z "$_ACME_CURL" ] && _exists "curl"; then + _ACME_CURL="curl -L --silent --dump-header $HTTP_HEADER " + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _CURL_DUMP="$(_mktemp)" + _ACME_CURL="$_ACME_CURL --trace-ascii $_CURL_DUMP " + fi + + if [ "$CA_BUNDLE" ]; then + _ACME_CURL="$_ACME_CURL --cacert $CA_BUNDLE " + fi + + fi + + if [ -z "$_ACME_WGET" ] && _exists "wget"; then + _ACME_WGET="wget -q" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _ACME_WGET="$_ACME_WGET -d " + fi + if [ "$CA_BUNDLE" ]; then + _ACME_WGET="$_ACME_WGET --ca-certificate $CA_BUNDLE " + fi + fi + + __HTTP_INITIALIZED=1 + +} + +# body url [needbase64] [POST|PUT] +_post() { + body="$1" + url="$2" + needbase64="$3" + httpmethod="$4" + + if [ -z "$httpmethod" ]; then + httpmethod="POST" + fi + _debug $httpmethod + _debug "url" "$url" + _debug2 "body" "$body" + + _inithttp + + if [ "$_ACME_CURL" ]; then + _CURL="$_ACME_CURL" + if [ "$HTTPS_INSECURE" ]; then + _CURL="$_CURL --insecure " + fi + _debug "_CURL" "$_CURL" + if [ "$needbase64" ]; then + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$url" | _base64)" + else + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$url")" + fi + _ret="$?" + if [ "$_ret" != "0" ]; then + _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _err "Here is the curl dump log:" + _err "$(cat "$_CURL_DUMP")" + fi + fi + elif [ "$_ACME_WGET" ]; then + _WGET="$_ACME_WGET" + if [ "$HTTPS_INSECURE" ]; then + _WGET="$_WGET --no-check-certificate " + fi + _debug "_WGET" "$_WGET" + if [ "$needbase64" ]; then + if [ "$httpmethod" = "POST" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$url" 2>"$HTTP_HEADER" | _base64)" + else + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$url" 2>"$HTTP_HEADER" | _base64)" + fi + else + if [ "$httpmethod" = "POST" ]; then + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$url" 2>"$HTTP_HEADER")" + else + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --method $httpmethod --body-data="$body" "$url" 2>"$HTTP_HEADER")" + fi + fi + _ret="$?" + if [ "$_ret" = "8" ]; then + _ret=0 + _debug "wget returns 8, the server returns a 'Bad request' respons, lets process the response later." + fi + if [ "$_ret" != "0" ]; then + _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" + fi + _sed_i "s/^ *//g" "$HTTP_HEADER" + else + _ret="$?" + _err "Neither curl nor wget is found, can not do $httpmethod." + fi + _debug "_ret" "$_ret" + printf "%s" "$response" + return $_ret +} + +# url getheader timeout +_get() { + _debug GET + url="$1" + onlyheader="$2" + t="$3" + _debug url "$url" + _debug "timeout" "$t" + + _inithttp + + if [ "$_ACME_CURL" ]; then + _CURL="$_ACME_CURL" + if [ "$HTTPS_INSECURE" ]; then + _CURL="$_CURL --insecure " + fi + if [ "$t" ]; then + _CURL="$_CURL --connect-timeout $t" + fi + _debug "_CURL" "$_CURL" + if [ "$onlyheader" ]; then + $_CURL -I --user-agent "$USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url" + else + $_CURL --user-agent "$USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url" + fi + ret=$? + if [ "$ret" != "0" ]; then + _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _err "Here is the curl dump log:" + _err "$(cat "$_CURL_DUMP")" + fi + fi + elif [ "$_ACME_WGET" ]; then + _WGET="$_ACME_WGET" + if [ "$HTTPS_INSECURE" ]; then + _WGET="$_WGET --no-check-certificate " + fi + if [ "$t" ]; then + _WGET="$_WGET --timeout=$t" + fi + _debug "_WGET" "$_WGET" + if [ "$onlyheader" ]; then + $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -S -O /dev/null "$url" 2>&1 | sed 's/^[ ]*//g' + else + $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -O - "$url" + fi + ret=$? + if [ "$_ret" = "8" ]; then + _ret=0 + _debug "wget returns 8, the server returns a 'Bad request' respons, lets process the response later." + fi + if [ "$ret" != "0" ]; then + _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $ret" + fi + else + ret=$? + _err "Neither curl nor wget is found, can not do GET." + fi + _debug "ret" "$ret" + return $ret +} + +_head_n() { + head -n "$1" +} + +_tail_n() { + if ! tail -n "$1" 2>/dev/null; then + #fix for solaris + tail -"$1" + fi +} + +# url payload needbase64 keyfile +_send_signed_request() { + url=$1 + payload=$2 + needbase64=$3 + keyfile=$4 + if [ -z "$keyfile" ]; then + keyfile="$ACCOUNT_KEY_PATH" + fi + _debug url "$url" + _debug payload "$payload" + + if ! _calcjwk "$keyfile"; then + return 1 + fi + + payload64=$(printf "%s" "$payload" | _base64 | _urlencode) + _debug3 payload64 "$payload64" + + if [ -z "$_CACHED_NONCE" ]; then + _debug2 "Get nonce." + nonceurl="$API/directory" + _headers="$(_get "$nonceurl" "onlyheader")" + + if [ "$?" != "0" ]; then + _err "Can not connect to $nonceurl to get nonce." + return 1 + fi + + _debug2 _headers "$_headers" + + _CACHED_NONCE="$(echo "$_headers" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + _debug2 _CACHED_NONCE "$_CACHED_NONCE" + else + _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" + fi + nonce="$_CACHED_NONCE" + _debug2 nonce "$nonce" + + protected="$JWK_HEADERPLACE_PART1$nonce$JWK_HEADERPLACE_PART2" + _debug3 protected "$protected" + + protected64="$(printf "%s" "$protected" | _base64 | _urlencode)" + _debug3 protected64 "$protected64" + + if ! _sig_t="$(printf "%s" "$protected64.$payload64" | _sign "$keyfile" "sha256")"; then + _err "Sign request failed." + return 1 + fi + _debug3 _sig_t "$_sig_t" + + sig="$(printf "%s" "$_sig_t" | _urlencode)" + _debug3 sig "$sig" + + body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" + _debug3 body "$body" + + response="$(_post "$body" "$url" "$needbase64")" + _CACHED_NONCE="" + if [ "$?" != "0" ]; then + _err "Can not post to $url" + return 1 + fi + _debug2 original "$response" + + response="$(echo "$response" | _normalizeJson)" + + responseHeaders="$(cat "$HTTP_HEADER")" + + _debug2 responseHeaders "$responseHeaders" + _debug2 response "$response" + code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug code "$code" + + _CACHED_NONCE="$(echo "$responseHeaders" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + +} + +#setopt "file" "opt" "=" "value" [";"] +_setopt() { + __conf="$1" + __opt="$2" + __sep="$3" + __val="$4" + __end="$5" + if [ -z "$__opt" ]; then + _usage usage: _setopt '"file" "opt" "=" "value" [";"]' + return + fi + if [ ! -f "$__conf" ]; then + touch "$__conf" + fi + + if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then + _debug3 OK + if _contains "$__val" "&"; then + __val="$(echo "$__val" | sed 's/&/\\&/g')" + fi + text="$(cat "$__conf")" + echo "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + + elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then + if _contains "$__val" "&"; then + __val="$(echo "$__val" | sed 's/&/\\&/g')" + fi + text="$(cat "$__conf")" + echo "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + + else + _debug3 APP + echo "$__opt$__sep$__val$__end" >>"$__conf" + fi + _debug2 "$(grep -n "^$__opt$__sep" "$__conf")" +} + +#_save_conf file key value +#save to conf +_save_conf() { + _s_c_f="$1" + _sdkey="$2" + _sdvalue="$3" + if [ "$_s_c_f" ]; then + _setopt "$_s_c_f" "$_sdkey" "=" "'$_sdvalue'" + else + _err "config file is empty, can not save $_sdkey=$_sdvalue" + fi +} + +#_clear_conf file key +_clear_conf() { + _c_c_f="$1" + _sdkey="$2" + if [ "$_c_c_f" ]; then + _conf_data="$(cat "$_c_c_f")" + echo "$_conf_data" | sed "s/^$_sdkey *=.*$//" >"$_c_c_f" + else + _err "config file is empty, can not clear" + fi +} + +#_read_conf file key +_read_conf() { + _r_c_f="$1" + _sdkey="$2" + if [ -f "$_r_c_f" ]; then + ( + eval "$(grep "^$_sdkey *=" "$_r_c_f")" + eval "printf \"%s\" \"\$$_sdkey\"" + ) + else + _debug "config file is empty, can not read $_sdkey" + fi +} + +#_savedomainconf key value +#save to domain.conf +_savedomainconf() { + _save_conf "$DOMAIN_CONF" "$1" "$2" +} + +#_cleardomainconf key +_cleardomainconf() { + _clear_conf "$DOMAIN_CONF" "$1" +} + +#_readdomainconf key +_readdomainconf() { + _read_conf "$DOMAIN_CONF" "$1" +} + +#_saveaccountconf key value +_saveaccountconf() { + _save_conf "$ACCOUNT_CONF_PATH" "$1" "$2" +} + +#_clearaccountconf key +_clearaccountconf() { + _clear_conf "$ACCOUNT_CONF_PATH" "$1" +} + +#_savecaconf key value +_savecaconf() { + _save_conf "$CA_CONF" "$1" "$2" +} + +#_readcaconf key +_readcaconf() { + _read_conf "$CA_CONF" "$1" +} + +#_clearaccountconf key +_clearcaconf() { + _clear_conf "$CA_CONF" "$1" +} + +# content localaddress +_startserver() { + content="$1" + ncaddr="$2" + _debug "ncaddr" "$ncaddr" + + _debug "startserver: $$" + nchelp="$(nc -h 2>&1)" + + _debug Le_HTTPPort "$Le_HTTPPort" + _debug Le_Listen_V4 "$Le_Listen_V4" + _debug Le_Listen_V6 "$Le_Listen_V6" + _NC="nc" + + if [ "$Le_Listen_V4" ]; then + _NC="$_NC -4" + elif [ "$Le_Listen_V6" ]; then + _NC="$_NC -6" + fi + + if echo "$nchelp" | grep "\-q[ ,]" >/dev/null; then + _NC="$_NC -q 1 -l $ncaddr" + else + if echo "$nchelp" | grep "GNU netcat" >/dev/null && echo "$nchelp" | grep "\-c, \-\-close" >/dev/null; then + _NC="$_NC -c -l $ncaddr" + elif echo "$nchelp" | grep "\-N" | grep "Shutdown the network socket after EOF on stdin" >/dev/null; then + _NC="$_NC -N -l $ncaddr" + else + _NC="$_NC -l $ncaddr" + fi + fi + + _debug "_NC" "$_NC" + + #for centos ncat + if _contains "$nchelp" "nmap.org"; then + _debug "Using ncat: nmap.org" + if ! _exec "printf \"%s\r\n\r\n%s\" \"HTTP/1.1 200 OK\" \"$content\" | $_NC \"$Le_HTTPPort\" >&2"; then + _exec_err + return 1 + fi + if [ "$DEBUG" ]; then + _exec_err + fi + return + fi + + # while true ; do + if ! _exec "printf \"%s\r\n\r\n%s\" \"HTTP/1.1 200 OK\" \"$content\" | $_NC -p \"$Le_HTTPPort\" >&2"; then + _exec "printf \"%s\r\n\r\n%s\" \"HTTP/1.1 200 OK\" \"$content\" | $_NC \"$Le_HTTPPort\" >&2" + fi + + if [ "$?" != "0" ]; then + _err "nc listen error." + _exec_err + exit 1 + fi + if [ "$DEBUG" ]; then + _exec_err + fi + # done +} + +_stopserver() { + pid="$1" + _debug "pid" "$pid" + if [ -z "$pid" ]; then + return + fi + + _debug2 "Le_HTTPPort" "$Le_HTTPPort" + if [ "$Le_HTTPPort" ]; then + if [ "$DEBUG" ] && [ "$DEBUG" -gt "3" ]; then + _get "http://localhost:$Le_HTTPPort" "" 1 + else + _get "http://localhost:$Le_HTTPPort" "" 1 >/dev/null 2>&1 + fi + fi + + _debug2 "Le_TLSPort" "$Le_TLSPort" + if [ "$Le_TLSPort" ]; then + if [ "$DEBUG" ] && [ "$DEBUG" -gt "3" ]; then + _get "https://localhost:$Le_TLSPort" "" 1 + _get "https://localhost:$Le_TLSPort" "" 1 + else + _get "https://localhost:$Le_TLSPort" "" 1 >/dev/null 2>&1 + _get "https://localhost:$Le_TLSPort" "" 1 >/dev/null 2>&1 + fi + fi +} + +# sleep sec +_sleep() { + _sleep_sec="$1" + if [ "$__INTERACTIVE" ]; then + _sleep_c="$_sleep_sec" + while [ "$_sleep_c" -ge "0" ]; do + printf "\r \r" + __green "$_sleep_c" + _sleep_c="$(_math "$_sleep_c" - 1)" + sleep 1 + done + printf "\r" + else + sleep "$_sleep_sec" + fi +} + +# _starttlsserver san_a san_b port content _ncaddr +_starttlsserver() { + _info "Starting tls server." + san_a="$1" + san_b="$2" + port="$3" + content="$4" + opaddr="$5" + + _debug san_a "$san_a" + _debug san_b "$san_b" + _debug port "$port" + + #create key TLS_KEY + if ! _createkey "2048" "$TLS_KEY"; then + _err "Create tls validation key error." + return 1 + fi + + #create csr + alt="$san_a" + if [ "$san_b" ]; then + alt="$alt,$san_b" + fi + if ! _createcsr "tls.acme.sh" "$alt" "$TLS_KEY" "$TLS_CSR" "$TLS_CONF"; then + _err "Create tls validation csr error." + return 1 + fi + + #self signed + if ! _signcsr "$TLS_KEY" "$TLS_CSR" "$TLS_CONF" "$TLS_CERT"; then + _err "Create tls validation cert error." + return 1 + fi + + __S_OPENSSL="$OPENSSL_BIN s_server -cert $TLS_CERT -key $TLS_KEY " + if [ "$opaddr" ]; then + __S_OPENSSL="$__S_OPENSSL -accept $opaddr:$port" + else + __S_OPENSSL="$__S_OPENSSL -accept $port" + fi + + _debug Le_Listen_V4 "$Le_Listen_V4" + _debug Le_Listen_V6 "$Le_Listen_V6" + if [ "$Le_Listen_V4" ]; then + __S_OPENSSL="$__S_OPENSSL -4" + elif [ "$Le_Listen_V6" ]; then + __S_OPENSSL="$__S_OPENSSL -6" + fi + + _debug "$__S_OPENSSL" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + (printf "%s\r\n\r\n%s" "HTTP/1.1 200 OK" "$content" | $__S_OPENSSL -tlsextdebug) & + else + (printf "%s\r\n\r\n%s" "HTTP/1.1 200 OK" "$content" | $__S_OPENSSL >/dev/null 2>&1) & + fi + + serverproc="$!" + sleep 1 + _debug serverproc "$serverproc" +} + +#file +_readlink() { + _rf="$1" + if ! readlink -f "$_rf" 2>/dev/null; then + if _startswith "$_rf" "\./$PROJECT_ENTRY"; then + printf -- "%s" "$(pwd)/$PROJECT_ENTRY" + return 0 + fi + readlink "$_rf" + fi +} + +__initHome() { + if [ -z "$_SCRIPT_HOME" ]; then + if _exists readlink && _exists dirname; then + _debug "Lets find script dir." + _debug "_SCRIPT_" "$_SCRIPT_" + _script="$(_readlink "$_SCRIPT_")" + _debug "_script" "$_script" + _script_home="$(dirname "$_script")" + _debug "_script_home" "$_script_home" + if [ -d "$_script_home" ]; then + _SCRIPT_HOME="$_script_home" + else + _err "It seems the script home is not correct:$_script_home" + fi + fi + fi + + # if [ -z "$LE_WORKING_DIR" ]; then + # if [ -f "$DEFAULT_INSTALL_HOME/account.conf" ]; then + # _debug "It seems that $PROJECT_NAME is already installed in $DEFAULT_INSTALL_HOME" + # LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" + # else + # LE_WORKING_DIR="$_SCRIPT_HOME" + # fi + # fi + + if [ -z "$LE_WORKING_DIR" ]; then + _debug "Using default home:$DEFAULT_INSTALL_HOME" + LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" + fi + export LE_WORKING_DIR + + if [ -z "$LE_CONFIG_HOME" ]; then + LE_CONFIG_HOME="$LE_WORKING_DIR" + fi + _debug "Using config home:$LE_CONFIG_HOME" + export LE_CONFIG_HOME + + _DEFAULT_ACCOUNT_CONF_PATH="$LE_CONFIG_HOME/account.conf" + + if [ -z "$ACCOUNT_CONF_PATH" ]; then + if [ -f "$_DEFAULT_ACCOUNT_CONF_PATH" ]; then + . "$_DEFAULT_ACCOUNT_CONF_PATH" + fi + fi + + if [ -z "$ACCOUNT_CONF_PATH" ]; then + ACCOUNT_CONF_PATH="$_DEFAULT_ACCOUNT_CONF_PATH" + fi + + DEFAULT_LOG_FILE="$LE_CONFIG_HOME/$PROJECT_NAME.log" + + DEFAULT_CA_HOME="$LE_CONFIG_HOME/ca" + + if [ -z "$LE_TEMP_DIR" ]; then + LE_TEMP_DIR="$LE_CONFIG_HOME/tmp" + fi +} + +#[domain] [keylength] +_initpath() { + + __initHome + + if [ -f "$ACCOUNT_CONF_PATH" ]; then + . "$ACCOUNT_CONF_PATH" + fi + + if [ "$IN_CRON" ]; then + if [ ! "$_USER_PATH_EXPORTED" ]; then + _USER_PATH_EXPORTED=1 + export PATH="$USER_PATH:$PATH" + fi + fi + + if [ -z "$CA_HOME" ]; then + CA_HOME="$DEFAULT_CA_HOME" + fi + + if [ -z "$API" ]; then + if [ -z "$STAGE" ]; then + API="$DEFAULT_CA" + else + API="$STAGE_CA" + _info "Using stage api:$API" + fi + fi + + _API_HOST="$(echo "$API" | cut -d : -f 2 | tr -d '/')" + CA_DIR="$CA_HOME/$_API_HOST" + + _DEFAULT_CA_CONF="$CA_DIR/ca.conf" + + if [ -z "$CA_CONF" ]; then + CA_CONF="$_DEFAULT_CA_CONF" + fi + _debug3 CA_CONF "$CA_CONF" + + if [ -f "$CA_CONF" ]; then + . "$CA_CONF" + fi + + if [ -z "$ACME_DIR" ]; then + ACME_DIR="/home/.acme" + fi + + if [ -z "$APACHE_CONF_BACKUP_DIR" ]; then + APACHE_CONF_BACKUP_DIR="$LE_CONFIG_HOME" + fi + + if [ -z "$USER_AGENT" ]; then + USER_AGENT="$DEFAULT_USER_AGENT" + fi + + if [ -z "$HTTP_HEADER" ]; then + HTTP_HEADER="$LE_CONFIG_HOME/http.header" + fi + + _OLD_ACCOUNT_KEY="$LE_WORKING_DIR/account.key" + _OLD_ACCOUNT_JSON="$LE_WORKING_DIR/account.json" + + _DEFAULT_ACCOUNT_KEY_PATH="$CA_DIR/account.key" + _DEFAULT_ACCOUNT_JSON_PATH="$CA_DIR/account.json" + if [ -z "$ACCOUNT_KEY_PATH" ]; then + ACCOUNT_KEY_PATH="$_DEFAULT_ACCOUNT_KEY_PATH" + fi + + if [ -z "$ACCOUNT_JSON_PATH" ]; then + ACCOUNT_JSON_PATH="$_DEFAULT_ACCOUNT_JSON_PATH" + fi + + _DEFAULT_CERT_HOME="$LE_CONFIG_HOME" + if [ -z "$CERT_HOME" ]; then + CERT_HOME="$_DEFAULT_CERT_HOME" + fi + + if [ -z "$OPENSSL_BIN" ]; then + OPENSSL_BIN="$DEFAULT_OPENSSL_BIN" + fi + + if [ -z "$1" ]; then + return 0 + fi + + domain="$1" + _ilength="$2" + + if [ -z "$DOMAIN_PATH" ]; then + domainhome="$CERT_HOME/$domain" + domainhomeecc="$CERT_HOME/$domain$ECC_SUFFIX" + + DOMAIN_PATH="$domainhome" + + if _isEccKey "$_ilength"; then + DOMAIN_PATH="$domainhomeecc" + else + if [ ! -d "$domainhome" ] && [ -d "$domainhomeecc" ]; then + _info "The domain '$domain' seems to have a ECC cert already, please add '$(__red "--ecc")' parameter if you want to use that cert." + fi + fi + _debug DOMAIN_PATH "$DOMAIN_PATH" + fi + + if [ -z "$DOMAIN_CONF" ]; then + DOMAIN_CONF="$DOMAIN_PATH/$domain.conf" + fi + + if [ -z "$DOMAIN_SSL_CONF" ]; then + DOMAIN_SSL_CONF="$DOMAIN_PATH/$domain.csr.conf" + fi + + if [ -z "$CSR_PATH" ]; then + CSR_PATH="$DOMAIN_PATH/$domain.csr" + fi + if [ -z "$CERT_KEY_PATH" ]; then + CERT_KEY_PATH="$DOMAIN_PATH/$domain.key" + fi + if [ -z "$CERT_PATH" ]; then + CERT_PATH="$DOMAIN_PATH/$domain.cer" + fi + if [ -z "$CA_CERT_PATH" ]; then + CA_CERT_PATH="$DOMAIN_PATH/ca.cer" + fi + if [ -z "$CERT_FULLCHAIN_PATH" ]; then + CERT_FULLCHAIN_PATH="$DOMAIN_PATH/fullchain.cer" + fi + if [ -z "$CERT_PFX_PATH" ]; then + CERT_PFX_PATH="$DOMAIN_PATH/$domain.pfx" + fi + + if [ -z "$TLS_CONF" ]; then + TLS_CONF="$DOMAIN_PATH/tls.valdation.conf" + fi + if [ -z "$TLS_CERT" ]; then + TLS_CERT="$DOMAIN_PATH/tls.valdation.cert" + fi + if [ -z "$TLS_KEY" ]; then + TLS_KEY="$DOMAIN_PATH/tls.valdation.key" + fi + if [ -z "$TLS_CSR" ]; then + TLS_CSR="$DOMAIN_PATH/tls.valdation.csr" + fi + +} + +_exec() { + if [ -z "$_EXEC_TEMP_ERR" ]; then + _EXEC_TEMP_ERR="$(_mktemp)" + fi + + if [ "$_EXEC_TEMP_ERR" ]; then + eval "$@ 2>>$_EXEC_TEMP_ERR" + else + eval "$@" + fi +} + +_exec_err() { + [ "$_EXEC_TEMP_ERR" ] && _err "$(cat "$_EXEC_TEMP_ERR")" && echo "" >"$_EXEC_TEMP_ERR" +} + +_apachePath() { + _APACHECTL="apachectl" + if ! _exists apachectl; then + if _exists apache2ctl; then + _APACHECTL="apache2ctl" + else + _err "'apachectl not found. It seems that apache is not installed, or you are not root user.'" + _err "Please use webroot mode to try again." + return 1 + fi + fi + + if ! _exec $_APACHECTL -V >/dev/null; then + _exec_err + return 1 + fi + + if [ "$APACHE_HTTPD_CONF" ]; then + _saveaccountconf APACHE_HTTPD_CONF "$APACHE_HTTPD_CONF" + httpdconf="$APACHE_HTTPD_CONF" + httpdconfname="$(basename "$httpdconfname")" + else + httpdconfname="$($_APACHECTL -V | grep SERVER_CONFIG_FILE= | cut -d = -f 2 | tr -d '"')" + _debug httpdconfname "$httpdconfname" + + if [ -z "$httpdconfname" ]; then + _err "Can not read apache config file." + return 1 + fi + + if _startswith "$httpdconfname" '/'; then + httpdconf="$httpdconfname" + httpdconfname="$(basename "$httpdconfname")" + else + httpdroot="$($_APACHECTL -V | grep HTTPD_ROOT= | cut -d = -f 2 | tr -d '"')" + _debug httpdroot "$httpdroot" + httpdconf="$httpdroot/$httpdconfname" + httpdconfname="$(basename "$httpdconfname")" + fi + fi + _debug httpdconf "$httpdconf" + _debug httpdconfname "$httpdconfname" + if [ ! -f "$httpdconf" ]; then + _err "Apache Config file not found" "$httpdconf" + return 1 + fi + return 0 +} + +_restoreApache() { + if [ -z "$usingApache" ]; then + return 0 + fi + _initpath + if ! _apachePath; then + return 1 + fi + + if [ ! -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" ]; then + _debug "No config file to restore." + return 0 + fi + + cat "$APACHE_CONF_BACKUP_DIR/$httpdconfname" >"$httpdconf" + _debug "Restored: $httpdconf." + if ! _exec $_APACHECTL -t; then + _exec_err + _err "Sorry, restore apache config error, please contact me." + return 1 + fi + _debug "Restored successfully." + rm -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" + return 0 +} + +_setApache() { + _initpath + if ! _apachePath; then + return 1 + fi + + #test the conf first + _info "Checking if there is an error in the apache config file before starting." + + if ! _exec "$_APACHECTL" -t >/dev/null; then + _exec_err + _err "The apache config file has error, please fix it first, then try again." + _err "Don't worry, there is nothing changed to your system." + return 1 + else + _info "OK" + fi + + #backup the conf + _debug "Backup apache config file" "$httpdconf" + if ! cp "$httpdconf" "$APACHE_CONF_BACKUP_DIR/"; then + _err "Can not backup apache config file, so abort. Don't worry, the apache config is not changed." + _err "This might be a bug of $PROJECT_NAME , pleae report issue: $PROJECT" + return 1 + fi + _info "JFYI, Config file $httpdconf is backuped to $APACHE_CONF_BACKUP_DIR/$httpdconfname" + _info "In case there is an error that can not be restored automatically, you may try restore it yourself." + _info "The backup file will be deleted on success, just forget it." + + #add alias + + apacheVer="$($_APACHECTL -V | grep "Server version:" | cut -d : -f 2 | cut -d " " -f 2 | cut -d '/' -f 2)" + _debug "apacheVer" "$apacheVer" + apacheMajer="$(echo "$apacheVer" | cut -d . -f 1)" + apacheMinor="$(echo "$apacheVer" | cut -d . -f 2)" + + if [ "$apacheVer" ] && [ "$apacheMajer$apacheMinor" -ge "24" ]; then + echo " +Alias /.well-known/acme-challenge $ACME_DIR + + +Require all granted + + " >>"$httpdconf" + else + echo " +Alias /.well-known/acme-challenge $ACME_DIR + + +Order allow,deny +Allow from all + + " >>"$httpdconf" + fi + + _msg="$($_APACHECTL -t 2>&1)" + if [ "$?" != "0" ]; then + _err "Sorry, apache config error" + if _restoreApache; then + _err "The apache config file is restored." + else + _err "Sorry, The apache config file can not be restored, please report bug." + fi + return 1 + fi + + if [ ! -d "$ACME_DIR" ]; then + mkdir -p "$ACME_DIR" + chmod 755 "$ACME_DIR" + fi + + if ! _exec "$_APACHECTL" graceful; then + _exec_err + _err "$_APACHECTL graceful error, please contact me." + _restoreApache + return 1 + fi + usingApache="1" + return 0 +} + +_clearup() { + _stopserver "$serverproc" + serverproc="" + _restoreApache + _clearupdns + if [ -z "$DEBUG" ]; then + rm -f "$TLS_CONF" + rm -f "$TLS_CERT" + rm -f "$TLS_KEY" + rm -f "$TLS_CSR" + fi +} + +_clearupdns() { + _debug "_clearupdns" + if [ "$dnsadded" != 1 ] || [ -z "$vlist" ]; then + _debug "Dns not added, skip." + return + fi + + ventries=$(echo "$vlist" | tr ',' ' ') + for ventry in $ventries; do + d=$(echo "$ventry" | cut -d "$sep" -f 1) + keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) + vtype=$(echo "$ventry" | cut -d "$sep" -f 4) + _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _urlencode)" + _debug txt "$txt" + if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then + _info "$d is already verified, skip $vtype." + continue + fi + + if [ "$vtype" != "$VTYPE_DNS" ]; then + _info "Skip $d for $vtype" + continue + fi + + d_api="$(_findHook "$d" dnsapi "$_currentRoot")" + _debug d_api "$d_api" + + if [ -z "$d_api" ]; then + _info "Not Found domain api file: $d_api" + continue + fi + + ( + if ! . "$d_api"; then + _err "Load file $d_api error. Please check your api file and try again." + return 1 + fi + + rmcommand="${_currentRoot}_rm" + if ! _exists "$rmcommand"; then + _err "It seems that your api file doesn't define $rmcommand" + return 1 + fi + + txtdomain="_acme-challenge.$d" + + if ! $rmcommand "$txtdomain" "$txt"; then + _err "Error removing txt for domain:$txtdomain" + return 1 + fi + ) + + done +} + +# webroot removelevel tokenfile +_clearupwebbroot() { + __webroot="$1" + if [ -z "$__webroot" ]; then + _debug "no webroot specified, skip" + return 0 + fi + + _rmpath="" + if [ "$2" = '1' ]; then + _rmpath="$__webroot/.well-known" + elif [ "$2" = '2' ]; then + _rmpath="$__webroot/.well-known/acme-challenge" + elif [ "$2" = '3' ]; then + _rmpath="$__webroot/.well-known/acme-challenge/$3" + else + _debug "Skip for removelevel:$2" + fi + + if [ "$_rmpath" ]; then + if [ "$DEBUG" ]; then + _debug "Debugging, skip removing: $_rmpath" + else + rm -rf "$_rmpath" + fi + fi + + return 0 + +} + +_on_before_issue() { + _debug _on_before_issue + #run pre hook + if [ "$Le_PreHook" ]; then + _info "Run pre hook:'$Le_PreHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_PreHook" + ); then + _err "Error when run pre hook." + return 1 + fi + fi + + if _hasfield "$Le_Webroot" "$NO_VALUE"; then + if ! _exists "nc"; then + _err "Please install netcat(nc) tools first." + return 1 + fi + fi + + _debug Le_LocalAddress "$Le_LocalAddress" + + alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ') + _index=1 + _currentRoot="" + _addrIndex=1 + for d in $alldomains; do + _debug "Check for domain" "$d" + _currentRoot="$(_getfield "$Le_Webroot" $_index)" + _debug "_currentRoot" "$_currentRoot" + _index=$(_math $_index + 1) + _checkport="" + if [ "$_currentRoot" = "$NO_VALUE" ]; then + _info "Standalone mode." + if [ -z "$Le_HTTPPort" ]; then + Le_HTTPPort=80 + else + _savedomainconf "Le_HTTPPort" "$Le_HTTPPort" + fi + _checkport="$Le_HTTPPort" + elif [ "$_currentRoot" = "$W_TLS" ]; then + _info "Standalone tls mode." + if [ -z "$Le_TLSPort" ]; then + Le_TLSPort=443 + else + _savedomainconf "Le_TLSPort" "$Le_TLSPort" + fi + _checkport="$Le_TLSPort" + fi + + if [ "$_checkport" ]; then + _debug _checkport "$_checkport" + _checkaddr="$(_getfield "$Le_LocalAddress" $_addrIndex)" + _debug _checkaddr "$_checkaddr" + + _addrIndex="$(_math $_addrIndex + 1)" + + _netprc="$(_ss "$_checkport" | grep "$_checkport")" + netprc="$(echo "$_netprc" | grep "$_checkaddr")" + if [ -z "$netprc" ]; then + netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS")" + fi + if [ "$netprc" ]; then + _err "$netprc" + _err "tcp port $_checkport is already used by $(echo "$netprc" | cut -d : -f 4)" + _err "Please stop it first" + return 1 + fi + fi + done + + if _hasfield "$Le_Webroot" "apache"; then + if ! _setApache; then + _err "set up apache error. Report error to me." + return 1 + fi + else + usingApache="" + fi + +} + +_on_issue_err() { + _debug _on_issue_err + if [ "$LOG_FILE" ]; then + _err "Please check log file for more details: $LOG_FILE" + else + _err "Please add '--debug' or '--log' to check more details." + _err "See: $_DEBUG_WIKI" + fi + + if [ "$DEBUG" ] && [ "$DEBUG" -gt "0" ]; then + _debug "$(_dlg_versions)" + fi + + #run the post hook + if [ "$Le_PostHook" ]; then + _info "Run post hook:'$Le_PostHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_PostHook" + ); then + _err "Error when run post hook." + return 1 + fi + fi +} + +_on_issue_success() { + _debug _on_issue_success + #run the post hook + if [ "$Le_PostHook" ]; then + _info "Run post hook:'$Le_PostHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_PostHook" + ); then + _err "Error when run post hook." + return 1 + fi + fi + + #run renew hook + if [ "$IS_RENEW" ] && [ "$Le_RenewHook" ]; then + _info "Run renew hook:'$Le_RenewHook'" + if ! ( + cd "$DOMAIN_PATH" && eval "$Le_RenewHook" + ); then + _err "Error when run renew hook." + return 1 + fi + fi + +} + +updateaccount() { + _initpath + _regAccount +} + +registeraccount() { + _reg_length="$1" + _initpath + _regAccount "$_reg_length" +} + +__calcAccountKeyHash() { + [ -f "$ACCOUNT_KEY_PATH" ] && _digest sha256 <"$ACCOUNT_KEY_PATH" +} + +#keylength +_regAccount() { + _initpath + _reg_length="$1" + + if [ ! -f "$ACCOUNT_KEY_PATH" ] && [ -f "$_OLD_ACCOUNT_KEY" ]; then + mkdir -p "$CA_DIR" + _info "mv $_OLD_ACCOUNT_KEY to $ACCOUNT_KEY_PATH" + mv "$_OLD_ACCOUNT_KEY" "$ACCOUNT_KEY_PATH" + fi + + if [ ! -f "$ACCOUNT_JSON_PATH" ] && [ -f "$_OLD_ACCOUNT_JSON" ]; then + mkdir -p "$CA_DIR" + _info "mv $_OLD_ACCOUNT_JSON to $ACCOUNT_JSON_PATH" + mv "$_OLD_ACCOUNT_JSON" "$ACCOUNT_JSON_PATH" + fi + + if [ ! -f "$ACCOUNT_KEY_PATH" ]; then + if ! _create_account_key "$_reg_length"; then + _err "Create account key error." + return 1 + fi + fi + + if ! _calcjwk "$ACCOUNT_KEY_PATH"; then + return 1 + fi + + _updateTos="" + _reg_res="new-reg" + while true; do + _debug AGREEMENT "$AGREEMENT" + + regjson='{"resource": "'$_reg_res'", "agreement": "'$AGREEMENT'"}' + + if [ "$ACCOUNT_EMAIL" ]; then + regjson='{"resource": "'$_reg_res'", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' + fi + + if [ -z "$_updateTos" ]; then + _info "Registering account" + + if ! _send_signed_request "$API/acme/new-reg" "$regjson"; then + _err "Register account Error: $response" + return 1 + fi + + if [ "$code" = "" ] || [ "$code" = '201' ]; then + echo "$response" >"$ACCOUNT_JSON_PATH" + _info "Registered" + elif [ "$code" = '409' ]; then + _info "Already registered" + else + _err "Register account Error: $response" + return 1 + fi + + _accUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" + _debug "_accUri" "$_accUri" + + _tos="$(echo "$responseHeaders" | grep "^Link:.*rel=\"terms-of-service\"" | _head_n 1 | _egrep_o "<.*>" | tr -d '<>')" + _debug "_tos" "$_tos" + if [ -z "$_tos" ]; then + _debug "Use default tos: $DEFAULT_AGREEMENT" + _tos="$DEFAULT_AGREEMENT" + fi + if [ "$_tos" != "$AGREEMENT" ]; then + _updateTos=1 + AGREEMENT="$_tos" + _reg_res="reg" + continue + fi + + else + _debug "Update tos: $_tos" + if ! _send_signed_request "$_accUri" "$regjson"; then + _err "Update tos error." + return 1 + fi + if [ "$code" = '202' ]; then + _info "Update success." + + CA_KEY_HASH="$(__calcAccountKeyHash)" + _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH" + _savecaconf CA_KEY_HASH "$CA_KEY_HASH" + else + _err "Update account error." + return 1 + fi + fi + return 0 + done + +} + +# domain folder file +_findHook() { + _hookdomain="$1" + _hookcat="$2" + _hookname="$3" + + if [ -f "$_SCRIPT_HOME/$_hookcat/$_hookname" ]; then + d_api="$_SCRIPT_HOME/$_hookcat/$_hookname" + elif [ -f "$_SCRIPT_HOME/$_hookcat/$_hookname.sh" ]; then + d_api="$_SCRIPT_HOME/$_hookcat/$_hookname.sh" + elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname" ]; then + d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname" + elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" ]; then + d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" + elif [ -f "$LE_WORKING_DIR/$_hookname" ]; then + d_api="$LE_WORKING_DIR/$_hookname" + elif [ -f "$LE_WORKING_DIR/$_hookname.sh" ]; then + d_api="$LE_WORKING_DIR/$_hookname.sh" + elif [ -f "$LE_WORKING_DIR/$_hookcat/$_hookname" ]; then + d_api="$LE_WORKING_DIR/$_hookcat/$_hookname" + elif [ -f "$LE_WORKING_DIR/$_hookcat/$_hookname.sh" ]; then + d_api="$LE_WORKING_DIR/$_hookcat/$_hookname.sh" + fi + + printf "%s" "$d_api" +} + +#domain +__get_domain_new_authz() { + _gdnd="$1" + _info "Getting new-authz for domain" "$_gdnd" + + _Max_new_authz_retry_times=5 + _authz_i=0 + while [ "$_authz_i" -lt "$_Max_new_authz_retry_times" ]; do + _debug "Try new-authz for the $_authz_i time." + if ! _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$(_idn "$_gdnd")\"}}"; then + _err "Can not get domain new authz." + return 1 + fi + if _contains "$response" "No registration exists matching provided key"; then + _err "It seems there is an error, but it's recovered now, please try again." + _err "If you see this message for a second time, please report bug: $(__green "$PROJECT")" + _clearcaconf "CA_KEY_HASH" + break + fi + if ! _contains "$response" "An error occurred while processing your request"; then + _info "The new-authz request is ok." + break + fi + _authz_i="$(_math "$_authz_i" + 1)" + _info "The server is busy, Sleep $_authz_i to retry." + _sleep "$_authz_i" + done + + if [ "$_authz_i" = "$_Max_new_authz_retry_times" ]; then + _err "new-authz retry reach the max $_Max_new_authz_retry_times times." + fi + + if [ ! -z "$code" ] && [ ! "$code" = '201' ]; then + _err "new-authz error: $response" + return 1 + fi + +} + +#webroot, domain domainlist keylength +issue() { + if [ -z "$2" ]; then + _usage "Usage: $PROJECT_ENTRY --issue -d a.com -w /path/to/webroot/a.com/ " + return 1 + fi + Le_Webroot="$1" + Le_Domain="$2" + Le_Alt="$3" + if _contains "$Le_Domain" ","; then + Le_Domain=$(echo "$2,$3" | cut -d , -f 1) + Le_Alt=$(echo "$2,$3" | cut -d , -f 2- | sed "s/,${NO_VALUE}$//") + fi + Le_Keylength="$4" + Le_RealCertPath="$5" + Le_RealKeyPath="$6" + Le_RealCACertPath="$7" + Le_ReloadCmd="$8" + Le_RealFullChainPath="$9" + Le_PreHook="${10}" + Le_PostHook="${11}" + Le_RenewHook="${12}" + Le_LocalAddress="${13}" + + #remove these later. + if [ "$Le_Webroot" = "dns-cf" ]; then + Le_Webroot="dns_cf" + fi + if [ "$Le_Webroot" = "dns-dp" ]; then + Le_Webroot="dns_dp" + fi + if [ "$Le_Webroot" = "dns-cx" ]; then + Le_Webroot="dns_cx" + fi + _debug "Using api: $API" + + if [ ! "$IS_RENEW" ]; then + _initpath "$Le_Domain" "$Le_Keylength" + mkdir -p "$DOMAIN_PATH" + fi + + if [ -f "$DOMAIN_CONF" ]; then + Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime) + _debug Le_NextRenewTime "$Le_NextRenewTime" + if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _saved_domain=$(_readdomainconf Le_Domain) + _debug _saved_domain "$_saved_domain" + _saved_alt=$(_readdomainconf Le_Alt) + _debug _saved_alt "$_saved_alt" + if [ "$_saved_domain,$_saved_alt" = "$Le_Domain,$Le_Alt" ]; then + _info "Domains not changed." + _info "Skip, Next renewal time is: $(__green "$(_readdomainconf Le_NextRenewTimeStr)")" + _info "Add '$(__red '--force')' to force to renew." + return $RENEW_SKIP + else + _info "Domains have changed." + fi + fi + fi + + _savedomainconf "Le_Domain" "$Le_Domain" + _savedomainconf "Le_Alt" "$Le_Alt" + _savedomainconf "Le_Webroot" "$Le_Webroot" + + _savedomainconf "Le_PreHook" "$Le_PreHook" + _savedomainconf "Le_PostHook" "$Le_PostHook" + _savedomainconf "Le_RenewHook" "$Le_RenewHook" + + if [ "$Le_LocalAddress" ]; then + _savedomainconf "Le_LocalAddress" "$Le_LocalAddress" + else + _cleardomainconf "Le_LocalAddress" + fi + + Le_API="$API" + _savedomainconf "Le_API" "$Le_API" + + if [ "$Le_Alt" = "$NO_VALUE" ]; then + Le_Alt="" + fi + + if [ "$Le_Keylength" = "$NO_VALUE" ]; then + Le_Keylength="" + fi + + if ! _on_before_issue; then + _err "_on_before_issue." + return 1 + fi + + _saved_account_key_hash="$(_readcaconf "CA_KEY_HASH")" + _debug2 _saved_account_key_hash "$_saved_account_key_hash" + + if [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then + if ! _regAccount "$_accountkeylength"; then + _on_issue_err + return 1 + fi + else + _debug "_saved_account_key_hash is not changed, skip register account." + fi + + if [ -f "$CSR_PATH" ] && [ ! -f "$CERT_KEY_PATH" ]; then + _info "Signing from existing CSR." + else + _key=$(_readdomainconf Le_Keylength) + _debug "Read key length:$_key" + if [ ! -f "$CERT_KEY_PATH" ] || [ "$Le_Keylength" != "$_key" ]; then + if ! createDomainKey "$Le_Domain" "$Le_Keylength"; then + _err "Create domain key error." + _clearup + _on_issue_err + return 1 + fi + fi + + if ! _createcsr "$Le_Domain" "$Le_Alt" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then + _err "Create CSR error." + _clearup + _on_issue_err + return 1 + fi + fi + + _savedomainconf "Le_Keylength" "$Le_Keylength" + + vlist="$Le_Vlist" + + _info "Getting domain auth token for each domain" + sep='#' + if [ -z "$vlist" ]; then + alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ') + _index=1 + _currentRoot="" + for d in $alldomains; do + _info "Getting webroot for domain" "$d" + _w="$(echo $Le_Webroot | cut -d , -f $_index)" + _info _w "$_w" + if [ "$_w" ]; then + _currentRoot="$_w" + fi + _debug "_currentRoot" "$_currentRoot" + _index=$(_math $_index + 1) + + vtype="$VTYPE_HTTP" + if _startswith "$_currentRoot" "dns"; then + vtype="$VTYPE_DNS" + fi + + if [ "$_currentRoot" = "$W_TLS" ]; then + vtype="$VTYPE_TLS" + fi + + if ! __get_domain_new_authz "$d"; then + _clearup + _on_issue_err + return 1 + fi + + if [ -z "$thumbprint" ]; then + accountkey_json=$(printf "%s" "$jwk" | tr -d ' ') + thumbprint=$(printf "%s" "$accountkey_json" | _digest "sha256" | _urlencode) + fi + + entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + _debug entry "$entry" + if [ -z "$entry" ]; then + _err "Error, can not get domain token $d" + _clearup + _on_issue_err + return 1 + fi + token="$(printf "%s\n" "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + _debug token "$token" + + uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d : -f 2,3 | tr -d '"')" + _debug uri "$uri" + + keyauthorization="$token.$thumbprint" + _debug keyauthorization "$keyauthorization" + + if printf "%s" "$response" | grep '"status":"valid"' >/dev/null 2>&1; then + _info "$d is already verified, skip." + keyauthorization="$STATE_VERIFIED" + _debug keyauthorization "$keyauthorization" + fi + + dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot" + _debug dvlist "$dvlist" + + vlist="$vlist$dvlist," + + done + + #add entry + dnsadded="" + ventries=$(echo "$vlist" | tr ',' ' ') + for ventry in $ventries; do + d=$(echo "$ventry" | cut -d "$sep" -f 1) + keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) + vtype=$(echo "$ventry" | cut -d "$sep" -f 4) + _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + + if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then + _info "$d is already verified, skip $vtype." + continue + fi + + if [ "$vtype" = "$VTYPE_DNS" ]; then + dnsadded='0' + txtdomain="_acme-challenge.$d" + _debug txtdomain "$txtdomain" + txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _urlencode)" + _debug txt "$txt" + + d_api="$(_findHook "$d" dnsapi "$_currentRoot")" + + _debug d_api "$d_api" + + if [ "$d_api" ]; then + _info "Found domain api file: $d_api" + else + _err "Add the following TXT record:" + _err "Domain: '$(__green "$txtdomain")'" + _err "TXT value: '$(__green "$txt")'" + _err "Please be aware that you prepend _acme-challenge. before your domain" + _err "so the resulting subdomain will be: $txtdomain" + continue + fi + + ( + if ! . "$d_api"; then + _err "Load file $d_api error. Please check your api file and try again." + return 1 + fi + + addcommand="${_currentRoot}_add" + if ! _exists "$addcommand"; then + _err "It seems that your api file is not correct, it must have a function named: $addcommand" + return 1 + fi + + if ! $addcommand "$txtdomain" "$txt"; then + _err "Error add txt for domain:$txtdomain" + return 1 + fi + ) + + if [ "$?" != "0" ]; then + _clearup + _on_issue_err + return 1 + fi + dnsadded='1' + fi + done + + if [ "$dnsadded" = '0' ]; then + _savedomainconf "Le_Vlist" "$vlist" + _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit." + _err "Please add the TXT records to the domains, and retry again." + _clearup + _on_issue_err + return 1 + fi + + fi + + if [ "$dnsadded" = '1' ]; then + if [ -z "$Le_DNSSleep" ]; then + Le_DNSSleep="$DEFAULT_DNS_SLEEP" + else + _savedomainconf "Le_DNSSleep" "$Le_DNSSleep" + fi + + _info "Sleep $(__green $Le_DNSSleep) seconds for the txt records to take effect" + _sleep "$Le_DNSSleep" + fi + + _debug "ok, let's start to verify" + + _ncIndex=1 + ventries=$(echo "$vlist" | tr ',' ' ') + for ventry in $ventries; do + d=$(echo "$ventry" | cut -d "$sep" -f 1) + keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) + uri=$(echo "$ventry" | cut -d "$sep" -f 3) + vtype=$(echo "$ventry" | cut -d "$sep" -f 4) + _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + + if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then + _info "$d is already verified, skip $vtype." + continue + fi + + _info "Verifying:$d" + _debug "d" "$d" + _debug "keyauthorization" "$keyauthorization" + _debug "uri" "$uri" + removelevel="" + token="$(printf "%s" "$keyauthorization" | cut -d '.' -f 1)" + + _debug "_currentRoot" "$_currentRoot" + + if [ "$vtype" = "$VTYPE_HTTP" ]; then + if [ "$_currentRoot" = "$NO_VALUE" ]; then + _info "Standalone mode server" + _ncaddr="$(_getfield "$Le_LocalAddress" "$_ncIndex")" + _ncIndex="$(_math $_ncIndex + 1)" + _startserver "$keyauthorization" "$_ncaddr" & + if [ "$?" != "0" ]; then + _clearup + _on_issue_err + return 1 + fi + serverproc="$!" + sleep 1 + _debug serverproc "$serverproc" + + else + if [ "$_currentRoot" = "apache" ]; then + wellknown_path="$ACME_DIR" + else + wellknown_path="$_currentRoot/.well-known/acme-challenge" + if [ ! -d "$_currentRoot/.well-known" ]; then + removelevel='1' + elif [ ! -d "$_currentRoot/.well-known/acme-challenge" ]; then + removelevel='2' + else + removelevel='3' + fi + fi + + _debug wellknown_path "$wellknown_path" + + _debug "writing token:$token to $wellknown_path/$token" + + mkdir -p "$wellknown_path" + + if ! printf "%s" "$keyauthorization" >"$wellknown_path/$token"; then + _err "$d:Can not write token to file : $wellknown_path/$token" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + + if [ ! "$usingApache" ]; then + if webroot_owner=$(_stat "$_currentRoot"); then + _debug "Changing owner/group of .well-known to $webroot_owner" + chown -R "$webroot_owner" "$_currentRoot/.well-known" + else + _debug "not chaning owner/group of webroot" + fi + fi + + fi + + elif [ "$vtype" = "$VTYPE_TLS" ]; then + #create A + #_hash_A="$(printf "%s" $token | _digest "sha256" "hex" )" + #_debug2 _hash_A "$_hash_A" + #_x="$(echo $_hash_A | cut -c 1-32)" + #_debug2 _x "$_x" + #_y="$(echo $_hash_A | cut -c 33-64)" + #_debug2 _y "$_y" + #_SAN_A="$_x.$_y.token.acme.invalid" + #_debug2 _SAN_A "$_SAN_A" + + #create B + _hash_B="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")" + _debug2 _hash_B "$_hash_B" + _x="$(echo "$_hash_B" | cut -c 1-32)" + _debug2 _x "$_x" + _y="$(echo "$_hash_B" | cut -c 33-64)" + _debug2 _y "$_y" + + #_SAN_B="$_x.$_y.ka.acme.invalid" + + _SAN_B="$_x.$_y.acme.invalid" + _debug2 _SAN_B "$_SAN_B" + + _ncaddr="$(_getfield "$Le_LocalAddress" "$_ncIndex")" + _ncIndex="$(_math "$_ncIndex" + 1)" + if ! _starttlsserver "$_SAN_B" "$_SAN_A" "$Le_TLSPort" "$keyauthorization" "$_ncaddr"; then + _err "Start tls server error." + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + fi + + if ! _send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}"; then + _err "$d:Can not get challenge: $response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + + if [ ! -z "$code" ] && [ ! "$code" = '202' ]; then + _err "$d:Challenge error: $response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + + waittimes=0 + if [ -z "$MAX_RETRY_TIMES" ]; then + MAX_RETRY_TIMES=30 + fi + + while true; do + waittimes=$(_math "$waittimes" + 1) + if [ "$waittimes" -ge "$MAX_RETRY_TIMES" ]; then + _err "$d:Timeout" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + + _debug "sleep 2 secs to verify" + sleep 2 + _debug "checking" + response="$(_get "$uri")" + if [ "$?" != "0" ]; then + _err "$d:Verify error:$response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + _debug2 original "$response" + + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + + status=$(echo "$response" | _egrep_o '"status":"[^"]*' | cut -d : -f 2 | tr -d '"') + if [ "$status" = "valid" ]; then + _info "$(__green Success)" + _stopserver "$serverproc" + serverproc="" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + break + fi + + if [ "$status" = "invalid" ]; then + error="$(echo "$response" | tr -d "\r\n" | _egrep_o '"error":\{[^\}]*')" + _debug2 error "$error" + errordetail="$(echo "$error" | _egrep_o '"detail": *"[^"]*' | cut -d '"' -f 4)" + _debug2 errordetail "$errordetail" + if [ "$errordetail" ]; then + _err "$d:Verify error:$errordetail" + else + _err "$d:Verify error:$error" + fi + if [ "$DEBUG" ]; then + if [ "$vtype" = "$VTYPE_HTTP" ]; then + _debug "Debug: get token url." + _get "http://$d/.well-known/acme-challenge/$token" "" 1 + fi + fi + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + + if [ "$status" = "pending" ]; then + _info "Pending" + else + _err "$d:Verify error:$response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err + return 1 + fi + + done + + done + + _clearup + _info "Verify finished, start to sign." + der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _urlencode)" + + if ! _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64"; then + _err "Sign failed." + _on_issue_err + return 1 + fi + + _rcert="$response" + Le_LinkCert="$(grep -i '^Location.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" + _savedomainconf "Le_LinkCert" "$Le_LinkCert" + + if [ "$Le_LinkCert" ]; then + echo "$BEGIN_CERT" >"$CERT_PATH" + + #if ! _get "$Le_LinkCert" | _base64 "multiline" >> "$CERT_PATH" ; then + # _debug "Get cert failed. Let's try last response." + # printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >> "$CERT_PATH" + #fi + + if ! printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >>"$CERT_PATH"; then + _debug "Try cert link." + _get "$Le_LinkCert" | _base64 "multiline" >>"$CERT_PATH" + fi + + echo "$END_CERT" >>"$CERT_PATH" + _info "$(__green "Cert success.")" + cat "$CERT_PATH" + + _info "Your cert is in $(__green " $CERT_PATH ")" + + if [ -f "$CERT_KEY_PATH" ]; then + _info "Your cert key is in $(__green " $CERT_KEY_PATH ")" + fi + + cp "$CERT_PATH" "$CERT_FULLCHAIN_PATH" + + if [ ! "$USER_PATH" ] || [ ! "$IN_CRON" ]; then + USER_PATH="$PATH" + _saveaccountconf "USER_PATH" "$USER_PATH" + fi + fi + + if [ -z "$Le_LinkCert" ]; then + response="$(echo "$response" | _dbase64 "multiline" | _normalizeJson)" + _err "Sign failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')" + _on_issue_err + return 1 + fi + + _cleardomainconf "Le_Vlist" + + Le_LinkIssuer=$(grep -i '^Link' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | cut -d ';' -f 1 | tr -d '<>') + if ! _contains "$Le_LinkIssuer" ":"; then + Le_LinkIssuer="$API$Le_LinkIssuer" + fi + + _savedomainconf "Le_LinkIssuer" "$Le_LinkIssuer" + + if [ "$Le_LinkIssuer" ]; then + echo "$BEGIN_CERT" >"$CA_CERT_PATH" + _get "$Le_LinkIssuer" | _base64 "multiline" >>"$CA_CERT_PATH" + echo "$END_CERT" >>"$CA_CERT_PATH" + _info "The intermediate CA cert is in $(__green " $CA_CERT_PATH ")" + cat "$CA_CERT_PATH" >>"$CERT_FULLCHAIN_PATH" + _info "And the full chain certs is there: $(__green " $CERT_FULLCHAIN_PATH ")" + fi + + Le_CertCreateTime=$(_time) + _savedomainconf "Le_CertCreateTime" "$Le_CertCreateTime" + + Le_CertCreateTimeStr=$(date -u) + _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr" + + if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ] || [ "$Le_RenewalDays" -gt "$MAX_RENEW" ]; then + Le_RenewalDays="$MAX_RENEW" + else + _savedomainconf "Le_RenewalDays" "$Le_RenewalDays" + fi + + if [ "$CA_BUNDLE" ]; then + _saveaccountconf CA_BUNDLE "$CA_BUNDLE" + else + _clearaccountconf "CA_BUNDLE" + fi + + if [ "$HTTPS_INSECURE" ]; then + _saveaccountconf HTTPS_INSECURE "$HTTPS_INSECURE" + else + _clearaccountconf "HTTPS_INSECURE" + fi + + if [ "$Le_Listen_V4" ]; then + _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" + _cleardomainconf Le_Listen_V6 + elif [ "$Le_Listen_V6" ]; then + _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" + _cleardomainconf Le_Listen_V4 + fi + + Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) + + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" + + Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) + _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" + + _on_issue_success + + if [ "$Le_RealCertPath$Le_RealKeyPath$Le_RealCACertPath$Le_ReloadCmd$Le_RealFullChainPath" ]; then + _installcert + fi + +} + +#domain [isEcc] +renew() { + Le_Domain="$1" + if [ -z "$Le_Domain" ]; then + _usage "Usage: $PROJECT_ENTRY --renew -d domain.com [--ecc]" + return 1 + fi + + _isEcc="$2" + + _initpath "$Le_Domain" "$_isEcc" + + _info "$(__green "Renew: '$Le_Domain'")" + if [ ! -f "$DOMAIN_CONF" ]; then + _info "'$Le_Domain' is not a issued domain, skip." + return 0 + fi + + if [ "$Le_RenewalDays" ]; then + _savedomainconf Le_RenewalDays "$Le_RenewalDays" + fi + + . "$DOMAIN_CONF" + + if [ "$Le_API" ]; then + API="$Le_API" + #reload ca configs + ACCOUNT_KEY_PATH="" + ACCOUNT_JSON_PATH="" + CA_CONF="" + _debug3 "initpath again." + _initpath "$Le_Domain" "$_isEcc" + fi + + if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _info "Skip, Next renewal time is: $(__green "$Le_NextRenewTimeStr")" + _info "Add '$(__red '--force')' to force to renew." + return "$RENEW_SKIP" + fi + + IS_RENEW="1" + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" + res="$?" + if [ "$res" != "0" ]; then + return "$res" + fi + + if [ "$Le_DeployHook" ]; then + deploy "$Le_Domain" "$Le_DeployHook" "$Le_Keylength" + res="$?" + fi + + IS_RENEW="" + + return "$res" +} + +#renewAll [stopRenewOnError] +renewAll() { + _initpath + _stopRenewOnError="$1" + _debug "_stopRenewOnError" "$_stopRenewOnError" + _ret="0" + + for di in "${CERT_HOME}"/*.*/; do + _debug di "$di" + if ! [ -d "$di" ]; then + _debug "Not directory, skip: $di" + continue + fi + d=$(basename "$di") + _debug d "$d" + ( + if _endswith "$d" "$ECC_SUFFIX"; then + _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) + d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) + fi + renew "$d" "$_isEcc" + ) + rc="$?" + _debug "Return code: $rc" + if [ "$rc" != "0" ]; then + if [ "$rc" = "$RENEW_SKIP" ]; then + _info "Skipped $d" + elif [ "$_stopRenewOnError" ]; then + _err "Error renew $d, stop now." + return "$rc" + else + _ret="$rc" + _err "Error renew $d, Go ahead to next one." + fi + fi + done + return "$_ret" +} + +#csr webroot +signcsr() { + _csrfile="$1" + _csrW="$2" + if [ -z "$_csrfile" ] || [ -z "$_csrW" ]; then + _usage "Usage: $PROJECT_ENTRY --signcsr --csr mycsr.csr -w /path/to/webroot/a.com/ " + return 1 + fi + + _initpath + + _csrsubj=$(_readSubjectFromCSR "$_csrfile") + if [ "$?" != "0" ]; then + _err "Can not read subject from csr: $_csrfile" + return 1 + fi + _debug _csrsubj "$_csrsubj" + + _csrdomainlist=$(_readSubjectAltNamesFromCSR "$_csrfile") + if [ "$?" != "0" ]; then + _err "Can not read domain list from csr: $_csrfile" + return 1 + fi + _debug "_csrdomainlist" "$_csrdomainlist" + + if [ -z "$_csrsubj" ]; then + _csrsubj="$(_getfield "$_csrdomainlist" 1)" + _debug _csrsubj "$_csrsubj" + _csrdomainlist="$(echo "$_csrdomainlist" | cut -d , -f 2-)" + _debug "_csrdomainlist" "$_csrdomainlist" + fi + + if [ -z "$_csrsubj" ]; then + _err "Can not read subject from csr: $_csrfile" + return 1 + fi + + _csrkeylength=$(_readKeyLengthFromCSR "$_csrfile") + if [ "$?" != "0" ] || [ -z "$_csrkeylength" ]; then + _err "Can not read key length from csr: $_csrfile" + return 1 + fi + + _initpath "$_csrsubj" "$_csrkeylength" + mkdir -p "$DOMAIN_PATH" + + _info "Copy csr to: $CSR_PATH" + cp "$_csrfile" "$CSR_PATH" + + issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" + +} + +showcsr() { + _csrfile="$1" + _csrd="$2" + if [ -z "$_csrfile" ] && [ -z "$_csrd" ]; then + _usage "Usage: $PROJECT_ENTRY --showcsr --csr mycsr.csr" + return 1 + fi + + _initpath + + _csrsubj=$(_readSubjectFromCSR "$_csrfile") + if [ "$?" != "0" ] || [ -z "$_csrsubj" ]; then + _err "Can not read subject from csr: $_csrfile" + return 1 + fi + + _info "Subject=$_csrsubj" + + _csrdomainlist=$(_readSubjectAltNamesFromCSR "$_csrfile") + if [ "$?" != "0" ]; then + _err "Can not read domain list from csr: $_csrfile" + return 1 + fi + _debug "_csrdomainlist" "$_csrdomainlist" + + _info "SubjectAltNames=$_csrdomainlist" + + _csrkeylength=$(_readKeyLengthFromCSR "$_csrfile") + if [ "$?" != "0" ] || [ -z "$_csrkeylength" ]; then + _err "Can not read key length from csr: $_csrfile" + return 1 + fi + _info "KeyLength=$_csrkeylength" +} + +list() { + _raw="$1" + _initpath + + _sep="|" + if [ "$_raw" ]; then + printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Created${_sep}Renew" + for di in "${CERT_HOME}"/*.*/; do + if ! [ -d "$di" ]; then + _debug "Not directory, skip: $di" + continue + fi + d=$(basename "$di") + _debug d "$d" + ( + if _endswith "$d" "$ECC_SUFFIX"; then + _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) + d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) + fi + _initpath "$d" "$_isEcc" + if [ -f "$DOMAIN_CONF" ]; then + . "$DOMAIN_CONF" + printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" + fi + ) + done + else + if _exists column; then + list "raw" | column -t -s "$_sep" + else + list "raw" | tr "$_sep" '\t' + fi + fi + +} + +deploy() { + Le_Domain="$1" + Le_DeployHook="$2" + _isEcc="$3" + if [ -z "$Le_DeployHook" ]; then + _usage "Usage: $PROJECT_ENTRY --deploy -d domain.com --deploy-hook cpanel [--ecc] " + return 1 + fi + + _initpath "$Le_Domain" "$_isEcc" + if [ ! -d "$DOMAIN_PATH" ]; then + _err "Domain is not valid:'$Le_Domain'" + return 1 + fi + + _deployApi="$(_findHook "$Le_Domain" deploy "$Le_DeployHook")" + if [ -z "$_deployApi" ]; then + _err "The deploy hook $Le_DeployHook is not found." + return 1 + fi + _debug _deployApi "$_deployApi" + + _savedomainconf Le_DeployHook "$Le_DeployHook" + + if ! ( + if ! . "$_deployApi"; then + _err "Load file $_deployApi error. Please check your api file and try again." + return 1 + fi + + d_command="${Le_DeployHook}_deploy" + if ! _exists "$d_command"; then + _err "It seems that your api file is not correct, it must have a function named: $d_command" + return 1 + fi + + if ! $d_command "$Le_Domain" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then + _err "Error deploy for domain:$Le_Domain" + _on_issue_err + return 1 + fi + ); then + _err "Deploy error." + return 1 + else + _info "$(__green Success)" + fi + +} + +installcert() { + Le_Domain="$1" + if [ -z "$Le_Domain" ]; then + _usage "Usage: $PROJECT_ENTRY --installcert -d domain.com [--ecc] [--certpath cert-file-path] [--keypath key-file-path] [--capath ca-cert-file-path] [ --reloadCmd reloadCmd] [--fullchainpath fullchain-path]" + return 1 + fi + + Le_RealCertPath="$2" + Le_RealKeyPath="$3" + Le_RealCACertPath="$4" + Le_ReloadCmd="$5" + Le_RealFullChainPath="$6" + _isEcc="$7" + + _initpath "$Le_Domain" "$_isEcc" + if [ ! -d "$DOMAIN_PATH" ]; then + _err "Domain is not valid:'$Le_Domain'" + return 1 + fi + + _installcert +} + +_installcert() { + _savedomainconf "Le_RealCertPath" "$Le_RealCertPath" + _savedomainconf "Le_RealCACertPath" "$Le_RealCACertPath" + _savedomainconf "Le_RealKeyPath" "$Le_RealKeyPath" + _savedomainconf "Le_ReloadCmd" "$Le_ReloadCmd" + _savedomainconf "Le_RealFullChainPath" "$Le_RealFullChainPath" + + if [ "$Le_RealCertPath" = "$NO_VALUE" ]; then + Le_RealCertPath="" + fi + if [ "$Le_RealKeyPath" = "$NO_VALUE" ]; then + Le_RealKeyPath="" + fi + if [ "$Le_RealCACertPath" = "$NO_VALUE" ]; then + Le_RealCACertPath="" + fi + if [ "$Le_ReloadCmd" = "$NO_VALUE" ]; then + Le_ReloadCmd="" + fi + if [ "$Le_RealFullChainPath" = "$NO_VALUE" ]; then + Le_RealFullChainPath="" + fi + + if [ "$Le_RealCertPath" ]; then + + _info "Installing cert to:$Le_RealCertPath" + if [ -f "$Le_RealCertPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealCertPath" "$Le_RealCertPath".bak + fi + cat "$CERT_PATH" >"$Le_RealCertPath" + fi + + if [ "$Le_RealCACertPath" ]; then + + _info "Installing CA to:$Le_RealCACertPath" + if [ "$Le_RealCACertPath" = "$Le_RealCertPath" ]; then + echo "" >>"$Le_RealCACertPath" + cat "$CA_CERT_PATH" >>"$Le_RealCACertPath" + else + if [ -f "$Le_RealCACertPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealCACertPath" "$Le_RealCACertPath".bak + fi + cat "$CA_CERT_PATH" >"$Le_RealCACertPath" + fi + fi + + if [ "$Le_RealKeyPath" ]; then + + _info "Installing key to:$Le_RealKeyPath" + if [ -f "$Le_RealKeyPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealKeyPath" "$Le_RealKeyPath".bak + fi + cat "$CERT_KEY_PATH" >"$Le_RealKeyPath" + fi + + if [ "$Le_RealFullChainPath" ]; then + + _info "Installing full chain to:$Le_RealFullChainPath" + if [ -f "$Le_RealFullChainPath" ] && [ ! "$IS_RENEW" ]; then + cp "$Le_RealFullChainPath" "$Le_RealFullChainPath".bak + fi + cat "$CERT_FULLCHAIN_PATH" >"$Le_RealFullChainPath" + fi + + if [ "$Le_ReloadCmd" ]; then + _info "Run Le_ReloadCmd: $Le_ReloadCmd" + if ( + export CERT_PATH + export CERT_KEY_PATH + export CA_CERT_PATH + export CERT_FULLCHAIN_PATH + cd "$DOMAIN_PATH" && eval "$Le_ReloadCmd" + ); then + _info "$(__green "Reload success")" + else + _err "Reload error for :$Le_Domain" + fi + fi + +} + +#confighome +installcronjob() { + _c_home="$1" + _initpath + if ! _exists "crontab"; then + _err "crontab doesn't exist, so, we can not install cron jobs." + _err "All your certs will not be renewed automatically." + _err "You must add your own cron job to call '$PROJECT_ENTRY --cron' everyday." + return 1 + fi + + _info "Installing cron job" + if ! crontab -l | grep "$PROJECT_ENTRY --cron"; then + if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY" ]; then + lesh="\"$LE_WORKING_DIR\"/$PROJECT_ENTRY" + else + _err "Can not install cronjob, $PROJECT_ENTRY not found." + return 1 + fi + + if [ "$_c_home" ]; then + _c_entry="--config-home \"$_c_home\" " + fi + _t=$(_time) + random_minute=$(_math $_t % 60) + if _exists uname && uname -a | grep SunOS >/dev/null; then + crontab -l | { + cat + echo "$random_minute 0 * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" + } | crontab -- + else + crontab -l | { + cat + echo "$random_minute 0 * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" + } | crontab - + fi + fi + if [ "$?" != "0" ]; then + _err "Install cron job failed. You need to manually renew your certs." + _err "Or you can add cronjob by yourself:" + _err "$lesh --cron --home \"$LE_WORKING_DIR\" > /dev/null" + return 1 + fi +} + +uninstallcronjob() { + if ! _exists "crontab"; then + return + fi + _info "Removing cron job" + cr="$(crontab -l | grep "$PROJECT_ENTRY --cron")" + if [ "$cr" ]; then + if _exists uname && uname -a | grep solaris >/dev/null; then + crontab -l | sed "/$PROJECT_ENTRY --cron/d" | crontab -- + else + crontab -l | sed "/$PROJECT_ENTRY --cron/d" | crontab - + fi + LE_WORKING_DIR="$(echo "$cr" | cut -d ' ' -f 9 | tr -d '"')" + _info LE_WORKING_DIR "$LE_WORKING_DIR" + if _contains "$cr" "--config-home"; then + LE_CONFIG_HOME="$(echo "$cr" | cut -d ' ' -f 11 | tr -d '"')" + _debug LE_CONFIG_HOME "$LE_CONFIG_HOME" + fi + fi + _initpath + +} + +revoke() { + Le_Domain="$1" + if [ -z "$Le_Domain" ]; then + _usage "Usage: $PROJECT_ENTRY --revoke -d domain.com [--ecc]" + return 1 + fi + + _isEcc="$2" + + _initpath "$Le_Domain" "$_isEcc" + if [ ! -f "$DOMAIN_CONF" ]; then + _err "$Le_Domain is not a issued domain, skip." + return 1 + fi + + if [ ! -f "$CERT_PATH" ]; then + _err "Cert for $Le_Domain $CERT_PATH is not found, skip." + return 1 + fi + + cert="$(_getfile "${CERT_PATH}" "${BEGIN_CERT}" "${END_CERT}" | tr -d "\r\n" | _urlencode)" + + if [ -z "$cert" ]; then + _err "Cert for $Le_Domain is empty found, skip." + return 1 + fi + + data="{\"resource\": \"revoke-cert\", \"certificate\": \"$cert\"}" + uri="$API/acme/revoke-cert" + + if [ -f "$CERT_KEY_PATH" ]; then + _info "Try domain key first." + if _send_signed_request "$uri" "$data" "" "$CERT_KEY_PATH"; then + if [ -z "$response" ]; then + _info "Revoke success." + rm -f "$CERT_PATH" + return 0 + else + _err "Revoke error by domain key." + _err "$response" + fi + fi + else + _info "Domain key file doesn't exists." + fi + + _info "Try account key." + + if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then + if [ -z "$response" ]; then + _info "Revoke success." + rm -f "$CERT_PATH" + return 0 + else + _err "Revoke error." + _debug "$response" + fi + fi + return 1 +} + +#domain ecc +remove() { + Le_Domain="$1" + if [ -z "$Le_Domain" ]; then + _usage "Usage: $PROJECT_ENTRY --remove -d domain.com [--ecc]" + return 1 + fi + + _isEcc="$2" + + _initpath "$Le_Domain" "$_isEcc" + _removed_conf="$DOMAIN_CONF.removed" + if [ ! -f "$DOMAIN_CONF" ]; then + if [ -f "$_removed_conf" ]; then + _err "$Le_Domain is already removed, You can remove the folder by yourself: $DOMAIN_PATH" + else + _err "$Le_Domain is not a issued domain, skip." + fi + return 1 + fi + + if mv "$DOMAIN_CONF" "$_removed_conf"; then + _info "$Le_Domain is removed, the key and cert files are in $(__green $DOMAIN_PATH)" + _info "You can remove them by yourself." + return 0 + else + _err "Remove $Le_Domain failed." + return 1 + fi +} + +#domain vtype +_deactivate() { + _d_domain="$1" + _d_type="$2" + _initpath + + _d_i=0 + _d_max_retry=9 + while [ "$_d_i" -lt "$_d_max_retry" ]; do + _info "Deactivate: $_d_domain" + _d_i="$(_math $_d_i + 1)" + + if ! __get_domain_new_authz "$_d_domain"; then + _err "Can not get domain new authz token." + return 1 + fi + + authzUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" + _debug "authzUri" "$authzUri" + + if [ ! -z "$code" ] && [ ! "$code" = '201' ]; then + _err "new-authz error: $response" + return 1 + fi + + entry="$(printf "%s\n" "$response" | _egrep_o '{"type":"[^"]*","status":"valid","uri"[^}]*')" + _debug entry "$entry" + + if [ -z "$entry" ]; then + _info "No more valid entry found." + break + fi + + _vtype="$(printf "%s\n" "$entry" | _egrep_o '"type": *"[^"]*"' | cut -d : -f 2 | tr -d '"')" + _debug _vtype "$_vtype" + _info "Found $_vtype" + + uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d : -f 2,3 | tr -d '"')" + _debug uri "$uri" + + if [ "$_d_type" ] && [ "$_d_type" != "$_vtype" ]; then + _info "Skip $_vtype" + continue + fi + + _info "Deactivate: $_vtype" + + if ! _send_signed_request "$authzUri" "{\"resource\": \"authz\", \"status\":\"deactivated\"}"; then + _err "Can not deactivate $_vtype." + return 1 + fi + + _info "Deactivate: $_vtype success." + + done + _debug "$_d_i" + if [ "$_d_i" -lt "$_d_max_retry" ]; then + _info "Deactivated success!" + else + _err "Deactivate failed." + fi + +} + +deactivate() { + _d_domain_list="$1" + _d_type="$2" + _initpath + _debug _d_domain_list "$_d_domain_list" + if [ -z "$(echo $_d_domain_list | cut -d , -f 1)" ]; then + _usage "Usage: $PROJECT_ENTRY --deactivate -d domain.com [-d domain.com]" + return 1 + fi + for _d_dm in $(echo "$_d_domain_list" | tr ',' ' '); do + if [ -z "$_d_dm" ] || [ "$_d_dm" = "$NO_VALUE" ]; then + continue + fi + if ! _deactivate "$_d_dm" "$_d_type"; then + return 1 + fi + done +} + +# Detect profile file if not specified as environment variable +_detect_profile() { + if [ -n "$PROFILE" -a -f "$PROFILE" ]; then + echo "$PROFILE" + return + fi + + DETECTED_PROFILE='' + SHELLTYPE="$(basename "/$SHELL")" + + if [ "$SHELLTYPE" = "bash" ]; then + if [ -f "$HOME/.bashrc" ]; then + DETECTED_PROFILE="$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + DETECTED_PROFILE="$HOME/.bash_profile" + fi + elif [ "$SHELLTYPE" = "zsh" ]; then + DETECTED_PROFILE="$HOME/.zshrc" + fi + + if [ -z "$DETECTED_PROFILE" ]; then + if [ -f "$HOME/.profile" ]; then + DETECTED_PROFILE="$HOME/.profile" + elif [ -f "$HOME/.bashrc" ]; then + DETECTED_PROFILE="$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + DETECTED_PROFILE="$HOME/.bash_profile" + elif [ -f "$HOME/.zshrc" ]; then + DETECTED_PROFILE="$HOME/.zshrc" + fi + fi + + if [ ! -z "$DETECTED_PROFILE" ]; then + echo "$DETECTED_PROFILE" + fi +} + +_initconf() { + _initpath + if [ ! -f "$ACCOUNT_CONF_PATH" ]; then + echo "#ACCOUNT_CONF_PATH=xxxx + +#ACCOUNT_EMAIL=aaa@example.com # the account email used to register account. +#ACCOUNT_KEY_PATH=\"/path/to/account.key\" +#CERT_HOME=\"/path/to/cert/home\" + + +#LOG_FILE=\"$DEFAULT_LOG_FILE\" +#LOG_LEVEL=1 + +#AUTO_UPGRADE=\"1\" + +#NO_TIMESTAMP=1 +#OPENSSL_BIN=openssl + +#USER_AGENT=\"$USER_AGENT\" + +#USER_PATH= + + + " >"$ACCOUNT_CONF_PATH" + fi +} + +# nocron +_precheck() { + _nocron="$1" + + if ! _exists "curl" && ! _exists "wget"; then + _err "Please install curl or wget first, we need to access http resources." + return 1 + fi + + if [ -z "$_nocron" ]; then + if ! _exists "crontab"; then + _err "It is recommended to install crontab first. try to install 'cron, crontab, crontabs or vixie-cron'." + _err "We need to set cron job to renew the certs automatically." + _err "Otherwise, your certs will not be able to be renewed automatically." + if [ -z "$FORCE" ]; then + _err "Please add '--force' and try install again to go without crontab." + _err "./$PROJECT_ENTRY --install --force" + return 1 + fi + fi + fi + + if ! _exists "$OPENSSL_BIN"; then + _err "Please install openssl first." + _err "We need openssl to generate keys." + return 1 + fi + + if ! _exists "nc"; then + _err "It is recommended to install nc first, try to install 'nc' or 'netcat'." + _err "We use nc for standalone server if you use standalone mode." + _err "If you don't use standalone mode, just ignore this warning." + fi + + return 0 +} + +_setShebang() { + _file="$1" + _shebang="$2" + if [ -z "$_shebang" ]; then + _usage "Usage: file shebang" + return 1 + fi + cp "$_file" "$_file.tmp" + echo "$_shebang" >"$_file" + sed -n 2,99999p "$_file.tmp" >>"$_file" + rm -f "$_file.tmp" +} + +#confighome +_installalias() { + _c_home="$1" + _initpath + + _envfile="$LE_WORKING_DIR/$PROJECT_ENTRY.env" + if [ "$_upgrading" ] && [ "$_upgrading" = "1" ]; then + echo "$(cat "$_envfile")" | sed "s|^LE_WORKING_DIR.*$||" >"$_envfile" + echo "$(cat "$_envfile")" | sed "s|^alias le.*$||" >"$_envfile" + echo "$(cat "$_envfile")" | sed "s|^alias le.sh.*$||" >"$_envfile" + fi + + if [ "$_c_home" ]; then + _c_entry=" --config-home '$_c_home'" + fi + + _setopt "$_envfile" "export LE_WORKING_DIR" "=" "\"$LE_WORKING_DIR\"" + if [ "$_c_home" ]; then + _setopt "$_envfile" "export LE_CONFIG_HOME" "=" "\"$LE_CONFIG_HOME\"" + fi + _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" + + _profile="$(_detect_profile)" + if [ "$_profile" ]; then + _debug "Found profile: $_profile" + _info "Installing alias to '$_profile'" + _setopt "$_profile" ". \"$_envfile\"" + _info "OK, Close and reopen your terminal to start using $PROJECT_NAME" + else + _info "No profile is found, you will need to go into $LE_WORKING_DIR to use $PROJECT_NAME" + fi + + #for csh + _cshfile="$LE_WORKING_DIR/$PROJECT_ENTRY.csh" + _csh_profile="$HOME/.cshrc" + if [ -f "$_csh_profile" ]; then + _info "Installing alias to '$_csh_profile'" + _setopt "$_cshfile" "setenv LE_WORKING_DIR" " " "\"$LE_WORKING_DIR\"" + if [ "$_c_home" ]; then + _setopt "$_cshfile" "setenv LE_CONFIG_HOME" " " "\"$LE_CONFIG_HOME\"" + fi + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" + _setopt "$_csh_profile" "source \"$_cshfile\"" + fi + + #for tcsh + _tcsh_profile="$HOME/.tcshrc" + if [ -f "$_tcsh_profile" ]; then + _info "Installing alias to '$_tcsh_profile'" + _setopt "$_cshfile" "setenv LE_WORKING_DIR" " " "\"$LE_WORKING_DIR\"" + if [ "$_c_home" ]; then + _setopt "$_cshfile" "setenv LE_CONFIG_HOME" " " "\"$LE_CONFIG_HOME\"" + fi + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" + _setopt "$_tcsh_profile" "source \"$_cshfile\"" + fi + +} + +# nocron confighome +install() { + + if [ -z "$LE_WORKING_DIR" ]; then + LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" + fi + + _nocron="$1" + _c_home="$2" + if ! _initpath; then + _err "Install failed." + return 1 + fi + if [ "$_nocron" ]; then + _debug "Skip install cron job" + fi + + if ! _precheck "$_nocron"; then + _err "Pre-check failed, can not install." + return 1 + fi + + #convert from le + if [ -d "$HOME/.le" ]; then + for envfile in "le.env" "le.sh.env"; do + if [ -f "$HOME/.le/$envfile" ]; then + if grep "le.sh" "$HOME/.le/$envfile" >/dev/null; then + _upgrading="1" + _info "You are upgrading from le.sh" + _info "Renaming \"$HOME/.le\" to $LE_WORKING_DIR" + mv "$HOME/.le" "$LE_WORKING_DIR" + mv "$LE_WORKING_DIR/$envfile" "$LE_WORKING_DIR/$PROJECT_ENTRY.env" + break + fi + fi + done + fi + + _info "Installing to $LE_WORKING_DIR" + + if ! mkdir -p "$LE_WORKING_DIR"; then + _err "Can not create working dir: $LE_WORKING_DIR" + return 1 + fi + + chmod 700 "$LE_WORKING_DIR" + + if ! mkdir -p "$LE_CONFIG_HOME"; then + _err "Can not create config dir: $LE_CONFIG_HOME" + return 1 + fi + + chmod 700 "$LE_CONFIG_HOME" + + cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY" + + if [ "$?" != "0" ]; then + _err "Install failed, can not copy $PROJECT_ENTRY" + return 1 + fi + + _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" + + _installalias "$_c_home" + + for subf in $_SUB_FOLDERS; do + if [ -d "$subf" ]; then + mkdir -p "$LE_WORKING_DIR/$subf" + cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/ + fi + done + + if [ ! -f "$ACCOUNT_CONF_PATH" ]; then + _initconf + fi + + if [ "$_DEFAULT_ACCOUNT_CONF_PATH" != "$ACCOUNT_CONF_PATH" ]; then + _setopt "$_DEFAULT_ACCOUNT_CONF_PATH" "ACCOUNT_CONF_PATH" "=" "\"$ACCOUNT_CONF_PATH\"" + fi + + if [ "$_DEFAULT_CERT_HOME" != "$CERT_HOME" ]; then + _saveaccountconf "CERT_HOME" "$CERT_HOME" + fi + + if [ "$_DEFAULT_ACCOUNT_KEY_PATH" != "$ACCOUNT_KEY_PATH" ]; then + _saveaccountconf "ACCOUNT_KEY_PATH" "$ACCOUNT_KEY_PATH" + fi + + if [ -z "$_nocron" ]; then + installcronjob "$_c_home" + fi + + if [ -z "$NO_DETECT_SH" ]; then + #Modify shebang + if _exists bash; then + _info "Good, bash is found, so change the shebang to use bash as preferred." + _shebang='#!/usr/bin/env bash' + _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang" + for subf in $_SUB_FOLDERS; do + if [ -d "$LE_WORKING_DIR/$subf" ]; then + for _apifile in "$LE_WORKING_DIR/$subf/"*.sh; do + _setShebang "$_apifile" "$_shebang" + done + fi + done + fi + fi + + _info OK +} + +# nocron +uninstall() { + _nocron="$1" + if [ -z "$_nocron" ]; then + uninstallcronjob + fi + _initpath + + _uninstallalias + + rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY" + _info "The keys and certs are in \"$(__green "$LE_CONFIG_HOME")\", you can remove them by yourself." + +} + +_uninstallalias() { + _initpath + + _profile="$(_detect_profile)" + if [ "$_profile" ]; then + _info "Uninstalling alias from: '$_profile'" + text="$(cat "$_profile")" + echo "$text" | sed "s|^.*\"$LE_WORKING_DIR/$PROJECT_NAME.env\"$||" >"$_profile" + fi + + _csh_profile="$HOME/.cshrc" + if [ -f "$_csh_profile" ]; then + _info "Uninstalling alias from: '$_csh_profile'" + text="$(cat "$_csh_profile")" + echo "$text" | sed "s|^.*\"$LE_WORKING_DIR/$PROJECT_NAME.csh\"$||" >"$_csh_profile" + fi + + _tcsh_profile="$HOME/.tcshrc" + if [ -f "$_tcsh_profile" ]; then + _info "Uninstalling alias from: '$_csh_profile'" + text="$(cat "$_tcsh_profile")" + echo "$text" | sed "s|^.*\"$LE_WORKING_DIR/$PROJECT_NAME.csh\"$||" >"$_tcsh_profile" + fi + +} + +cron() { + IN_CRON=1 + _initpath + if [ "$AUTO_UPGRADE" = "1" ]; then + export LE_WORKING_DIR + ( + if ! upgrade; then + _err "Cron:Upgrade failed!" + return 1 + fi + ) + . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null + + if [ -t 1 ]; then + __INTERACTIVE="1" + fi + + _info "Auto upgraded to: $VER" + fi + renewAll + _ret="$?" + IN_CRON="" + exit $_ret +} + +version() { + echo "$PROJECT" + echo "v$VER" +} + +showhelp() { + _initpath + version + echo "Usage: $PROJECT_ENTRY command ...[parameters].... +Commands: + --help, -h Show this help message. + --version, -v Show version info. + --install Install $PROJECT_NAME to your system. + --uninstall Uninstall $PROJECT_NAME, and uninstall the cron job. + --upgrade Upgrade $PROJECT_NAME to the latest code from $PROJECT . + --issue Issue a cert. + --signcsr Issue a cert from an existing csr. + --deploy Deploy the cert to your server. + --install-cert Install the issued cert to apache/nginx or any other server. + --renew, -r Renew a cert. + --renew-all Renew all the certs. + --revoke Revoke a cert. + --remove Remove the cert from $PROJECT + --list List all the certs. + --showcsr Show the content of a csr. + --install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. + --uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically. + --cron Run cron job to renew all the certs. + --toPkcs Export the certificate and key to a pfx file. + --update-account Update account info. + --register-account Register account key. + --createAccountKey, -cak Create an account private key, professional use. + --createDomainKey, -cdk Create an domain private key, professional use. + --createCSR, -ccsr Create CSR , professional use. + --deactivate Deactivate the domain authz, professional use. + +Parameters: + --domain, -d domain.tld Specifies a domain, used to issue, renew or revoke etc. + --force, -f Used to force to install or force to renew a cert immediately. + --staging, --test Use staging server, just for test. + --debug Output debug info. + + --webroot, -w /path/to/webroot Specifies the web root folder for web root mode. + --standalone Use standalone mode. + --tls Use standalone tls mode. + --apache Use apache mode. + --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file] Use dns mode or dns api. + --dnssleep [$DEFAULT_DNS_SLEEP] The time in seconds to wait for all the txt records to take effect in dns api mode. Default $DEFAULT_DNS_SLEEP seconds. + + --keylength, -k [2048] Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384. + --accountkeylength, -ak [2048] Specifies the account key length. + --log [/path/to/logfile] Specifies the log file. The default is: \"$DEFAULT_LOG_FILE\" if you don't give a file path here. + --log-level 1|2 Specifies the log level, default is 1. + + These parameters are to install the cert to nginx/apache or anyother server after issue/renew a cert: + + --certpath /path/to/real/cert/file After issue/renew, the cert will be copied to this path. + --keypath /path/to/real/key/file After issue/renew, the key will be copied to this path. + --capath /path/to/real/ca/file After issue/renew, the intermediate cert will be copied to this path. + --fullchainpath /path/to/fullchain/file After issue/renew, the fullchain cert will be copied to this path. + + --reloadcmd \"service nginx reload\" After issue/renew, it's used to reload the server. + + --accountconf Specifies a customized account config file. + --home Specifies the home dir for $PROJECT_NAME . + --cert-home Specifies the home dir to save all the certs, only valid for '--install' command. + --config-home Specifies the home dir to save all the configurations. + --useragent Specifies the user agent string. it will be saved for future use too. + --accountemail Specifies the account email for registering, Only valid for the '--install' command. + --accountkey Specifies the account key path, Only valid for the '--install' command. + --days Specifies the days to renew the cert when using '--issue' command. The max value is $MAX_RENEW days. + --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. + --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. + --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. + --listraw Only used for '--list' command, list the certs in raw format. + --stopRenewOnError, -se Only valid for '--renew-all' command. Stop if one cert has error in renewal. + --insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted. + --ca-bundle Specifices the path to the CA certificate bundle to verify api server's certificate. + --nocron Only valid for '--install' command, which means: do not install the default cron job. In this case, the certs will not be renewed automatically. + --ecc Specifies to use the ECC cert. Valid for '--install-cert', '--renew', '--revoke', '--toPkcs' and '--createCSR' + --csr Specifies the input csr. + --pre-hook Command to be run before obtaining any certificates. + --post-hook Command to be run after attempting to obtain/renew certificates. No matter the obain/renew is success or failed. + --renew-hook Command to be run once for each successfully renewed certificate. + --deploy-hook The hook file to deploy cert + --ocsp-must-staple, --ocsp Generate ocsp must Staple extension. + --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. + --listen-v4 Force standalone/tls server to listen at ipv4. + --listen-v6 Force standalone/tls server to listen at ipv6. + --openssl-bin Specifies a custom openssl bin location. + " +} + +# nocron +_installOnline() { + _info "Installing from online archive." + _nocron="$1" + if [ ! "$BRANCH" ]; then + BRANCH="master" + fi + + target="$PROJECT/archive/$BRANCH.tar.gz" + _info "Downloading $target" + localname="$BRANCH.tar.gz" + if ! _get "$target" >$localname; then + _err "Download error." + return 1 + fi + ( + _info "Extracting $localname" + if ! (tar xzf $localname || gtar xzf $localname); then + _err "Extraction error." + exit 1 + fi + + cd "$PROJECT_NAME-$BRANCH" + chmod +x $PROJECT_ENTRY + if ./$PROJECT_ENTRY install "$_nocron"; then + _info "Install success!" + fi + + cd .. + + rm -rf "$PROJECT_NAME-$BRANCH" + rm -f "$localname" + ) +} + +upgrade() { + if ( + _initpath + export LE_WORKING_DIR + cd "$LE_WORKING_DIR" + _installOnline "nocron" + ); then + _info "Upgrade success!" + exit 0 + else + _err "Upgrade failed!" + exit 1 + fi +} + +_processAccountConf() { + if [ "$_useragent" ]; then + _saveaccountconf "USER_AGENT" "$_useragent" + elif [ "$USER_AGENT" ] && [ "$USER_AGENT" != "$DEFAULT_USER_AGENT" ]; then + _saveaccountconf "USER_AGENT" "$USER_AGENT" + fi + + if [ "$_accountemail" ]; then + _saveaccountconf "ACCOUNT_EMAIL" "$_accountemail" + elif [ "$ACCOUNT_EMAIL" ] && [ "$ACCOUNT_EMAIL" != "$DEFAULT_ACCOUNT_EMAIL" ]; then + _saveaccountconf "ACCOUNT_EMAIL" "$ACCOUNT_EMAIL" + fi + + if [ "$_openssl_bin" ]; then + _saveaccountconf "OPENSSL_BIN" "$_openssl_bin" + elif [ "$OPENSSL_BIN" ] && [ "$OPENSSL_BIN" != "$DEFAULT_OPENSSL_BIN" ]; then + _saveaccountconf "OPENSSL_BIN" "$OPENSSL_BIN" + fi + + if [ "$_auto_upgrade" ]; then + _saveaccountconf "AUTO_UPGRADE" "$_auto_upgrade" + elif [ "$AUTO_UPGRADE" ]; then + _saveaccountconf "AUTO_UPGRADE" "$AUTO_UPGRADE" + fi + +} + +_process() { + _CMD="" + _domain="" + _altdomains="$NO_VALUE" + _webroot="" + _keylength="" + _accountkeylength="" + _certpath="" + _keypath="" + _capath="" + _fullchainpath="" + _reloadcmd="" + _password="" + _accountconf="" + _useragent="" + _accountemail="" + _accountkey="" + _certhome="" + _confighome="" + _httpport="" + _tlsport="" + _dnssleep="" + _listraw="" + _stopRenewOnError="" + #_insecure="" + _ca_bundle="" + _nocron="" + _ecc="" + _csr="" + _pre_hook="" + _post_hook="" + _renew_hook="" + _deploy_hook="" + _logfile="" + _log="" + _local_address="" + _log_level="" + _auto_upgrade="" + _listen_v4="" + _listen_v6="" + _openssl_bin="" + while [ ${#} -gt 0 ]; do + case "${1}" in + + --help | -h) + showhelp + return + ;; + --version | -v) + version + return + ;; + --install) + _CMD="install" + ;; + --uninstall) + _CMD="uninstall" + ;; + --upgrade) + _CMD="upgrade" + ;; + --issue) + _CMD="issue" + ;; + --deploy) + _CMD="deploy" + ;; + --signcsr) + _CMD="signcsr" + ;; + --showcsr) + _CMD="showcsr" + ;; + --installcert | -i | --install-cert) + _CMD="installcert" + ;; + --renew | -r) + _CMD="renew" + ;; + --renewAll | --renewall | --renew-all) + _CMD="renewAll" + ;; + --revoke) + _CMD="revoke" + ;; + --remove) + _CMD="remove" + ;; + --list) + _CMD="list" + ;; + --installcronjob | --install-cronjob) + _CMD="installcronjob" + ;; + --uninstallcronjob | --uninstall-cronjob) + _CMD="uninstallcronjob" + ;; + --cron) + _CMD="cron" + ;; + --toPkcs) + _CMD="toPkcs" + ;; + --createAccountKey | --createaccountkey | -cak) + _CMD="createAccountKey" + ;; + --createDomainKey | --createdomainkey | -cdk) + _CMD="createDomainKey" + ;; + --createCSR | --createcsr | -ccr) + _CMD="createCSR" + ;; + --deactivate) + _CMD="deactivate" + ;; + --updateaccount | --update-account) + _CMD="updateaccount" + ;; + --registeraccount | --register-account) + _CMD="registeraccount" + ;; + --domain | -d) + _dvalue="$2" + + if [ "$_dvalue" ]; then + if _startswith "$_dvalue" "-"; then + _err "'$_dvalue' is not a valid domain for parameter '$1'" + return 1 + fi + if _is_idn "$_dvalue" && ! _exists idn; then + _err "It seems that $_dvalue is an IDN( Internationalized Domain Names), please install 'idn' command first." + return 1 + fi + + if [ -z "$_domain" ]; then + _domain="$_dvalue" + else + if [ "$_altdomains" = "$NO_VALUE" ]; then + _altdomains="$_dvalue" + else + _altdomains="$_altdomains,$_dvalue" + fi + fi + fi + + shift + ;; + + --force | -f) + FORCE="1" + ;; + --staging | --test) + STAGE="1" + ;; + --debug) + if [ -z "$2" ] || _startswith "$2" "-"; then + DEBUG="1" + else + DEBUG="$2" + shift + fi + ;; + --webroot | -w) + wvalue="$2" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + shift + ;; + --standalone) + wvalue="$NO_VALUE" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --local-address) + lvalue="$2" + _local_address="$_local_address$lvalue," + shift + ;; + --apache) + wvalue="apache" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --tls) + wvalue="$W_TLS" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --dns) + wvalue="dns" + if ! _startswith "$2" "-"; then + wvalue="$2" + shift + fi + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --dnssleep) + _dnssleep="$2" + Le_DNSSleep="$_dnssleep" + shift + ;; + + --keylength | -k) + _keylength="$2" + shift + ;; + --accountkeylength | -ak) + _accountkeylength="$2" + shift + ;; + + --certpath) + _certpath="$2" + shift + ;; + --keypath) + _keypath="$2" + shift + ;; + --capath) + _capath="$2" + shift + ;; + --fullchainpath) + _fullchainpath="$2" + shift + ;; + --reloadcmd | --reloadCmd) + _reloadcmd="$2" + shift + ;; + --password) + _password="$2" + shift + ;; + --accountconf) + _accountconf="$2" + ACCOUNT_CONF_PATH="$_accountconf" + shift + ;; + --home) + LE_WORKING_DIR="$2" + shift + ;; + --certhome | --cert-home) + _certhome="$2" + CERT_HOME="$_certhome" + shift + ;; + --config-home) + _confighome="$2" + LE_CONFIG_HOME="$_confighome" + shift + ;; + --useragent) + _useragent="$2" + USER_AGENT="$_useragent" + shift + ;; + --accountemail) + _accountemail="$2" + ACCOUNT_EMAIL="$_accountemail" + shift + ;; + --accountkey) + _accountkey="$2" + ACCOUNT_KEY_PATH="$_accountkey" + shift + ;; + --days) + _days="$2" + Le_RenewalDays="$_days" + shift + ;; + --httpport) + _httpport="$2" + Le_HTTPPort="$_httpport" + shift + ;; + --tlsport) + _tlsport="$2" + Le_TLSPort="$_tlsport" + shift + ;; + + --listraw) + _listraw="raw" + ;; + --stopRenewOnError | --stoprenewonerror | -se) + _stopRenewOnError="1" + ;; + --insecure) + #_insecure="1" + HTTPS_INSECURE="1" + ;; + --ca-bundle) + _ca_bundle="$(readlink -f "$2")" + CA_BUNDLE="$_ca_bundle" + shift + ;; + --nocron) + _nocron="1" + ;; + --ecc) + _ecc="isEcc" + ;; + --csr) + _csr="$2" + shift + ;; + --pre-hook) + _pre_hook="$2" + shift + ;; + --post-hook) + _post_hook="$2" + shift + ;; + --renew-hook) + _renew_hook="$2" + shift + ;; + --deploy-hook) + _deploy_hook="$2" + shift + ;; + --ocsp-must-staple | --ocsp) + Le_OCSP_Staple="1" + ;; + --log | --logfile) + _log="1" + _logfile="$2" + if _startswith "$_logfile" '-'; then + _logfile="" + else + shift + fi + LOG_FILE="$_logfile" + if [ -z "$LOG_LEVEL" ]; then + LOG_LEVEL="$DEFAULT_LOG_LEVEL" + fi + ;; + --log-level) + _log_level="$2" + LOG_LEVEL="$_log_level" + shift + ;; + --auto-upgrade) + _auto_upgrade="$2" + if [ -z "$_auto_upgrade" ] || _startswith "$_auto_upgrade" '-'; then + _auto_upgrade="1" + else + shift + fi + AUTO_UPGRADE="$_auto_upgrade" + ;; + --listen-v4) + _listen_v4="1" + Le_Listen_V4="$_listen_v4" + ;; + --listen-v6) + _listen_v6="1" + Le_Listen_V6="$_listen_v6" + ;; + --openssl-bin) + _openssl_bin="$2" + OPENSSL_BIN="$_openssl_bin" + ;; + *) + _err "Unknown parameter : $1" + return 1 + ;; + esac + + shift 1 + done + + if [ "${_CMD}" != "install" ]; then + __initHome + if [ "$_log" ]; then + if [ -z "$_logfile" ]; then + _logfile="$DEFAULT_LOG_FILE" + fi + fi + if [ "$_logfile" ]; then + _saveaccountconf "LOG_FILE" "$_logfile" + LOG_FILE="$_logfile" + fi + + if [ "$_log_level" ]; then + _saveaccountconf "LOG_LEVEL" "$_log_level" + LOG_LEVEL="$_log_level" + fi + + _processAccountConf + fi + + _debug2 LE_WORKING_DIR "$LE_WORKING_DIR" + + if [ "$DEBUG" ]; then + version + fi + + case "${_CMD}" in + install) install "$_nocron" "$_confighome" ;; + uninstall) uninstall "$_nocron" ;; + upgrade) upgrade ;; + issue) + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_certpath" "$_keypath" "$_capath" "$_reloadcmd" "$_fullchainpath" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" + ;; + deploy) + deploy "$_domain" "$_deploy_hook" "$_ecc" + ;; + signcsr) + signcsr "$_csr" "$_webroot" + ;; + showcsr) + showcsr "$_csr" "$_domain" + ;; + installcert) + installcert "$_domain" "$_certpath" "$_keypath" "$_capath" "$_reloadcmd" "$_fullchainpath" "$_ecc" + ;; + renew) + renew "$_domain" "$_ecc" + ;; + renewAll) + renewAll "$_stopRenewOnError" + ;; + revoke) + revoke "$_domain" "$_ecc" + ;; + remove) + remove "$_domain" "$_ecc" + ;; + deactivate) + deactivate "$_domain,$_altdomains" + ;; + registeraccount) + registeraccount "$_accountkeylength" + ;; + updateaccount) + updateaccount + ;; + list) + list "$_listraw" + ;; + installcronjob) installcronjob "$_confighome" ;; + uninstallcronjob) uninstallcronjob ;; + cron) cron ;; + toPkcs) + toPkcs "$_domain" "$_password" "$_ecc" + ;; + createAccountKey) + createAccountKey "$_accountkeylength" + ;; + createDomainKey) + createDomainKey "$_domain" "$_keylength" + ;; + createCSR) + createCSR "$_domain" "$_altdomains" "$_ecc" + ;; + + *) + if [ "$_CMD" ]; then + _err "Invalid command: $_CMD" + fi + showhelp + return 1 + ;; + esac + _ret="$?" + if [ "$_ret" != "0" ]; then + return $_ret + fi + + if [ "${_CMD}" = "install" ]; then + if [ "$_log" ]; then + if [ -z "$LOG_FILE" ]; then + LOG_FILE="$DEFAULT_LOG_FILE" + fi + _saveaccountconf "LOG_FILE" "$LOG_FILE" + fi + + if [ "$_log_level" ]; then + _saveaccountconf "LOG_LEVEL" "$_log_level" + fi + _processAccountConf + fi + +} + +if [ "$INSTALLONLINE" ]; then + INSTALLONLINE="" + _installOnline + exit +fi + +main() { + [ -z "$1" ] && showhelp && return + if _startswith "$1" '-'; then _process "$@"; else "$@"; fi +} + +main "$@" diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php new file mode 100755 index 000000000..890cea028 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php @@ -0,0 +1,872 @@ +#!/usr/local/bin/php + + * Copyright (C) 2008 Shrew Soft Inc + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +// Hello. I am the spaghetti monster. Yummy. + +// Use legacy code to manage certificates. +require_once("config.inc"); +require_once("certs.inc"); +require_once("legacy_bindings.inc"); +require_once("interfaces.inc"); +require_once("util.inc"); +// Some stuff requires the almighty MVC framework. +use OPNsense\Core\Config; +use OPNsense\Base; +use OPNsense\AcmeClient\AcmeClient; +global $config; + +/* CLI arguments: + * -a (action) + * -c (certificate id, NOT the uuid) + * -A (all certificates) + * -C (cron, special rules apply when running as cronjob) + * -F (force, rewew/recreate) + * -S (staging) + */ +$options = getopt("a:c:ACFS"); + +// Simple validation +if (!isset($options["a"]) or (!isset($options["c"]) and !isset($options["A"]))) { + // ALL actions require either a certificate ID or the -A switch + echo "ERROR: not enough arguments\n"; + exit(1); +} +if (($options["a"] == 'revoke') and !isset($options["c"])) { + echo "ERROR: option revoke requires a certificate ID\n"; + exit(1); +} + +// Cron mode +if (isset($options["C"])) { + // Automatically work on ALL certificates + $options["A"] = ""; +} + +// Run the specified action +switch ($options["a"]) { + case 'sign': + //$result = sign_or_renew_cert($options["c"]); + $result = cert_action_validator($options["c"]); + echo json_encode(Array('status'=>$result)); + break; + case 'renew': + //$result = sign_or_renew_cert($options["c"]); + $result = cert_action_validator($options["c"]); + echo json_encode(Array('status'=>$result)); + break; + case 'revoke': + //$result = revoke_cert($options["c"]); + $result = cert_action_validator($options["c"]); + echo json_encode(Array('status'=>$result)); + exit(1); + case 'cleanup': + // TODO: remove certs from filesystem if they cannot be found in config.xml + echo "XXX: not yet implemented\n"; + exit(1); + default: + echo "ERROR: invalid argument specified\n"; + log_error("invalid argument specified"); + exit(1); +} + +// ALL certificate work starts here. First we do some common validation and +// make sure that everything is prepared for acme client to run. +// The actual issue/renew/revoke work is done by separate functions. +function cert_action_validator($opt_cert_id) +{ + global $options; + + $modelObj = new OPNsense\AcmeClient\AcmeClient; + + // Search for cert ID in configuration + $configObj = Config::getInstance()->object(); + if (isset($configObj->OPNsense->AcmeClient->certificates)) { + foreach ($configObj->OPNsense->AcmeClient->certificates->children() as $certObj) { + + // Extract cert ID + $cert_id = (string)$certObj->id; + if (empty($cert_id)) { + continue; // Cert is invalid, skip it. + } + + // Either work with ALL certificates or check if cert ID matches + if (isset($options["A"]) or ((string)$cert_id == (string)$opt_cert_id)) { + + // Ignore disabled certificates + if ($certObj->enabled == 0) { + if (isset($options["A"])) continue; // skip to next item + return(1); // Cert is disabled, skip it. + } + + // Extract Account from referenced obj + $acctRef = (string)$certObj->account; + $acctObj = null; + $acctref_found = false; + foreach ($modelObj->getNodeByReference('accounts.account')->__items as $node) { + if ((string)$node->getAttributes()["uuid"] == $acctRef ) { + $acctref_found = true; + $acctObj = $node; + break; // Match! Go ahead. + } + } + + // Make sure we found the configured account + if ( $acctref_found == true ) { + // Ensure that this account was properly setup and registered. + $acct_result = run_acme_account_registration($acctObj,$certObj,$modelObj); + if (!$acct_result) { + //echo "DEBUG: account registration OK\n"; + } else { + //echo "DEBUG: account registration failed\n"; + log_error("AcmeClient: account registration failed"); + if (isset($options["A"])) continue; // skip to next item + return(1); + } + } else { + //echo "DEBUG: account not found\n"; + log_error("AcmeClient: account not found"); + if (isset($options["A"])) continue; // skip to next item + return(1); + } + + // Extract Validation Method from referenced obj + $valRef = (string)$certObj->validationMethod; + $valObj = null; + $ref_found = false; + foreach ($modelObj->getNodeByReference('validations.validation')->__items as $node) { + if ((string)$node->getAttributes()["uuid"] == $valRef ) { + $ref_found = true; + $valObj = $node; + break; // Match! Go ahead. + } + } + + // Make sure we found the configured validation method + if ($ref_found == true) { + + // Was a revocation requested? + // NOTE: Revocation is not even considered when some elements have already been + // deleted from the GUI. It's likely that it would fail anyway. + if ($options["a"] == "revoke") { + // Start acme client to revoke the certificate + $rev_result = revoke_cert($certObj,$valObj,$acctObj); + if (!$rev_result) { + return(0); // Success! + } else { + // Revocation failure + log_error("AcmeClient: revocation for certificate failed"); + if (isset($options["A"])) continue; // skip to next item + return(1); + } + } + + // Which validation method? + if ((string)$valObj->method == 'http01' or ((string)$valObj->method == 'dns01')) { + // Start acme client to issue or renew certificate + $val_result = run_acme_validation($certObj,$valObj,$acctObj); + if (!$val_result) { + // Import certificate to Cert Manager + if (!import_certificate($certObj,$modelObj)) { + //echo "DEBUG: cert import done\n"; + } else { + log_error("AcmeClient: unable to import certificate: " . (string)$certObj->name); + if (isset($options["A"])) continue; // skip to next item + return(1); + } + } else { + // validation failure + log_error("AcmeClient: validation for certificate failed: " . (string)$certObj->name); + if (isset($options["A"])) continue; // skip to next item + return(1); + } + } else { + log_error("AcmeClient: invalid validation method specified: " . (string)$valObj->method); + if (isset($options["A"])) continue; // skip to next item + return(1); + } + + } else { + log_error("AcmeClient: validation method not found for cert " . $certObj->name); + if (isset($options["A"])) continue; // skip to next item + return(1); + } + + // Work on ALL certificates? + if (!isset($options["A"])) { + break; // Stop after first match. + } + } + } + } else { + log_error("AcmeClient: no LE certificates found in configuration"); + return(1); + } + return(0); +} + +// Prepare optional parameters for acme client +function eval_optional_acme_args() +{ + global $options; + $configObj = Config::getInstance()->object(); + + $acme_args = Array(); + // Force certificate renewal? + $acme_args[] = isset($options["F"]) ? "--force" : null; + // Use LE staging environment? + $acme_args[] = $configObj->OPNsense->AcmeClient->settings->environment == "stg" ? "--staging" : null; + $acme_args[] = isset($options["S"]) ? "--staging" : null; // for debug purpose + + // Remove empty and duplicate elements from array + return(array_unique(array_filter($acme_args))); +} + +// Create account keys and register accounts, export/import them from/to filesystem/config.xml +function run_acme_account_registration($acctObj,$certObj,$modelObj) +{ + global $options; + + // Prepare optional parameters for acme-client + $acme_args = eval_optional_acme_args(); + + // Collect account information + $account_conf_dir = "/var/etc/acme-client/accounts/" . $acctObj->id; + $account_conf_file = $account_conf_dir . "/account.conf"; + $account_key_file = $account_conf_dir . "/account.key"; + $acme_conf = Array(); + $acme_conf[] = "CERT_HOME='/var/etc/acme-client/home'"; + $acme_conf[] = "LOG_FILE='/var/log/acme.sh.log'"; + $acme_conf[] = "ACCOUNT_KEY_PATH='" . $account_key_file . "'"; + if (!empty((string)$acctObj->email)) { + $acme_conf[] = "ACCOUNT_EMAIL='" . (string)$acctObj->email . "'"; + } + + // Create account configuration file + if (!is_dir($account_conf_dir)) { + mkdir($account_conf_dir, 0700, true); + } + file_put_contents($account_conf_file, (string)implode("\n",$acme_conf) . "\n"); + chmod($account_conf_file, 0600); + //echo "DEBUG: ${account_conf_file} | ${account_key_file}\n"; + + // Check if account key already exists + if ( is_file($account_key_file) ) { + //echo "DEBUG: account key found\n"; + } else { + // Check if we have an account key in our configuration + if (!empty((string)$acctObj->key)) { + // Write key to disk + file_put_contents($account_key_file, (string)base64_decode((string)$acctObj->key)); + chmod($account_key_file, 0600); + //echo "DEBUG: exported existing account key to filesystem\n"; + } else { + // Do not generate new key if a revocation was requested. + if ($options["a"] == "revoke") { + log_error("AcmeClient: account key not found, but a revocation was requested"); + return(1); + } + + // Let acme client generate a new account key + $acmecmd = "/usr/local/opnsense/scripts/OPNsense/AcmeClient/acme.sh " + . implode(" ", $acme_args) . " " + . "--createAccountKey " + . "--accountkeylength 4096 " + . "--home /var/etc/acme-client/home " + . "--accountconf " . $account_conf_file; + //echo "DEBUG: executing command: " . $acmecmd . "\n"; + $result = mwexec($acmecmd); + + // Check exit code + if (!($result)) { + //echo "DEBUG: created a new account key\n"; + } else { + //echo "DEBUG: AcmeClient: failed to create a new account key\n"; + log_error("AcmeClient: failed to create a new account key"); + return(1); + } + + // Read account key + $account_key_content = @file_get_contents($account_key_file); + if ($account_key_content == false) { + //echo "DEBUG: AcmeClient: unable to read account key from file\n"; + log_error("AcmeClient: unable to read account key from file"); + return(1); + } + + // Import account key into config + $acctObj->key = base64_encode($account_key_content); + // serialize to config and save + $modelObj->serializeToConfig(); + Config::getInstance()->save(); + } + } + + // Check if account was already registered + if (!empty((string)$acctObj->lastUpdate)) { + //echo "DEBUG: account key already registered\n"; + } else { + // Do not register new account if a revocation was requested. + if ($options["a"] == "revoke") { + log_error("AcmeClient: account not registered, but a revocation was requested"); + return(1); + } + + // Run acme client to register the account + $acmecmd = "/usr/local/opnsense/scripts/OPNsense/AcmeClient/acme.sh " + . implode(" ", $acme_args) . " " + . "--registeraccount " + . "--log-level 2 " + . "--home /var/etc/acme-client/home " + . "--accountconf " . $account_conf_file; + //echo "DEBUG: executing command: " . $acmecmd . "\n"; + $result = mwexec($acmecmd); + + // Check exit code + if (!($result)) { + //echo "DEBUG: registered a new account key\n"; + } else { + //echo "DEBUG: AcmeClient: failed to register a new account key\n"; + log_error("AcmeClient: failed to register a new account key"); + return(1); + } + + // Set update/create time in config + $acctObj->lastUpdate = time(); + // serialize to config and save + $modelObj->serializeToConfig(); + Config::getInstance()->save(); + } + + return; +} + +// Run acme client with HTTP-01 or DNS-01 validation to issue/renew certificate +function run_acme_validation($certObj,$valObj,$acctObj) +{ + // TODO: add support for other HTTP-01 validation services/methods + + global $options; + + // Collect account information + $account_conf_dir = "/var/etc/acme-client/accounts/" . $acctObj->id; + $account_conf_file = $account_conf_dir . "/account.conf"; + + // Generate certificate filenames + $cert_id = (string)$certObj->id; + $cert_filename = "/var/etc/acme-client/certs/${cert_id}/cert.pem"; + $cert_chain_filename = "/var/etc/acme-client/certs/${cert_id}/chain.pem"; + $cert_fullchain_filename = "/var/etc/acme-client/certs/${cert_id}/fullchain.pem"; + $key_filename = "/var/etc/acme-client/keys/${cert_id}/private.key"; + + // Setup our own ACME environment + $certdir = "/var/etc/acme-client/certs/${cert_id}"; + $keydir = "/var/etc/acme-client/keys/${cert_id}"; + $configdir = "/var/etc/acme-client/configs/${cert_id}"; + foreach (Array($certdir, $keydir, $configdir) as $dir) { + if (!is_dir($dir)) { + mkdir($dir, 0700, true); + } + } + + // Preparation to run acme client + $acme_args = eval_optional_acme_args(); + $proc_env = Array(); // env variables for proc_open() + $proc_env['PATH'] = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:/root/bin'; + $proc_desc = array( // descriptor array for proc_open() + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + $proc_pipes = array(); + + // Do we need to issue or renew the certificate? + $acme_action = !empty((string)$certObj->lastUpdate) ? "renew" : "issue"; + + // Calculate next renewal date + $last_update = !empty((string)$certObj->lastUpdate) ? (string)$certObj->lastUpdate : 0; + $renew_cert = false; + $current_time = new \DateTime(); + $last_update_time = new \DateTime(); + $last_update_time->setTimestamp($last_update); + $renew_interval = (string)$certObj->renewInterval; + $next_update = $last_update_time->add(new \DateInterval('P'.$renew_interval.'D')); + + // Check if it's time to renew, otherwise report success + if (isset($options["F"]) or ($current_time >= $next_update)) { + $renew_cert = true; + } else { + // Renewal not yet required, report success + return(0); + } + + // Try HTTP-01 or DNS-01 validation? + $val_method = (string)$valObj->method; + $acme_validation = ""; // val.method as argument for acme.sh + $acme_hook_options = Array(); // store addition arguments for acme.sh here + switch ($val_method) { + case 'http01': + $acme_validation = "--webroot /var/etc/acme-client/challenges "; + break; + case 'dns01': + $acme_validation = "--dns " . (string)$valObj->dns_service . " "; + break; + default: + log_error("AcmeClient: invalid validation method specified: " . (string)$valObj->method); + return(1); + } + + // HTTP-01: setup OPNsense internal port forward + if (($val_method == 'http01') and ((string)$valObj->http_service == 'opnsense')) { + // Get configured HTTP port for local lighttpd server + $configObj = Config::getInstance()->object(); + $local_http_port = $configObj->OPNsense->AcmeClient->settings->challengePort; + //echo "DEBUG: local http challenge port: ${local_http_port}\n"; + + // Collect all IP addresses here, automatic port forward will be applied for each IP + $iplist = Array(); + + // Add IP addresses from auto-discovery feature + if ($valObj->http_opn_autodiscovery == 1) { + $dnslist = explode(',',$certObj->altNames); + $dnslist[] = $certObj->name; + foreach($dnslist as $fqdn) { + // NOTE: This may take some time. + //echo "DEBUG: resolving ${fqdn}\n"; + $ip_found = gethostbyname("${fqdn}."); + if (!empty($ip_found)) { + //echo "DEBUG: got ip ${ip_found}\n"; + $iplist[] = (string)$ip_found; + } + } + } + + // Add IP addresses from user input + $additional_ip = (string)$valObj->http_opn_ipaddresses; + if (!empty($additional_ip)) { + foreach(explode(',',$additional_ip) as $ip) { + //echo "DEBUG: additional IP ${ip}\n"; + $iplist[] = $ip; + } + } + + // Add IP address from chosen interface + if (!empty((string)$valObj->http_opn_interface)) { + $interface_ip = get_interface_ip((string)$valObj->http_opn_interface); + if (!empty($interface_ip)) { + //echo "DEBUG: interface " . (string)$valObj->http_opn_interface . ", IP ${interface_ip}\n"; + $iplist[] = $interface_ip; + } + } + + // Generate rules for all IP addresses + $anchor_rules = ""; + if (!empty($iplist)) { + $dedup_iplist = array_unique($iplist); + // Add one rule for every IP + foreach ($dedup_iplist as $ip) { + if ($ip == '.') continue; // skip broken entries + $anchor_rules .= "rdr pass inet proto tcp from any to ${ip} port 80 -> 127.0.0.1 port ${local_http_port}\n"; + } + } else { + log_error("AcmeClient: no IP addresses found to setup port forward"); + return(1); + } + + // Abort if no rules were generated + if (empty($anchor_rules)) { + log_error("AcmeClient: unable to setup a port forward (empty ruleset)"); + return(1); + } + + // Create temporary port forward to allow acme challenges to get through + $anchor_setup = "rdr-anchor \"acme-client\"\n"; + file_put_contents("${configdir}/acme_anchor_setup", $anchor_setup); + chmod("${configdir}/acme_anchor_setup", 0600); + mwexec("/sbin/pfctl -f ${configdir}/acme_anchor_setup"); + file_put_contents("${configdir}/acme_anchor_rules", $anchor_rules); + chmod("${configdir}/acme_anchor_rules", 0600); + mwexec("/sbin/pfctl -a acme-client -f ${configdir}/acme_anchor_rules"); + } + + // Prepare DNS-01 hooks + if ($val_method == 'dns01') { + // Some common stuff + $secret_key_filename = "${configdir}/secret.key"; + $acme_args[] = '--dnssleep ' . $valObj->dns_sleep; + + // Setup DNS hook: + // Set required env variables, write secrets to files, etc. + switch ((string)$valObj->dns_service) { + case 'dns_ad': + $proc_env['AD_API_KEY'] = (string)$valObj->dns_ad_key; + break; + case 'dns_ali': + $proc_env['Ali_Key'] = (string)$valObj->dns_ali_key; + $proc_env['Ali_Secret'] = (string)$valObj->dns_ali_secret; + break; + case 'dns_aws': + $proc_env['AWS_ACCESS_KEY_ID'] = (string)$valObj->dns_aws_id; + $proc_env['AWS_SECRET_ACCESS_KEY'] = (string)$valObj->dns_aws_secret; + break; + case 'dns_cf': + $proc_env['CF_Key'] = (string)$valObj->dns_cf_key; + $proc_env['CF_Email'] = (string)$valObj->dns_cf_email; + break; + case 'dns_cx': + $proc_env['CX_Key'] = (string)$valObj->dns_cx_key; + $proc_env['CX_Secret'] = (string)$valObj->dns_cx_secret; + break; + case 'dns_dp': + $proc_env['DP_Id'] = (string)$valObj->dns_dp_id; + $proc_env['DP_Key'] = (string)$valObj->dns_dp_key; + break; + case 'dns_gd': + $proc_env['GD_Key'] = (string)$valObj->dns_gd_key; + $proc_env['GD_Secret'] = (string)$valObj->dns_gd_secret; + break; + case 'dns_ispconfig': + $proc_env['ISPC_User'] = (string)$valObj->dns_ispconfig_user; + $proc_env['ISPC_Password'] = (string)$valObj->dns_ispconfig_password; + $proc_env['ISPC_Api'] = (string)$valObj->dns_ispconfig_api; + $proc_env['ISPC_Api_Insecure'] = (string)$valObj->dns_ispconfig_insecure; + break; + case 'dns_lexicon': + $proc_env['PROVIDER'] = (string)$valObj->dns_lexicon_provider; + $proc_env['LEXICON_CLOUDFLARE_USERNAME'] = (string)$valObj->dns_lexicon_user; + $proc_env['LEXICON_CLOUDFLARE_TOKEN'] = (string)$valObj->dns_lexicon_token; + $proc_env['LEXICON_NAMESILO_TOKEN'] = (string)$valObj->dns_lexicon_token; + if ((string)$valObj->dns_lexicon_provider == 'namesilo') { + // Namesilo applies changes to DNS records only every 15 minutes. + $acme_hook_options[] = "--dnssleep 960"; + } + break; + case 'dns_lua': + $proc_env['LUA_Key'] = (string)$valObj->dns_lua_key; + $proc_env['LUA_Email'] = (string)$valObj->dns_lua_email; + break; + case 'dns_me': + $proc_env['ME_Key'] = (string)$valObj->dns_me_key; + $proc_env['ME_Secret'] = (string)$valObj->dns_me_secret; + break; + case 'dns_nsupdate': + // Write secret key to filesystem + $secret_key_data = (string)$valObj->dns_nsupdate_key . "\n"; + file_put_contents($secret_key_filename, $secret_key_data); + + $proc_env['NSUPDATE_KEY'] = $secret_key_filename; + $proc_env['NSUPDATE_SERVER'] = (string)$valObj->dns_nsupdate_server; + break; + case 'dns_ovh': + $proc_env['OVH_AK'] = (string)$valObj->dns_ovh_app_key; + $proc_env['OVH_AS'] = (string)$valObj->dns_ovh_app_secret; + $proc_env['OVH_CK'] = (string)$valObj->dns_ovh_consumer_key; + $proc_env['OVH_END_POINT'] = (string)$valObj->dns_ovh_endpoint; + break; + case 'dns_pdns': + $proc_env['PDNS_Url'] = (string)$valObj->dns_pdns_url; + $proc_env['PDNS_ServerId'] = (string)$valObj->dns_pdns_serverid; + $proc_env['PDNS_Token'] = (string)$valObj->dns_pdns_token; + break; + default: + log_error("AcmeClient: invalid DNS-01 service specified: " . (string)$valObj->dns_service); + return(1); + } + } + + // Prepare altNames + $altnames = ""; + if (!empty((string)$certObj->altNames)) { + $_altnames = explode(",",(string)$certObj->altNames); + foreach (explode(",",(string)$certObj->altNames) as $altname) { + $altnames .= "--domain ${altname} "; + } + } + + // Run acme client + // NOTE: We "export" certificates to our own directory, so we don't have to deal + // with domain names in filesystem, but instead can use the ID of our certObj. + $acmecmd = "/usr/local/opnsense/scripts/OPNsense/AcmeClient/acme.sh " + . implode(" ", $acme_args) . " " + . "--${acme_action} " + . "--domain " . (string)$certObj->name . " " + . $altnames + . $acme_validation . " " + . "--log-level 2 " + . "--home /var/etc/acme-client/home " + . "--keylength 4096 " + . "--accountconf " . $account_conf_file . " " + . "--certpath ${cert_filename} " + . "--keypath ${key_filename} " + . "--capath ${cert_chain_filename} " + . "--fullchainpath ${cert_fullchain_filename} " + . implode(" ", $acme_hook_options); + //echo "DEBUG: executing command: " . $acmecmd . "\n"; + $proc = proc_open($acmecmd , $proc_desc, $proc_pipes, null, $proc_env); + + // Make sure the resource could be setup properly + if (is_resource($proc)) { + // Close all pipes + fclose($proc_pipes[0]); + fclose($proc_pipes[1]); + fclose($proc_pipes[2]); + // Get exit code + $result = proc_close($proc); + } else { + log_error("AcmeClient: unable to start acme client process"); + return(1); + } + + // HTTP-01: flush OPNsense port forward rules + if (($val_method == 'http01') and ((string)$valObj->http_service == 'opnsense')) { + mwexec('/sbin/pfctl -a acme-client -F all'); + } + + // Check validation result + if ($result) { + log_error("AcmeClient: domain validation failed"); + return(1); + } + + // Simply return acme clients exit code + return($result); +} + +// Revoke a certificate. +function revoke_cert($certObj,$valObj,$acctObj) +{ + // NOTE: Revocation will fail if additional domain names were added + // to the certificate after issue/renewal. + + // Prepare optional parameters for acme-client + $acme_args = eval_optional_acme_args(); + + // Collect account information + $account_conf_dir = "/var/etc/acme-client/accounts/" . $acctObj->id; + $account_conf_file = $account_conf_dir . "/account.conf"; + + // Generate certificate filenames + $cert_id = (string)$certObj->id; + + // Run acme client + // NOTE: We "export" certificates to our own directory, so we don't have to deal + // with domain names in filesystem, but instead can use the ID of our certObj. + $acmecmd = "/usr/local/opnsense/scripts/OPNsense/AcmeClient/acme.sh " + . implode(" ", $acme_args) . " " + . "--revoke " + . "--domain " . (string)$certObj->name . " " + . "--log-level 2 " + . "--home /var/etc/acme-client/home " + . "--keylength 4096 " + . "--accountconf " . $account_conf_file; + //echo "DEBUG: executing command: " . $acmecmd . "\n"; + $result = mwexec($acmecmd); + + // TODO: maybe clear lastUpdate value? + + // Simply return acme clients exit code + return($result); +} + +function import_certificate($certObj,$modelObj) +{ + global $config; + + $cert_id = (string)$certObj->id; + $cert_filename = "/var/etc/acme-client/certs/${cert_id}/cert.pem"; + $cert_fullchain_filename = "/var/etc/acme-client/certs/${cert_id}/fullchain.pem"; + $key_filename = "/var/etc/acme-client/keys/${cert_id}/private.key"; + + // Check if certificate files can be found + clearstatcache(); // don't let the cache fool us + foreach (Array($cert_filename, $key_filename, $cert_fullchain_filename) as $file) { + if (is_file($file)) { + // certificate file found + } else { + log_error("AcmeClient: unable to import certificate, file not found: ${file}"); + return(1); + } + } + + // Read contents from certificate file + $cert_content = @file_get_contents($cert_filename); + if ($cert_content != false) { + $cert_subject = cert_get_subject($cert_content,false); + $cert_serial = cert_get_serial($cert_content,false); + $cert_cn = local_cert_get_cn($cert_content,false); + $cert_issuer = cert_get_issuer($cert_content,false); + $cert_purpose = cert_get_purpose($cert_content,false); + //echo "DEBUG: importing cert: subject: ${cert_subject}, serial: ${cert_serial}, issuer: ${cert_issuer} \n"; + } else { + log_error("AcmeClient: unable to read certificate content from file"); + return(1); + } + + // Prepare certificate for import in Cert Manager + $cert = array(); + $cert_refid = uniqid(); + $cert['refid'] = $cert_refid; + $import_log_message = 'Imported'; + $cert_found = false; + + // Check if cert was previously imported + if (isset($certObj->certRefId)) { + // Check if the imported certificate can still be found + $configObj = Config::getInstance()->object(); + foreach ($configObj->cert as $cfgCert) { + // Check if the IDs matches + if ( (string)$certObj->certRefId == (string)$cfgCert->refid ) { + $cert_found = true; + break; + } + } + // Existing cert? + if ($cert_found == true) { + // Use old refid instead of generating a new one + $cert_refid = (string)$certObj->certRefId; + $import_log_message = 'Updated'; + //echo "DEBUG: updating EXISTING certificate\n"; + } + } else { + // Not found. Just import as new cert. + //echo "DEBUG: importing NEW certificate\n"; + } + + // Read private key + $key_content = @file_get_contents($key_filename); + if ($key_content == false) { + log_error("AcmeClient: unable to read private key from file: ${key_filename}"); + return(1); + } + + // Read cert fullchain + $cert_fullchain_content = @file_get_contents($cert_fullchain_filename); + if ($cert_fullchain_content == false) { + log_error("AcmeClient: unable to read full certificate chain from file: ${cert_fullchain_filename}"); + return(1); + } + + // Collect required cert information + $cert_cn = local_cert_get_cn($cert_content,false); + $cert['descr'] = (string)$cert_cn . ' (Let\'s Encrypt)'; + $cert['refid'] = $cert_refid; + + // Prepare certificate for import + cert_import($cert, $cert_fullchain_content, $key_content); + + // Update existing certificate? + if ($cert_found == true) { + // FIXME: Do legacy configs really depend on counters? + $cnt = 0; + foreach($config['cert'] as $crt) { + if ( $crt['refid'] == $cert_refid ) { + //echo "DEBUG: found legacy cert object\n"; + $config['cert'][$cnt] = $cert; + break; + } + $cnt++; + } + } else { + // Create new certificate item + $config['cert'][] = $cert; + } + + // Write changes to config + // TODO: Legacy code, should be replaced with code from OPNsense framework + write_config("${import_log_message} Lets Encrypt SSL certificate: ${cert_cn}"); + + // Update (acme) certificate object (through MVC framework) + $uuid = $certObj->attributes()->uuid; + $node = $modelObj->getNodeByReference('certificates.certificate.' . $uuid); + if ($node != null) { + // Add refid to certObj + $node->certRefId = $cert_refid; + // Set update/create time + $node->lastUpdate = time(); + // if node was found, serialize to config and save + $modelObj->serializeToConfig(); + Config::getInstance()->save(); + } else { + log_error("AcmeClient: unable to update LE certificate object"); + return(1); + } + + return(0); +} + +// taken from certs.inc +function local_cert_get_subject_array($str_crt, $decode = true) +{ + if ($decode) { + $str_crt = base64_decode($str_crt); + } + $inf_crt = openssl_x509_parse($str_crt); + $components = $inf_crt['subject']; + + if (!is_array($components)) { + return; + } + + $subject_array = array(); + + foreach($components as $a => $v) { + $subject_array[] = array('a' => $a, 'v' => $v); + } + + return $subject_array; +} + +// taken from certs.inc +function local_cert_get_cn($crt, $decode = true) +{ + $sub = local_cert_get_subject_array($crt,$decode); + if (is_array($sub)) { + foreach ($sub as $s) { + if (strtoupper($s['a']) == "CN") { + return $s['v']; + } + } + } + return ""; +} + +function base64url_encode($str) { + return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); +} +function base64url_decode($str) { + return base64_decode(str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT)); +} + +exit; diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ad.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ad.sh new file mode 100644 index 000000000..fc4a664be --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ad.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env sh + +# +#AD_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje" + +#This is the Alwaysdata api wrapper for acme.sh +# +#Author: Paul Koppen +#Report Bugs here: https://github.com/wpk-/acme.sh + +AD_API_URL="https://$AD_API_KEY:@api.alwaysdata.com/v1" + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ad_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$AD_API_KEY" ]; then + AD_API_KEY="" + _err "You didn't specify the AD api key yet." + _err "Please create you key and try again." + return 1 + fi + + _saveaccountconf AD_API_KEY "$AD_API_KEY" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _ad_tmpl_json="{\"domain\":$_domain_id,\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\"}" + + if _ad_rest POST "record/" "$_ad_tmpl_json" && [ -z "$response" ]; then + _info "txt record updated success." + return 0 + fi + + return 1 +} + +#fulldomain txtvalue +dns_ad_rm() { + fulldomain=$1 + txtvalue=$2 + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _ad_rest GET "record/?domain=$_domain_id&name=$_sub_domain" + + if [ -n "$response" ]; then + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug record_id "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if _ad_rest DELETE "record/$record_id/" && [ -z "$response" ]; then + _info "txt record deleted success." + return 0 + fi + _debug response "$response" + return 1 + fi + + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=12345 +_get_root() { + domain=$1 + i=2 + p=1 + + if _ad_rest GET "domain/"; then + response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + hostedzone="$(echo "$response" | _egrep_o "{.*\"name\":\s*\"$h\".*}")" + if [ "$hostedzone" ]; then + _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + fi + return 1 +} + +#method uri qstr data +_ad_rest() { + mtd="$1" + ep="$2" + data="$3" + + _debug mtd "$mtd" + _debug ep "$ep" + + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + + if [ "$mtd" != "GET" ]; then + # both POST and DELETE. + _debug data "$data" + response="$(_post "$data" "$AD_API_URL/$ep" "" "$mtd")" + else + response="$(_get "$AD_API_URL/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh new file mode 100644 index 000000000..98c56f878 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env sh + +Ali_API="https://alidns.aliyuncs.com/" + +#Ali_Key="LTqIA87hOKdjevsf5" +#Ali_Secret="0p5EYueFNq501xnCPzKNbx6K51qPH2" + +#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ali_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$Ali_Key" ] || [ -z "$Ali_Secret" ]; then + Ali_Key="" + Ali_Secret="" + _err "You don't specify aliyun api key and secret yet." + return 1 + fi + + #save the api key and secret to the account conf file. + _saveaccountconf Ali_Key "$Ali_Key" + _saveaccountconf Ali_Secret "$Ali_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + return 1 + fi + + _debug "Add record" + _add_record_query "$_domain" "$_sub_domain" "$txtvalue" && _ali_rest "Add record" +} + +dns_ali_rm() { + fulldomain=$1 + _clean +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _describe_records_query "$h" + if ! _ali_rest "Get root" "ignore"; then + return 1 + fi + + if _contains "$response" "PageNumber"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _debug _sub_domain "$_sub_domain" + _domain="$h" + _debug _domain "$_domain" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +_ali_rest() { + signature=$(printf "%s" "GET&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(_hex "$Ali_Secret&")" | _base64) + signature=$(_ali_urlencode "$signature") + url="$Ali_API?$query&Signature=$signature" + + if ! response="$(_get "$url")"; then + _err "Error <$1>" + return 1 + fi + + if [ -z "$2" ]; then + message="$(printf "%s" "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")" + if [ -n "$message" ]; then + _err "$message" + return 1 + fi + fi + + _debug2 response "$response" + return 0 +} + +_ali_urlencode() { + _str="$1" + _str_len=${#_str} + _u_i=1 + while [ "$_u_i" -le "$_str_len" ]; do + _str_c="$(printf "%s" "$_str" | cut -c "$_u_i")" + case $_str_c in [a-zA-Z0-9.~_-]) + printf "%s" "$_str_c" + ;; + *) + printf "%%%02X" "'$_str_c" + ;; + esac + _u_i="$(_math "$_u_i" + 1)" + done +} + +_ali_nonce() { + #_head_n 1 UPSERT$fulldomainTXT300\"$txtvalue\"" + + if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then + _info "txt record updated success." + return 0 + fi + + return 1 +} + +#fulldomain txtvalue +dns_aws_rm() { + fulldomain=$1 + txtvalue=$2 + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _aws_tmpl_xml="DELETE\"$txtvalue\"$fulldomain.TXT300" + + if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then + _info "txt record deleted success." + return 0 + fi + + return 1 + +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=2 + p=1 + + if aws_rest GET "2013-04-01/hostedzone"; then + _debug "response" "$response" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "$h."; then + hostedzone="$(echo "$response" | sed 's//\n&/g' | _egrep_o ".*?$h.<.Name>.*?<.HostedZone>")" + _debug hostedzone "$hostedzone" + if [ -z "$hostedzone" ]; then + _err "Error, can not get hostedzone." + return 1 + fi + _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o ".*<.Id>" | head -n 1 | _egrep_o ">.*<" | tr -d "<>") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + fi + return 1 +} + +#method uri qstr data +aws_rest() { + mtd="$1" + ep="$2" + qsr="$3" + data="$4" + + _debug mtd "$mtd" + _debug ep "$ep" + _debug qsr "$qsr" + _debug data "$data" + + CanonicalURI="/$ep" + _debug2 CanonicalURI "$CanonicalURI" + + CanonicalQueryString="$qsr" + _debug2 CanonicalQueryString "$CanonicalQueryString" + + RequestDate="$(date -u +"%Y%m%dT%H%M%SZ")" + _debug2 RequestDate "$RequestDate" + + #RequestDate="20161120T141056Z" ############## + + export _H1="x-amz-date: $RequestDate" + + aws_host="$AWS_HOST" + CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n" + SignedHeaders="host;x-amz-date" + if [ -n "$AWS_SESSION_TOKEN" ]; then + export _H2="x-amz-security-token: $AWS_SESSION_TOKEN" + CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n" + SignedHeaders="${SignedHeaders};x-amz-security-token" + fi + _debug2 CanonicalHeaders "$CanonicalHeaders" + _debug2 SignedHeaders "$SignedHeaders" + + RequestPayload="$data" + _debug2 RequestPayload "$RequestPayload" + + Hash="sha256" + + CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$(printf "%s" "$RequestPayload" | _digest "$Hash" hex)" + _debug2 CanonicalRequest "$CanonicalRequest" + + HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)" + _debug2 HashedCanonicalRequest "$HashedCanonicalRequest" + + Algorithm="AWS4-HMAC-SHA256" + _debug2 Algorithm "$Algorithm" + + RequestDateOnly="$(echo "$RequestDate" | cut -c 1-8)" + _debug2 RequestDateOnly "$RequestDateOnly" + + Region="us-east-1" + Service="route53" + + CredentialScope="$RequestDateOnly/$Region/$Service/aws4_request" + _debug2 CredentialScope "$CredentialScope" + + StringToSign="$Algorithm\n$RequestDate\n$CredentialScope\n$HashedCanonicalRequest" + + _debug2 StringToSign "$StringToSign" + + kSecret="AWS4$AWS_SECRET_ACCESS_KEY" + + #kSecret="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" ############################ + + _debug2 kSecret "$kSecret" + + kSecretH="$(_hex "$kSecret")" + _debug2 kSecretH "$kSecretH" + + kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)" + _debug2 kDateH "$kDateH" + + kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)" + _debug2 kRegionH "$kRegionH" + + kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)" + _debug2 kServiceH "$kServiceH" + + kSigningH="$(printf "aws4_request%s" | _hmac "$Hash" "$kServiceH" hex)" + _debug2 kSigningH "$kSigningH" + + signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)" + _debug2 signature "$signature" + + Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" + _debug2 Authorization "$Authorization" + + _H3="Authorization: $Authorization" + _debug _H3 "$_H3" + + url="$AWS_URL/$ep" + + if [ "$mtd" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$data" "$url")" + fi + + _ret="$?" + if [ "$_ret" = "0" ]; then + if _contains "$response" "/dev/null; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Adding record" + if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + else + _info "Updating record" + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) + _debug "record_id" "$record_id" + + _cf_rest PUT "zones/$_domain_id/dns_records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"zone_name\":\"$_domain\"}" + if [ "$?" = "0" ]; then + _info "Updated, OK" + return 0 + fi + _err "Update error" + return 1 + fi + +} + +#fulldomain txtvalue +dns_cf_rm() { + fulldomain=$1 + txtvalue=$2 + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain&content=$txtvalue" + + if ! printf "%s" "$response" | grep \"success\":true >/dev/null; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _cf_rest DELETE "zones/$_domain_id/dns_records/$record_id"; then + _err "Delete record error." + return 1 + fi + _contains "$response" '"success":true' + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _cf_rest GET "zones?name=$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_cf_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="X-Auth-Email: $CF_Email" + export _H2="X-Auth-Key: $CF_Key" + export _H3="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$CF_Api/$ep" "" "$m")" + else + response="$(_get "$CF_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh new file mode 100755 index 000000000..9c032fd7e --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env sh + +# Cloudxns.com Domain api +# +#CX_Key="1234" +# +#CX_Secret="sADDsdasdgdsf" + +CX_Api="https://www.cloudxns.net/api2" + +#REST_API +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_cx_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ]; then + CX_Key="" + CX_Secret="" + _err "You don't specify cloudxns.com api key or secret yet." + _err "Please create you key and try again." + return 1 + fi + + REST_API="$CX_Api" + + #save the api key and email to the account conf file. + _saveaccountconf CX_Key "$CX_Key" + _saveaccountconf CX_Secret "$CX_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + existing_records "$_domain" "$_sub_domain" + _debug count "$count" + if [ "$?" != "0" ]; then + _err "Error get existing records." + return 1 + fi + + if [ "$count" = "0" ]; then + add_record "$_domain" "$_sub_domain" "$txtvalue" + else + update_record "$_domain" "$_sub_domain" "$txtvalue" + fi + + if [ "$?" = "0" ]; then + return 0 + fi + return 1 +} + +#fulldomain +dns_cx_rm() { + fulldomain=$1 + REST_API="$CX_Api" + if _get_root "$fulldomain"; then + record_id="" + existing_records "$_domain" "$_sub_domain" + if ! [ "$record_id" = "" ]; then + _rest DELETE "record/$record_id/$_domain_id" "{}" + _info "Deleted record ${fulldomain}" + fi + fi +} + +#usage: root sub +#return if the sub record already exists. +#echos the existing records count. +# '0' means doesn't exist +existing_records() { + _debug "Getting txt records" + root=$1 + sub=$2 + count=0 + if ! _rest GET "record/$_domain_id?:domain_id?host_id=0&offset=0&row_num=100"; then + return 1 + fi + + seg=$(printf "%s\n" "$response" | _egrep_o '[^{]*host":"'"$_sub_domain"'"[^}]*\}') + _debug seg "$seg" + if [ -z "$seg" ]; then + return 0 + fi + + if printf "%s" "$response" | grep '"type":"TXT"' >/dev/null; then + count=1 + record_id=$(printf "%s\n" "$seg" | _egrep_o '"record_id":"[^"]*"' | cut -d : -f 2 | tr -d \" | _head_n 1) + _debug record_id "$record_id" + return 0 + fi + +} + +#add the txt record. +#usage: root sub txtvalue +add_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Adding record" + + if ! _rest POST "record" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then + return 1 + fi + + return 0 +} + +#update the txt record +#Usage: root sub txtvalue +update_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Updating record" + + if _rest PUT "record/$record_id" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + + if ! _rest GET "domain"; then + return 1 + fi + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "$h."; then + seg=$(printf "%s\n" "$response" | _egrep_o '[^{]*"'"$h"'."[^}]*}') + _debug seg "$seg" + _domain_id=$(printf "%s\n" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") + _debug _domain_id "$_domain_id" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _debug _sub_domain "$_sub_domain" + _domain="$h" + _debug _domain "$_domain" + return 0 + fi + return 1 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +#Usage: method URI data +_rest() { + m=$1 + ep="$2" + _debug ep "$ep" + url="$REST_API/$ep" + _debug url "$url" + + cdate=$(date -u "+%Y-%m-%d %H:%M:%S UTC") + _debug cdate "$cdate" + + data="$3" + _debug data "$data" + + sec="$CX_Key$url$data$cdate$CX_Secret" + _debug sec "$sec" + hmac=$(printf "%s" "$sec" | _digest md5 hex) + _debug hmac "$hmac" + + export _H1="API-KEY: $CX_Key" + export _H2="API-REQUEST-DATE: $cdate" + export _H3="API-HMAC: $hmac" + export _H4="Content-Type: application/json" + + if [ "$data" ]; then + response="$(_post "$data" "$url" "" "$m")" + else + response="$(_get "$url")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + if ! _contains "$response" '"message":"success"'; then + return 1 + fi + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_dp.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_dp.sh new file mode 100755 index 000000000..301a1f6cc --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_dp.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env sh + +# Dnspod.cn Domain api +# +#DP_Id="1234" +# +#DP_Key="sADDsdasdgdsf" + +REST_API="https://dnsapi.cn" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_dp_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$DP_Id" ] || [ -z "$DP_Key" ]; then + DP_Id="" + DP_Key="" + _err "You don't specify dnspod api key and key id yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf DP_Id "$DP_Id" + _saveaccountconf DP_Key "$DP_Key" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + existing_records "$_domain" "$_sub_domain" + _debug count "$count" + if [ "$?" != "0" ]; then + _err "Error get existing records." + return 1 + fi + + if [ "$count" = "0" ]; then + add_record "$_domain" "$_sub_domain" "$txtvalue" + else + update_record "$_domain" "$_sub_domain" "$txtvalue" + fi +} + +#fulldomain txtvalue +dns_dp_rm() { + fulldomain=$1 + txtvalue=$2 + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then + _err "Record.Lis error." + return 1 + fi + + if _contains "$response" 'No records'; then + _info "Don't need to remove." + return 0 + fi + + record_id=$(echo "$response" | _egrep_o '{[^{]*"value":"'"$txtvalue"'"' | cut -d , -f 1 | cut -d : -f 2 | tr -d \") + _debug record_id "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id." + return 1 + fi + + if ! _rest POST "Record.Remove" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then + _err "Record.Remove error." + return 1 + fi + + _contains "$response" "Action completed successful" + +} + +#usage: root sub +#return if the sub record already exists. +#echos the existing records count. +# '0' means doesn't exist +existing_records() { + _debug "Getting txt records" + root=$1 + sub=$2 + + if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&domain_id=$_domain_id&sub_domain=$_sub_domain"; then + return 1 + fi + + if _contains "$response" 'No records'; then + count=0 + return 0 + fi + + if _contains "$response" "Action completed successful"; then + count=$(printf "%s" "$response" | grep -c 'TXT' | tr -d ' ') + record_id=$(printf "%s" "$response" | grep '^' | tail -1 | cut -d '>' -f 2 | cut -d '<' -f 1) + _debug record_id "$record_id" + return 0 + else + _err "get existing records error." + return 1 + fi + + count=0 +} + +#add the txt record. +#usage: root sub txtvalue +add_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Adding record" + + if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认"; then + return 1 + fi + + if _contains "$response" "Action completed successful"; then + + return 0 + fi + + return 1 #error +} + +#update the txt record +#Usage: root sub txtvalue +update_record() { + root=$1 + sub=$2 + txtvalue=$3 + fulldomain="$sub.$root" + + _info "Updating record" + + if ! _rest POST "Record.Modify" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认&record_id=$record_id"; then + return 1 + fi + + if _contains "$response" "Action completed successful"; then + + return 0 + fi + + return 1 #error +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _rest POST "Domain.Info" "login_token=$DP_Id,$DP_Key&format=json&domain=$h"; then + return 1 + fi + + if _contains "$response" "Action completed successful"; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") + _debug _domain_id "$_domain_id" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _debug _sub_domain "$_sub_domain" + _domain="$h" + _debug _domain "$_domain" + return 0 + fi + return 1 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +#Usage: method URI data +_rest() { + m="$1" + ep="$2" + data="$3" + _debug "$ep" + url="$REST_API/$ep" + + _debug url "$url" + + if [ "$m" = "GET" ]; then + response="$(_get "$url" | tr -d '\r')" + else + _debug2 data "$data" + response="$(_post "$data" "$url" | tr -d '\r')" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh new file mode 100755 index 000000000..1abeeacf4 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env sh + +#Godaddy domain api +# +#GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#GD_Secret="asdfsdfsfsdfsdfdfsdf" + +GD_Api="https://api.godaddy.com/v1" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_gd_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$GD_Key" ] || [ -z "$GD_Secret" ]; then + GD_Key="" + GD_Secret="" + _err "You don't specify godaddy api key and secret yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf GD_Key "$GD_Key" + _saveaccountconf GD_Secret "$GD_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[{\"data\":\"$txtvalue\"}]"; then + if [ "$response" = "{}" ]; then + _info "Added, sleeping 10 seconds" + sleep 10 + #todo: check if the record takes effect + return 0 + else + _err "Add txt record error." + _err "$response" + return 1 + fi + fi + _err "Add txt record error." + +} + +#fulldomain +dns_gd_rm() { + fulldomain=$1 + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _gd_rest GET "domains/$h"; then + return 1 + fi + + if _contains "$response" '"code":"NOT_FOUND"'; then + _debug "$h not found" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +_gd_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Authorization: sso-key $GD_Key:$GD_Secret" + export _H2="Content-Type: application/json" + + if [ "$data" ]; then + _debug data "$data" + response="$(_post "$data" "$GD_Api/$ep" "" "$m")" + else + response="$(_get "$GD_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ispconfig.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ispconfig.sh new file mode 100755 index 000000000..6d1f34c59 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ispconfig.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env sh + +# ISPConfig 3.1 API +# User must provide login data and URL to the ISPConfig installation incl. port. The remote user in ISPConfig must have access to: +# - DNS zone Functions +# - DNS txt Functions + +# Report bugs to https://github.com/sjau/acme.sh + +# Values to export: +# export ISPC_User="remoteUser" +# export ISPC_Password="remotePassword" +# export ISPC_Api="https://ispc.domain.tld:8080/remote/json.php" +# export ISPC_Api_Insecure=1 # Set 1 for insecure and 0 for secure -> difference is whether ssl cert is checked for validity (0) or whether it is just accepted (1) + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ispconfig_add() { + fulldomain="${1}" + txtvalue="${2}" + _debug "Calling: dns_ispconfig_add() '${fulldomain}' '${txtvalue}'" + _ISPC_credentials && _ISPC_login && _ISPC_getZoneInfo && _ISPC_addTxt +} + +#Usage: dns_myapi_rm _acme-challenge.www.domain.com +dns_ispconfig_rm() { + fulldomain="${1}" + _debug "Calling: dns_ispconfig_rm() '${fulldomain}'" + _ISPC_credentials && _ISPC_login && _ISPC_rmTxt +} + +#################### Private functions below ################################## + +_ISPC_credentials() { + if [ -z "${ISPC_User}" ] || [ -z "$ISPC_Password" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then + ISPC_User="" + ISPC_Password="" + ISPC_Api="" + ISPC_Api_Insecure="" + _err "You haven't specified the ISPConfig Login data, URL and whether you want check the ISPC SSL cert. Please try again." + return 1 + else + _saveaccountconf ISPC_User "${ISPC_User}" + _saveaccountconf ISPC_Password "${ISPC_Password}" + _saveaccountconf ISPC_Api "${ISPC_Api}" + _saveaccountconf ISPC_Api_Insecure "${ISPC_Api_Insecure}" + # Set whether curl should use secure or insecure mode + export HTTPS_INSECURE="${ISPC_Api_Insecure}" + fi +} + +_ISPC_login() { + _info "Getting Session ID" + curData="{\"username\":\"${ISPC_User}\",\"password\":\"${ISPC_Password}\",\"client_login\":false}" + curResult="$(_post "${curData}" "${ISPC_Api}?login")" + _debug "Calling _ISPC_login: '${curData}' '${ISPC_Api}?login'" + _debug "Result of _ISPC_login: '$curResult'" + if _contains "${curResult}" '"code":"ok"'; then + sessionID=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _info "Retrieved Session ID." + _debug "Session ID: '${sessionID}'" + else + _err "Couldn't retrieve the Session ID." + return 1 + fi +} + +_ISPC_getZoneInfo() { + _info "Getting Zoneinfo" + zoneEnd=false + curZone="${fulldomain}" + while [ "${zoneEnd}" = false ]; do + # we can strip the first part of the fulldomain, since it's just the _acme-challenge string + curZone="${curZone#*.}" + # suffix . needed for zone -> domain.tld. + curData="{\"session_id\":\"${sessionID}\",\"primary_id\":{\"origin\":\"${curZone}.\"}}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_zone_get")" + _debug "Calling _ISPC_getZoneInfo: '${curData}' '${ISPC_Api}?login'" + _debug "Result of _ISPC_getZoneInfo: '$curResult'" + if _contains "${curResult}" '"id":"'; then + zoneFound=true + zoneEnd=true + _info "Retrieved zone data." + _debug "Zone data: '${curResult}'" + fi + if [ "${curZone#*.}" != "$curZone" ]; then + _debug2 "$curZone still contains a '.' - so we can check next higher level" + else + zoneEnd=true + _err "Couldn't retrieve zone data." + return 1 + fi + done + if [ "${zoneFound}" ]; then + server_id=$(echo "${curResult}" | _egrep_o "server_id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Server ID: '${server_id}'" + case "${server_id}" in + '' | *[!0-9]*) + _err "Server ID is not numeric." + return 1 + ;; + *) _info "Retrieved Server ID" ;; + esac + zone=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Zone: '${zone}'" + case "${zone}" in + '' | *[!0-9]*) + _err "Zone ID is not numeric." + return 1 + ;; + *) _info "Retrieved Zone ID" ;; + esac + client_id=$(echo "${curResult}" | _egrep_o "sys_userid.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Client ID: '${client_id}'" + case "${client_id}" in + '' | *[!0-9]*) + _err "Client ID is not numeric." + return 1 + ;; + *) _info "Retrieved Client ID." ;; + esac + zoneFound="" + zoneEnd="" + fi +} + +_ISPC_addTxt() { + curSerial="$(date +%s)" + curStamp="$(date +'%F %T')" + params="\"server_id\":\"${server_id}\",\"zone\":\"${zone}\",\"name\":\"${fulldomain}.\",\"type\":\"txt\",\"data\":\"${txtvalue}\",\"aux\":\"0\",\"ttl\":\"3600\",\"active\":\"y\",\"stamp\":\"${curStamp}\",\"serial\":\"${curSerial}\"" + curData="{\"session_id\":\"${sessionID}\",\"client_id\":\"${client_id}\",\"params\":{${params}}}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_add")" + _debug "Calling _ISPC_addTxt: '${curData}' '${ISPC_Api}?dns_txt_add'" + _debug "Result of _ISPC_addTxt: '$curResult'" + record_id=$(echo "${curResult}" | _egrep_o "\"response.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Record ID: '${record_id}'" + case "${record_id}" in + '' | *[!0-9]*) + _err "Couldn't add ACME Challenge TXT record to zone." + return 1 + ;; + *) _info "Added ACME Challenge TXT record to zone." ;; + esac +} + +_ISPC_rmTxt() { + # Need to get the record ID. + curData="{\"session_id\":\"${sessionID}\",\"primary_id\":{\"name\":\"${fulldomain}.\",\"type\":\"TXT\"}}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_get")" + _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_get'" + _debug "Result of _ISPC_rmTxt: '$curResult'" + if _contains "${curResult}" '"code":"ok"'; then + record_id=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "Record ID: '${record_id}'" + case "${record_id}" in + '' | *[!0-9]*) + _err "Record ID is not numeric." + return 1 + ;; + *) + unset IFS + _info "Retrieved Record ID." + curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\"}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_delete")" + _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_delete'" + _debug "Result of _ISPC_rmTxt: '$curResult'" + if _contains "${curResult}" '"code":"ok"'; then + _info "Removed ACME Challenge TXT record from zone." + else + _err "Couldn't remove ACME Challenge TXT record from zone." + return 1 + fi + ;; + esac + fi +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh new file mode 100755 index 000000000..c38ff3e38 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env sh + +# dns api wrapper of lexicon for acme.sh + +# https://github.com/AnalogJ/lexicon +lexicon_cmd="lexicon" + +wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_lexicon_add() { + fulldomain=$1 + txtvalue=$2 + + domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999) + + if ! _exists "$lexicon_cmd"; then + _err "Please install $lexicon_cmd first: $wiki" + return 1 + fi + + if [ -z "$PROVIDER" ]; then + PROVIDER="" + _err "Please define env PROVIDER first: $wiki" + return 1 + fi + + _savedomainconf PROVIDER "$PROVIDER" + export PROVIDER + + # e.g. busybox-ash does not know [:upper:] + # shellcheck disable=SC2018,SC2019 + Lx_name=$(echo LEXICON_"${PROVIDER}"_USERNAME | tr 'a-z' 'A-Z') + Lx_name_v=$(eval echo \$"$Lx_name") + _debug "$Lx_name" "$Lx_name_v" + if [ "$Lx_name_v" ]; then + _saveaccountconf "$Lx_name" "$Lx_name_v" + eval export "$Lx_name" + fi + + # shellcheck disable=SC2018,SC2019 + Lx_token=$(echo LEXICON_"${PROVIDER}"_TOKEN | tr 'a-z' 'A-Z') + Lx_token_v=$(eval echo \$"$Lx_token") + _debug "$Lx_token" "$Lx_token_v" + if [ "$Lx_token_v" ]; then + _saveaccountconf "$Lx_token" "$Lx_token_v" + eval export "$Lx_token" + fi + + # shellcheck disable=SC2018,SC2019 + Lx_password=$(echo LEXICON_"${PROVIDER}"_PASSWORD | tr 'a-z' 'A-Z') + Lx_password_v=$(eval echo \$"$Lx_password") + _debug "$Lx_password" "$Lx_password_v" + if [ "$Lx_password_v" ]; then + _saveaccountconf "$Lx_password" "$Lx_password_v" + eval export "$Lx_password" + fi + + # shellcheck disable=SC2018,SC2019 + Lx_domaintoken=$(echo LEXICON_"${PROVIDER}"_DOMAINTOKEN | tr 'a-z' 'A-Z') + Lx_domaintoken_v=$(eval echo \$"$Lx_domaintoken") + _debug "$Lx_domaintoken" "$Lx_domaintoken_v" + if [ "$Lx_domaintoken_v" ]; then + eval export "$Lx_domaintoken" + _saveaccountconf "$Lx_domaintoken" "$Lx_domaintoken_v" + fi + + $lexicon_cmd "$PROVIDER" create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" + +} + +#fulldomain +dns_lexicon_rm() { + fulldomain=$1 + +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh new file mode 100755 index 000000000..47f4497a6 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env sh + +# bug reports to dev@1e.ca + +# +#LUA_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#LUA_Email="user@luadns.net" + +LUA_Api="https://api.luadns.com/v1" +LUA_auth=$(printf "%s" "$LUA_Email:$LUA_Key" | _base64) + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_lua_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$LUA_Key" ] || [ -z "$LUA_Email" ]; then + LUA_Key="" + LUA_Email="" + _err "You don't specify luadns api key and email yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf LUA_Key "$LUA_Key" + _saveaccountconf LUA_Email "$LUA_Email" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _LUA_rest GET "zones/${_domain_id}/records" + + if ! _contains "$response" "\"id\":"; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain\"" | wc -l) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Adding record" + if _LUA_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then + _info "Added" + #todo: check if the record takes effect + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + else + _info "Updating record" + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | cut -d: -f2 | cut -d, -f1) + _debug "record_id" "$record_id" + + _LUA_rest PUT "zones/$_domain_id/records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"ttl\":120}" + if [ "$?" = "0" ]; then + _info "Updated!" + #todo: check if the record takes effect + return 0 + fi + _err "Update error" + return 1 + fi + +} + +#fulldomain +dns_lua_rm() { + fulldomain=$1 + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + if ! _LUA_rest GET "zones"; then + return 1 + fi + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$h\"" | cut -d : -f 2 | cut -d , -f 1) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_LUA_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Accept: application/json" + export _H2="Authorization: Basic $LUA_auth" + if [ "$data" ]; then + _debug data "$data" + response="$(_post "$data" "$LUA_Api/$ep" "" "$m")" + else + response="$(_get "$LUA_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh new file mode 100755 index 000000000..9fe6baf86 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env sh + +# bug reports to dev@1e.ca + +# ME_Key=qmlkdjflmkqdjf +# ME_Secret=qmsdlkqmlksdvnnpae + +ME_Api=https://api.dnsmadeeasy.com/V2.0/dns/managed + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_me_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$ME_Key" ] || [ -z "$ME_Secret" ]; then + ME_Key="" + ME_Secret="" + _err "You didn't specify DNSMadeEasy api key and secret yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf ME_Key "$ME_Key" + _saveaccountconf ME_Secret "$ME_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _me_rest GET "${_domain_id}/records?recordName=$_sub_domain&type=TXT" + + if ! _contains "$response" "\"totalRecords\":"; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"totalRecords\":[^,]*" | cut -d : -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Adding record" + if _me_rest POST "$_domain_id/records/" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"gtdLocation\":\"DEFAULT\",\"ttl\":120}"; then + if printf -- "%s" "$response" | grep \"id\": >/dev/null; then + _info "Added" + #todo: check if the record takes effect + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + else + _info "Updating record" + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | cut -d : -f 2 | head -n 1) + _debug "record_id" "$record_id" + + _me_rest PUT "$_domain_id/records/$record_id/" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"gtdLocation\":\"DEFAULT\",\"ttl\":120}" + if [ "$?" = "0" ]; then + _info "Updated" + #todo: check if the record takes effect + return 0 + fi + _err "Update error" + return 1 + fi + +} + +#fulldomain +dns_me_rm() { + fulldomain=$1 + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _me_rest GET "name?domainname=$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | head -n 1 | cut -d : -f 2 | tr -d '}') + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_me_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + cdate=$(date -u +"%a, %d %b %Y %T %Z") + hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(_hex "$ME_Secret")" hex) + + export _H1="x-dnsme-apiKey: $ME_Key" + export _H2="x-dnsme-requestDate: $cdate" + export _H3="x-dnsme-hmac: $hmac" + + if [ "$data" ]; then + _debug data "$data" + response="$(_post "$data" "$ME_Api/$ep" "" "$m")" + else + response="$(_get "$ME_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_myapi.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_myapi.sh new file mode 100755 index 000000000..6bf625081 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_myapi.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh + +#Here is a sample custom api script. +#This file name is "dns_myapi.sh" +#So, here must be a method dns_myapi_add() +#Which will be called by acme.sh to add the txt record to your api system. +#returns 0 means success, otherwise error. +# +#Author: Neilpang +#Report Bugs here: https://github.com/Neilpang/acme.sh +# +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_myapi_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using myapi" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + _err "Not implemented!" + return 1 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_myapi_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using myapi" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" +} + +#################### Private functions below ################################## diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_nsupdate.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_nsupdate.sh new file mode 100755 index 000000000..7acb2ef77 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_nsupdate.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env sh + +######## Public functions ##################### + +#Usage: dns_nsupdate_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_nsupdate_add() { + fulldomain=$1 + txtvalue=$2 + _checkKeyFile || return 1 + [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" + # save the dns server and key to the account conf file. + _saveaccountconf NSUPDATE_SERVER "${NSUPDATE_SERVER}" + _saveaccountconf NSUPDATE_KEY "${NSUPDATE_KEY}" + _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" + nsupdate -k "${NSUPDATE_KEY}" </dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_ovh_timestamp() { + _H1="" + _H2="" + _H3="" + _H4="" + _H5="" + _get "$OVH_API/auth/time" "" 30 +} + +_ovh_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + _ovh_url="$OVH_API/$ep" + _debug2 _ovh_url "$_ovh_url" + _ovh_t="$(_ovh_timestamp)" + _debug2 _ovh_t "$_ovh_t" + _ovh_p="$OVH_AS+$OVH_CK+$m+$_ovh_url+$data+$_ovh_t" + _debug _ovh_p "$_ovh_p" + _ovh_hex="$(printf "%s" "$_ovh_p" | _digest sha1 hex)" + _debug2 _ovh_hex "$_ovh_hex" + + export _H1="X-Ovh-Application: $OVH_AK" + export _H2="X-Ovh-Signature: \$1\$$_ovh_hex" + _debug2 _H2 "$_H2" + export _H3="X-Ovh-Timestamp: $_ovh_t" + export _H4="X-Ovh-Consumer: $OVH_CK" + export _H5="Content-Type: application/json;charset=utf-8" + if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ]; then + _debug data "$data" + response="$(_post "$data" "$_ovh_url" "" "$m")" + else + response="$(_get "$_ovh_url")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_pdns.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_pdns.sh new file mode 100755 index 000000000..ebc029490 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_pdns.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env sh + +#PowerDNS Emdedded API +#https://doc.powerdns.com/md/httpapi/api_spec/ +# +#PDNS_Url="http://ns.example.com:8081" +#PDNS_ServerId="localhost" +#PDNS_Token="0123456789ABCDEF" +#PDNS_Ttl=60 + +DEFAULT_PDNS_TTL=60 + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "123456789ABCDEF0000000000000000000000000000000000000" +#fulldomain +#txtvalue +dns_pdns_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$PDNS_Url" ]; then + PDNS_Url="" + _err "You don't specify PowerDNS address." + _err "Please set PDNS_Url and try again." + return 1 + fi + + if [ -z "$PDNS_ServerId" ]; then + PDNS_ServerId="" + _err "You don't specify PowerDNS server id." + _err "Please set you PDNS_ServerId and try again." + return 1 + fi + + if [ -z "$PDNS_Token" ]; then + PDNS_Token="" + _err "You don't specify PowerDNS token." + _err "Please create you PDNS_Token and try again." + return 1 + fi + + if [ -z "$PDNS_Ttl" ]; then + PDNS_Ttl="$DEFAULT_PDNS_TTL" + fi + + #save the api addr and key to the account conf file. + _saveaccountconf PDNS_Url "$PDNS_Url" + _saveaccountconf PDNS_ServerId "$PDNS_ServerId" + _saveaccountconf PDNS_Token "$PDNS_Token" + + if [ "$PDNS_Ttl" != "$DEFAULT_PDNS_TTL" ]; then + _saveaccountconf PDNS_Ttl "$PDNS_Ttl" + fi + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain "$_domain" + + if ! set_record "$_domain" "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +#fulldomain +dns_pdns_rm() { + fulldomain=$1 + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain "$_domain" + + if ! rm_record "$_domain" "$fulldomain"; then + return 1 + fi + + return 0 +} + +set_record() { + _info "Adding record" + root=$1 + full=$2 + txtvalue=$3 + + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [{\"name\": \"$full.\", \"type\": \"TXT\", \"content\": \"\\\"$txtvalue\\\"\", \"disabled\": false, \"ttl\": $PDNS_Ttl}]}]}"; then + _err "Set txt record error." + return 1 + fi + + if ! notify_slaves "$root"; then + return 1 + fi + + return 0 +} + +rm_record() { + _info "Remove record" + root=$1 + full=$2 + + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then + _err "Delete txt record error." + return 1 + fi + + if ! notify_slaves "$root"; then + return 1 + fi + + return 0 +} + +notify_slaves() { + root=$1 + + if ! _pdns_rest "PUT" "/api/v1/servers/$PDNS_ServerId/zones/$root./notify"; then + _err "Notify slaves error." + return 1 + fi + + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + + if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then + _zones_response="$response" + fi + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + return 1 + fi + + if _contains "$_zones_response" "\"name\": \"$h.\""; then + _domain="$h" + return 0 + fi + + i=$(_math $i + 1) + done + _debug "$domain not found" + + return 1 +} + +_pdns_rest() { + method=$1 + ep=$2 + data=$3 + + export _H1="X-API-Key: $PDNS_Token" + + if [ ! "$method" = "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$PDNS_Url$ep" "" "$method")" + else + response="$(_get "$PDNS_Url$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh new file mode 100755 index 000000000..0dce05038 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +ACME_DIRS="/var/etc/acme-client /var/etc/acme-client/certs /var/etc/acme-client/keys /var/etc/acme-client/configs /var/etc/acme-client/challenges /var/etc/acme-client/home" + +for directory in ${ACME_DIRS}; do + mkdir -p ${directory} + chown -R root:wheel ${directory} + chmod -R 755 ${directory} +done + +# XXX: fix file permissions of rc script (limitation of +TARGETS mechanism) +chmod 755 /usr/local/etc/rc.d/acme_http_challenge + +exit 0 diff --git a/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf new file mode 100644 index 000000000..1af954601 --- /dev/null +++ b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf @@ -0,0 +1,67 @@ +[setup] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh +parameters: +type:script_output + +########################################## +## lighttpd actions +########################################## + +[http-start] +command:/usr/local/etc/rc.d/acme_http_challenge start +parameters: +type:script +message:starting acme_http_challenge + +[http-stop] +command:/usr/local/etc/rc.d/acme_http_challenge stop; exit 0 +parameters: +type:script +message:stopping acme_http_challenge + +[http-restart] +command:/usr/local/etc/rc.d/acme_http_challenge restart +parameters: +type:script +message:restarting acme_http_challenge + +[http-status] +command:/usr/local/etc/rc.d/acme_http_challenge status || exit 0 +parameters: +type:script_output +message:requesting acme_http_challenge status + +[http-configtest] +command:/usr/local/etc/rc.d/acme_http_challenge configtest 2>&1 || exit 0 +parameters: +type:script_output +message:testing acme_http_challenge configuration + +########################################## +## certificate actions +########################################## + +[sign-cert] +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -F -a sign -c +parameters:%s +type:script +message:signing or renewing a certificate + +[revoke-cert] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a revoke -c +parameters:%s +type:script +message:revoking a certificate + +[sign-all-certs] +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a sign -A +parameters: +type:script +message:signing or renewing a certificate + +[cron-auto-renew] +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/certhelper.php -a sign -A -C +parameters: +type:script +message:cronjob running to sign or renew certificates +description:Renew Let's Encrypt certificates diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS new file mode 100644 index 000000000..91b1e9bdd --- /dev/null +++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS @@ -0,0 +1,3 @@ +lighttpd-acme-challenge.conf:/var/etc/lighttpd-acme-challenge.conf +rc.conf.d:/etc/rc.conf.d/acme_http_challenge +lighttpd-rc-script:/usr/local/etc/rc.d/acme_http_challenge diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf new file mode 100644 index 000000000..5215bcbae --- /dev/null +++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf @@ -0,0 +1,86 @@ +# +# Automatically generated configuration. +# Do not edit this file manually. + +# FreeBSD! +server.event-handler = "freebsd-kqueue" +server.network-backend = "writev" +#server.use-ipv6 = "enable" + +# modules to load +server.modules = ( "mod_access", "mod_expire", "mod_compress", "mod_redirect", + "mod_alias", "mod_rewrite" +) + +server.max-keep-alive-requests = 2 +server.max-keep-alive-idle = 30 + +# a static document-root, for virtual-hosting take look at the +# server.virtual-* options +server.document-root = "/var/empty" + +# Let's Encrypt acme challenges +alias.url += ( "/.well-known/acme-challenge/" => "/var/etc/acme-client/challenges/.well-known/acme-challenge/" ) + +# Maximum idle time with nothing being written +server.max-write-idle = 90 + +# where to send error-messages to +server.errorlog-use-syslog = "enable" + +# files to check for if .../ is requested +server.indexfiles = ( "index.html" ) + +# mimetype mapping +mimetype.assign = ( + ".gz" => "application/x-gzip", + ".tar.gz" => "application/x-tgz", + ".tgz" => "application/x-tgz", + ".tar" => "application/x-tar", + ".zip" => "application/zip", + ".css" => "text/css", + ".html" => "text/html", + ".htm" => "text/html", + ".js" => "text/javascript", + ".asc" => "text/plain", + ".c" => "text/plain", + ".conf" => "text/plain", + ".text" => "text/plain", + ".txt" => "text/plain", + ".dtd" => "text/xml", + ".xml" => "text/xml", + ".bz2" => "application/x-bzip", + ".tbz" => "application/x-bzip-compressed-tar", + ".tar.bz2" => "application/x-bzip-compressed-tar" + ) + +# deny access to file-extensions +url.access-deny = ( "~", ".inc" ) + +# bind to port +server.bind = "127.0.0.1" +server.port = {{OPNsense.AcmeClient.settings.challengePort}} +$SERVER["socket"] == "127.0.0.1:{{OPNsense.AcmeClient.settings.challengePort}}" { } + +# ssl configuration +ssl.engine = "disable" + +# to help the rc.scripts +server.pid-file = "/var/run/lighttpd-acme-challenge.pid" + +# virtual directory listings +server.dir-listing = "disable" + +# enable debugging +debug.log-request-header = "disable" +debug.log-response-header = "disable" +debug.log-request-handling = "disable" +debug.log-file-not-found = "disable" + +# gzip compression +compress.cache-dir = "/tmp/acmelighttpdcompress/" +compress.filetype = ("text/plain","text/css", "text/xml", "text/javascript" ) + +server.max-request-size = 4096 + +expire.url = ( "" => "access 10 hours" ) diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-rc-script b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-rc-script new file mode 100755 index 000000000..4eb01f559 --- /dev/null +++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-rc-script @@ -0,0 +1,67 @@ +#!/bin/sh +# +# $FreeBSD$ +# +# PROVIDE: acme_http_challenge +# REQUIRE: DAEMON +# KEYWORD: shutdown +# +# Add the following lines to /etc/rc.conf to enable acme_http_challenge: +# +# acme_http_challenge_enable (bool): Set it to "YES" to enable acme_http_challenge +# Default is "NO". +# acme_http_challenge_conf (path): Set full path to configuration file. +# Default is "/var/etc/lighttpd-acme-challenge.conf". +# acme_http_challenge_pidfile (path): Set full path to pid file. +# Default is "/var/run/lighttpd-acme-challenge.pid". +# + +. /etc/rc.subr + +name="acme_http_challenge" +rcvar=acme_http_challenge_enable + +load_rc_config $name + +: ${acme_http_challenge_enable="NO"} +: ${acme_http_challenge_conf="/var/etc/lighttpd-acme-challenge.conf"} +: ${acme_http_challenge_pidfile="/var/run/lighttpd-acme-challenge.pid"} + +command=/usr/local/sbin/lighttpd +stop_postcmd=stop_postcmd +restart_precmd="acme_http_challenge_checkconfig" +graceful_precmd="acme_http_challenge_checkconfig" +graceful_cmd="acme_http_challenge_graceful" +gracefulstop_cmd="acme_http_challenge_gracefulstop" +configtest_cmd="acme_http_challenge_checkconfig" +extra_commands="reload graceful gracefulstop configtest" +command_args="-f ${acme_http_challenge_conf}" +pidfile=${acme_http_challenge_pidfile} +required_files=${acme_http_challenge_conf} + +acme_http_challenge_checkconfig() +{ + echo "Performing sanity check on ${name} configuration:" + eval "${command} ${command_args} -t" +} + +acme_http_challenge_gracefulstop() +{ + echo "Stopping ${name} gracefully." + sig_reload="INT" + run_rc_command reload +} + +acme_http_challenge_graceful() +{ + acme_http_challenge_gracefulstop + rm -f ${pidfile} + run_rc_command start +} + +stop_postcmd() +{ + rm -f ${pidfile} +} + +run_rc_command "$1" diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d new file mode 100644 index 000000000..7ad7a1f13 --- /dev/null +++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d @@ -0,0 +1,7 @@ +{% if helpers.exists('OPNsense.AcmeClient.settings.enabled') and OPNsense.AcmeClient.settings.enabled|default("0") == "1" %} +acme_http_challenge_enable=YES +acme_http_challenge_conf="/var/etc/lighttpd-acme-challenge.conf" +acme_http_challenge_pidfile="/var/run/lighttpd-acme-challenge.pid" +{% else %} +acme_http_challenge_enable=NO +{% endif %} diff --git a/security/acme-client/src/www/diag_logs_acmeclient.php b/security/acme-client/src/www/diag_logs_acmeclient.php new file mode 100644 index 000000000..f59a4dab0 --- /dev/null +++ b/security/acme-client/src/www/diag_logs_acmeclient.php @@ -0,0 +1,6 @@ +