security/acme-client: add acme.sh to plugins, closes #6

This commit is contained in:
Frank Wall 2017-01-05 22:18:12 +01:00 committed by Franco Fichtner
parent 928228a860
commit dd4853d09f
47 changed files with 11147 additions and 0 deletions

View file

@ -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"

View file

@ -0,0 +1,95 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
*
* 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.
*
*/
function acmeclient_enabled()
{
global $config;
if (isset($config['OPNsense']['AcmeClient']['general']['enabled']) && $config['OPNsense']['AcmeClient']['general']['enabled'] == 1) {
return true;
}
return false;
}
function acmeclient_firewall($fw)
{
if (!acmeclient_enabled()) {
return;
}
// TODO
$fw->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);
}
*/

View file

@ -0,0 +1,46 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient;
/**
* Class AccountsController
* @package OPNsense\AcmeClient
*/
class AccountsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->title = "Let's Encrypt Accounts";
// include form definitions
$this->view->formDialogAccount = $this->getForm("dialogAccount");
// choose template
$this->view->pick('OPNsense/AcmeClient/accounts');
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\AcmeClient\AcmeClient;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class AccountsController
* @package OPNsense\AcmeClient
*/
class AccountsController extends ApiControllerBase
{
/**
* Validate and save model after update or insertion.
* Use the reference node and tag to rename validation output for a specific
* node to a new offset, which makes it easier to reference specific uuids
* without having to use them in the frontend descriptions.
* @param $mdl model reference
* @param $node reference node, to use as relative offset
* @param $reference reference for validation output, used to rename the validation output keys
* @return array result / validation output
*/
private function save($mdl, $node = null, $reference = null)
{
$result = array("result"=>"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"
);
}
}

View file

@ -0,0 +1,259 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\AcmeClient\AcmeClient;
use \OPNsense\Core\Backend;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class CertificatesController
* @package OPNsense\AcmeClient
*/
class CertificatesController extends ApiControllerBase
{
/**
* Validate and save model after update or insertion.
* Use the reference node and tag to rename validation output for a specific
* node to a new offset, which makes it easier to reference specific uuids
* without having to use them in the frontend descriptions.
* @param $mdl model reference
* @param $node reference node, to use as relative offset
* @param $reference reference for validation output, used to rename the validation output keys
* @return array result / validation output
*/
private function save($mdl, $node = null, $reference = null)
{
$result = array("result"=>"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;
}
}

View file

@ -0,0 +1,191 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\Core\Backend;
use \OPNsense\Core\Config;
use \OPNsense\Cron\Cron;
use \OPNsense\AcmeClient\AcmeClient;
/**
* Class ServiceController
* @package OPNsense\AcmeClient
*/
class ServiceController extends ApiControllerBase
{
/**
* start acmeclient service (in background)
* @return array
*/
public function startAction()
{
if ($this->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);
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient\Api;
use \OPNsense\Base\ApiMutableModelControllerBase;
use \OPNsense\Core\Backend;
use \OPNsense\Cron\Cron;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class SettingsController
* @package OPNsense\AcmeClient
*/
class SettingsController extends ApiMutableModelControllerBase
{
static protected $internalModelName = 'acmeclient';
static protected $internalModelClass = '\OPNsense\AcmeClient\AcmeClient';
/**
* create new cron job or return already available one
* @return array status action
*/
public function fetchRBCronAction()
{
$result = array("result" => "no change");
// TODO: How to force the system to write-out the cronjob?
if ($this->request->isPost()) {
$mdlAcme = $this->getModel();
$backend = new Backend();
// 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;
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient\Api;
use \OPNsense\Base\ApiControllerBase;
use \OPNsense\AcmeClient\AcmeClient;
use \OPNsense\Core\Config;
use \OPNsense\Base\UIModelGrid;
/**
* Class ValidationsController
* @package OPNsense\AcmeClient
*/
class ValidationsController extends ApiControllerBase
{
/**
* Validate and save model after update or insertion.
* Use the reference node and tag to rename validation output for a specific
* node to a new offset, which makes it easier to reference specific uuids
* without having to use them in the frontend descriptions.
* @param $mdl model reference
* @param $node reference node, to use as relative offset
* @param $reference reference for validation output, used to rename the validation output keys
* @return array result / validation output
*/
private function save($mdl, $node = null, $reference = null)
{
$result = array("result"=>"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"
);
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient;
/**
* Class CertificatesController
* @package OPNsense\AcmeClient
*/
class CertificatesController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->title = "Let's Encrypt Certificates";
// include form definitions
$this->view->formDialogCertificate = $this->getForm("dialogCertificate");
// choose template
$this->view->pick('OPNsense/AcmeClient/certificates');
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient;
/**
* Class IndexController
* @package OPNsense\AcmeClient
*/
class IndexController extends \OPNsense\Base\IndexController
{
/**
* acme-client index page
* @throws \Exception
*/
public function indexAction()
{
// set page title
$this->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');
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient;
/**
* Class ValidationsController
* @package OPNsense\AcmeClient
*/
class ValidationsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->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');
}
}

View file

@ -0,0 +1,33 @@
<form>
<field>
<id>account.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this account</help>
</field>
<field>
<id>account.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this account.</help>
</field>
<field>
<id>account.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this account.</help>
</field>
<field>
<id>account.email</id>
<label>E-Mail Address</label>
<type>text</type>
<help>Optional e-mail address for this account.</help>
</field>
<field>
<id>account.certificateAuthority</id>
<label>Certificate Authority</label>
<type>dropdown</type>
<help><![CDATA[Select the certificate authority for this account.]]></help>
<advanced>true</advanced>
</field>
</form>

View file

@ -0,0 +1,53 @@
<form>
<field>
<id>certificate.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this certificate</help>
</field>
<field>
<id>certificate.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this certificate.</help>
</field>
<field>
<id>certificate.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this certificate.</help>
</field>
<field>
<id>certificate.altNames</id>
<label>Alt Names</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[Configure additional names that should be part pf the certificate, i.e. www.example.com or mail.example.com. Use TAB key to complete typing a FQDN.<br/><div class="text-info"><b>NOTE:</b>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.</div>]]></help>
<hint>Enter FQDN here. Finish with TAB.</hint>
</field>
<field>
<id>certificate.account</id>
<label>CA Account</label>
<type>dropdown</type>
<help><![CDATA[Set the CA account to use for this certificate.]]></help>
</field>
<field>
<id>certificate.validationMethod</id>
<label>Validation Method</label>
<type>dropdown</type>
<help><![CDATA[Set the Let's Encrypt validation method for this certificate.]]></help>
</field>
<field>
<id>certificate.autoRenewal</id>
<label>Auto Renewal</label>
<type>checkbox</type>
<help>Enable automatic renewal for this certificate to prevent expiration.</help>
</field>
<field>
<id>certificate.renewInterval</id>
<label>Renewal Interval</label>
<type>text</type>
<help><![CDATA[Specifies the days to renew the cert. The max value is 60 days.]]></help>
</field>
</form>

View file

@ -0,0 +1,357 @@
<form>
<field>
<id>validation.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this validation</help>
</field>
<field>
<id>validation.name</id>
<label>Name</label>
<type>text</type>
<help>Name to identify this validation.</help>
</field>
<field>
<id>validation.description</id>
<label>Description</label>
<type>text</type>
<help>Description for this validation.</help>
</field>
<field>
<id>validation.method</id>
<label>Validation Method</label>
<type>dropdown</type>
<help><![CDATA[Set the Let's Encrypt validation method.]]></help>
</field>
<field>
<label>HTTP-01</label>
<type>header</type>
</field>
<field>
<id>validation.http_service</id>
<label>HTTP Service</label>
<type>dropdown</type>
<help></help>
</field>
<field>
<label>HTTP-01/OPNsense</label>
<type>header</type>
</field>
<field>
<id>validation.http_opn_autodiscovery</id>
<label>IP Auto-Discovery</label>
<type>checkbox</type>
<help><![CDATA[The FQDN's used in your certificate must currently point to an official IP address. Choose this option to let OPNsense tryo to auto-discover these IP addresses. This will lead to a short downtime of the service that is normally used with this IP address.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
</field>
<field>
<id>validation.http_opn_interface</id>
<label>Interface</label>
<type>dropdown</type>
<help><![CDATA[The FQDN's used in your certificate must currently point to an official IP address. Choose the interface where this IP address is currently configured. OPNsense will automatically create a temporary port forward to allow the Let's Encrypt validation to succeed. This will lead to a short downtime of the service that is normally used with this IP address.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
</field>
<field>
<id>validation.http_opn_ipaddresses</id>
<label>IP Addresses</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[The FQDN's used in your certificate must currently point to one or more official IP addresses. Enter the all of these IP addresses here. OPNsense will automatically create a temporary port forward to allow the Let's Encrypt validation to succeed. This will lead to a short downtime of the service that is normally used with these IP addresses.<br/><div class="text-info"><b>NOTE:</b>This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.</div>]]></help>
<hint>Enter IP addresses here. Finish each with TAB.</hint>
</field>
<!--
<field>
<id>validation.http_haproxyInject</id>
<label>HAProxy Config Injection</label>
<type>checkbox</type>
<help>Automatically inject config into the local HAProxy instance to let it serve acme challanges without service interruption.</help>
</field>
<field>
<id>validation.http_haproxyFrontend</id>
<label>HAProxy Frontend</label>
<type>dropdown</type>
<help>Choose the local HAProxy frontend that should be configured to server acme challenges.</help>
</field>
<field>
<id>validation.http_relaydInject</id>
<label>Loadbalancer Config Injection</label>
<type>checkbox</type>
<help>Automatically inject config into the local Loadbalancer (relayd) to let it serve acme challanges without service interruption.</help>
</field>
<field>
<id>validation.http_relaydVserver</id>
<label>Loadbalancer Virtual Server</label>
<type>text</type>
<help>Choose the Virtual Server from the relayd Loadbalancer that should be configured to server acme challenges.</help>
</field>
-->
<field>
<label>DNS-01</label>
<type>header</type>
</field>
<field>
<id>validation.dns_service</id>
<label>DNS Service</label>
<type>dropdown</type>
<help></help>
</field>
<field>
<id>validation.dns_sleep</id>
<label>Sleep Time</label>
<type>text</type>
<help><![CDATA[The time in seconds to wait for all the TXT records to take effect DNS API mode. Default 120 seconds.]]></help>
</field>
<field>
<label>DNS-01/ad</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ad_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/ali</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ali_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ali_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/AWS Route53</label>
<type>header</type>
</field>
<field>
<id>validation.dns_aws_id</id>
<label>AWS ID</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_aws_secret</id>
<label>AWS Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/Cloudflare</label>
<type>header</type>
</field>
<field>
<id>validation.dns_cf_email</id>
<label>CF E-Mail</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_cf_key</id>
<label>CF Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/CX</label>
<type>header</type>
</field>
<field>
<id>validation.dns_cx_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_cx_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/DP</label>
<type>header</type>
</field>
<field>
<id>validation.dns_dp_id</id>
<label>ID</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_dp_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/GD</label>
<type>header</type>
</field>
<field>
<id>validation.dns_gd_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_gd_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/IPSConfig</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ispconfig_user</id>
<label>User</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ispconfig_password</id>
<label>Password</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ispconfig_api</id>
<label>API URL</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ispconfig_insecure</id>
<label>Disable SSL Verification</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/lexicon</label>
<type>header</type>
</field>
<field>
<id>validation.dns_lexicon_provider</id>
<label>Provider</label>
<type>dropdown</type>
<help></help>
</field>
<field>
<id>validation.dns_lexicon_user</id>
<label>User</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_lexicon_token</id>
<label>Token</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/lua</label>
<type>header</type>
</field>
<field>
<id>validation.dns_lua_email</id>
<label>E-Mail</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_lua_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/ME</label>
<type>header</type>
</field>
<field>
<id>validation.dns_me_key</id>
<label>Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_me_secret</id>
<label>Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<label>DNS-01/nsupdate</label>
<type>header</type>
</field>
<field>
<id>validation.dns_nsupdate_server</id>
<label>Server (FQDN)</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_nsupdate_key</id>
<label>Secret Key</label>
<type>textbox</type>
<help></help>
</field>
<field>
<label>DNS-01/OVH</label>
<type>header</type>
</field>
<field>
<id>validation.dns_ovh_app_key</id>
<label>Application Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ovh_app_secret</id>
<label>Application Secret</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ovh_consumer_key</id>
<label>Consumer Key</label>
<type>text</type>
<help></help>
</field>
<field>
<id>validation.dns_ovh_endpoint</id>
<label>Endpoint</label>
<type>text</type>
<help><![CDATA[Specify the OVH endpoint, i.e. ovh-eu, ovh-ca, kimsufi-eu, etc. Please refer to the <a href="https://github.com/Neilpang/acme.sh/tree/master/dnsapi">acme.sh documentation</a> for further information.]]></help>
</field>
<field>
<label>DNS-01/PowerDNS</label>
<type>header</type>
</field>
<field>
<id>validation.dns_pdns_url</id>
<label>URL</label>
<type>text</type>
<help><![CDATA[Specify the URL for your PowerDNS server, i.e. http://ns.example.com:8081.]]></help>
</field>
<field>
<id>validation.dns_pdns_serverid</id>
<label>Server ID</label>
<type>text</type>
<help><![CDATA[Specify the Server ID of your PowerDNS server, i.e. localhost.]]></help>
</field>
<field>
<id>validation.dns_pdns_token</id>
<label>Token</label>
<type>text</type>
<help></help>
</field>
</form>

View file

@ -0,0 +1,27 @@
<form>
<field>
<id>acmeclient.settings.enabled</id>
<label>Enable Plugin</label>
<type>checkbox</type>
<help><![CDATA[Enable Let's Encrypt plugin]]></help>
</field>
<field>
<id>acmeclient.settings.autoRenewal</id>
<label>Auto Renewal</label>
<type>checkbox</type>
<help><![CDATA[Enable automatic renewal for certificates to prevent expiration.]]></help>
</field>
<field>
<id>acmeclient.settings.environment</id>
<label>Let's Encrypt Environment</label>
<type>dropdown</type>
<help><![CDATA[Choose Let's Encrypts staging environment when using it for the first time or while testing new validation methods. The staging environment offers <a href="https://letsencrypt.org/docs/staging-environment/">relaxed rate limits</a>.<br/><div class="text-info"><b>NOTE:</b>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.</div>]]></help>
</field>
<field>
<id>acmeclient.settings.challengePort</id>
<label>Local HTTP Port</label>
<type>text</type>
<help><![CDATA[When using HTTP-01 as validation method, a local webserver is used to provide acme challenge data to the Let's Encrypt servers. This setting allows you to change the local port of this webserver in case it interferes with another local services. Defaults to port 43580.]]></help>
<advanced>true</advanced>
</field>
</form>

View file

@ -0,0 +1,10 @@
<acl>
<page-services-letsencrypt>
<name>Services: Let's Encrypt</name>
<patterns>
<pattern>ui/acmeclient/*</pattern>
<pattern>api/acmeclient/*</pattern>
<pattern>diag_logs_acmeclient.php</pattern>
</patterns>
</page-services-letsencrypt>
</acl>

View file

@ -0,0 +1,68 @@
<?php
/**
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 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.
*
*/
namespace OPNsense\AcmeClient;
use OPNsense\Base\BaseModel;
/**
* Class AcmeClient
* @package OPNsense\AcmeClient
*/
class AcmeClient extends BaseModel
{
/**
* retrieve certificate by number
* @param $certificateid certificate number
* @return null|BaseField certificate details
*/
public function getByCertificateID($certificateid)
{
foreach ($this->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;
}
}

View file

@ -0,0 +1,373 @@
<model>
<mount>//OPNsense/AcmeClient</mount>
<version>1.0.0</version>
<description>
a secure Let's Encrypt plugin
</description>
<items>
<settings>
<enabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
</enabled>
<autoRenewal type="BooleanField">
<default>1</default>
<Required>Y</Required>
</autoRenewal>
<UpdateCron type="ModelRelationField">
<Model>
<queues>
<source>OPNsense.Cron.Cron</source>
<items>jobs.job</items>
<display>description</display>
<filters>
<origin>/AcmeClient/</origin>
</filters>
</queues>
</Model>
<ValidationMessage>Related cron not found.</ValidationMessage>
<Required>N</Required>
</UpdateCron>
<environment type="OptionField">
<Required>Y</Required>
<default>prod</default>
<OptionValues>
<prod>Production Environment [default]</prod>
<stg>Staging Environment</stg>
</OptionValues>
</environment>
<challengePort type="IntegerField">
<default>43580</default>
<MinimumValue>1024</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>Y</Required>
</challengePort>
</settings>
<accounts>
<account type="ArrayField">
<id type="UniqueIdField">
<Required>N</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>Y</Required>
<mask>/^([0-9a-zA-Z._]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</name>
<description type="TextField">
<Required>N</Required>
<mask>/^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</description>
<email type="EmailField">
<Required>N</Required>
</email>
<certificateAuthority type="OptionField">
<Required>Y</Required>
<default>letsencrypt</default>
<OptionValues>
<letsencrypt>Let's Encrypt CA</letsencrypt>
</OptionValues>
</certificateAuthority>
<!-- hidden field; the private key for this account -->
<key type="TextField">
<Required>N</Required>
</key>
<!-- hidden field; last update of this account (unixtime) -->
<lastUpdate type="IntegerField">
<Required>N</Required>
</lastUpdate>
</account>
</accounts>
<certificates>
<certificate type="ArrayField">
<id type="UniqueIdField">
<Required>N</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>Y</Required>
<mask>/^([0-9a-zA-Z._]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</name>
<description type="TextField">
<Required>N</Required>
<mask>/^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</description>
<altNames type="CSVListField">
<Required>N</Required>
<multiple>Y</multiple>
<!--- XXX: FQDN should at least contain one dot -->
<mask>/^((([0-9a-zA-Z._\-\*]+\.[0-9a-zA-Z._\-\*]+(-[0-9]+)?)([,]){0,1}))*/u</mask>
<ChangeCase>lower</ChangeCase>
<ValidationMessage>Please provide a valid FQDN, i.e. www.example.com or mail.example.com.</ValidationMessage>
</altNames>
<account type="ModelRelationField">
<Model>
<template>
<source>OPNsense.AcmeClient.AcmeClient</source>
<items>accounts.account</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related item not found</ValidationMessage>
<multiple>N</multiple>
<Required>Y</Required>
</account>
<validationMethod type="ModelRelationField">
<Model>
<template>
<source>OPNsense.AcmeClient.AcmeClient</source>
<items>validations.validation</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related item not found</ValidationMessage>
<multiple>N</multiple>
<Required>Y</Required>
</validationMethod>
<autoRenewal type="BooleanField">
<default>1</default>
<Required>Y</Required>
</autoRenewal>
<renewInterval type="IntegerField">
<Required>Y</Required>
<MinimumValue>1</MinimumValue>
<MaximumValue>60</MaximumValue>
<default>60</default>
</renewInterval>
<!-- hidden field; ID of the certificate in Cert Manager -->
<certRefId type="TextField">
<Required>N</Required>
</certRefId>
<!-- hidden field; last update of this certificate (unixtime) -->
<lastUpdate type="IntegerField">
<Required>N</Required>
</lastUpdate>
</certificate>
</certificates>
<validations>
<validation type="ArrayField">
<id type="UniqueIdField">
<Required>N</Required>
</id>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>Y</Required>
<mask>/^([0-9a-zA-Z._]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</name>
<description type="TextField">
<Required>N</Required>
<mask>/^([\t\n\v\f\r 0-9a-zA-Z.:\-,_()\x{00A0}-\x{FFFF}]){1,255}$/u</mask>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</description>
<method type="OptionField">
<Required>Y</Required>
<default>http01</default>
<OptionValues>
<http01>HTTP-01</http01>
<dns01>DNS-01</dns01>
</OptionValues>
</method>
<http_service type="OptionField">
<Required>Y</Required>
<default>opnsense</default>
<OptionValues>
<opnsense>OPNsense port forward (specify Interface or IP)</opnsense>
<!-- WIP/TODO
<haproxy>HAProxy Frontend (OPNsense plugin)</haproxy>
<relayd>relayd Loadbalancer Virtual Server (OPNsense plugin)</relayd>
-->
</OptionValues>
</http_service>
<http_opn_autodiscovery type="BooleanField">
<default>1</default>
<Required>N</Required>
</http_opn_autodiscovery>
<!-- XXX: we want something more like get_possible_listen_ips() instead -->
<http_opn_interface type="InterfaceField">
<Required>N</Required>
<default>wan</default>
<filters>
<enable>/^(?!0).*$/</enable>
</filters>
</http_opn_interface>
<http_opn_ipaddresses type="CSVListField">
<Required>N</Required>
<multiple>Y</multiple>
</http_opn_ipaddresses>
<!-- WIP/TODO
<http_haproxyInject type="BooleanField">
<default>1</default>
<Required>N</Required>
</http_haproxyInject>
<http_haproxyFrontend type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>frontends.frontend</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related item not found</ValidationMessage>
<multiple>N</multiple>
<Required>N</Required>
</http_haproxyFrontend>
<http_relaydInject type="BooleanField">
<default>1</default>
<Required>N</Required>
</http_relaydInject>
<http_relaydVserver type="TextField">
<Required>N</Required>
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
</http_relaydVserver>
-->
<dns_service type="OptionField">
<Required>Y</Required>
<default>dns_nsupdate</default>
<OptionValues>
<dns_ad>Alwaysdata.com API</dns_ad>
<dns_ali>aliyun.com API</dns_ali>
<dns_aws>AWS Route 53</dns_aws>
<dns_cf>CloudFlare.com API</dns_cf>
<dns_cx>CloudXNS.com API</dns_cx>
<dns_dp>DNSPod.cn API</dns_dp>
<dns_gd>GoDaddy.com API</dns_gd>
<dns_ispconfig>ISPConfig 3.1+ API</dns_ispconfig>
<dns_lexicon>lexicon DNS API</dns_lexicon>
<dns_lua>LuaDNS.com API</dns_lua>
<dns_me>DNSMadeEasy.com API</dns_me>
<dns_nsupdate>nsupdate (RFC 2136)</dns_nsupdate>
<dns_ovh>OVH, kimsufi, soyoustart and runabove API</dns_ovh>
<dns_pdns>PowerDNS.com API</dns_pdns>
</OptionValues>
</dns_service>
<dns_sleep type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>10000</MaximumValue>
<default>120</default>
<ValidationMessage>Please specify a value between 1 and 10000.</ValidationMessage>
<Required>Y</Required>
</dns_sleep>
<dns_ad_key type="TextField">
<Required>N</Required>
</dns_ad_key>
<dns_ali_key type="TextField">
<Required>N</Required>
</dns_ali_key>
<dns_ali_secret type="TextField">
<Required>N</Required>
</dns_ali_secret>
<dns_aws_id type="TextField">
<Required>N</Required>
</dns_aws_id>
<dns_aws_secret type="TextField">
<Required>N</Required>
</dns_aws_secret>
<dns_cf_email type="TextField">
<Required>N</Required>
</dns_cf_email>
<dns_cf_key type="TextField">
<Required>N</Required>
</dns_cf_key>
<dns_cx_key type="TextField">
<Required>N</Required>
</dns_cx_key>
<dns_cx_secret type="TextField">
<Required>N</Required>
</dns_cx_secret>
<dns_dp_id type="TextField">
<Required>N</Required>
</dns_dp_id>
<dns_dp_key type="TextField">
<Required>N</Required>
</dns_dp_key>
<dns_gd_key type="TextField">
<Required>N</Required>
</dns_gd_key>
<dns_gd_secret type="TextField">
<Required>N</Required>
</dns_gd_secret>
<dns_ispconfig_user type="TextField">
<Required>N</Required>
</dns_ispconfig_user>
<dns_ispconfig_password type="TextField">
<Required>N</Required>
</dns_ispconfig_password>
<dns_ispconfig_api type="TextField">
<Required>N</Required>
</dns_ispconfig_api>
<dns_ispconfig_insecure type="BooleanField">
<Required>N</Required>
<default>1</default>
</dns_ispconfig_insecure>
<dns_lexicon_provider type="OptionField">
<Required>N</Required>
<default>cloudflare</default>
<OptionValues>
<cloudflare>Cloudflare API</cloudflare>
<namesilo>Namesilo API</namesilo>
</OptionValues>
</dns_lexicon_provider>
<dns_lexicon_user type="TextField">
<Required>N</Required>
</dns_lexicon_user>
<dns_lexicon_token type="TextField">
<Required>N</Required>
</dns_lexicon_token>
<dns_lua_email type="TextField">
<Required>N</Required>
</dns_lua_email>
<dns_lua_key type="TextField">
<Required>N</Required>
</dns_lua_key>
<dns_me_key type="TextField">
<Required>N</Required>
</dns_me_key>
<dns_me_secret type="TextField">
<Required>N</Required>
</dns_me_secret>
<dns_nsupdate_server type="TextField">
<Required>N</Required>
</dns_nsupdate_server>
<!-- TODO: maybe we should base64encode this field? -->
<dns_nsupdate_key type="TextField">
<Required>N</Required>
</dns_nsupdate_key>
<dns_ovh_app_key type="TextField">
<Required>N</Required>
</dns_ovh_app_key>
<dns_ovh_app_secret type="TextField">
<Required>N</Required>
</dns_ovh_app_secret>
<dns_ovh_consumer_key type="TextField">
<Required>N</Required>
</dns_ovh_consumer_key>
<dns_ovh_endpoint type="TextField">
<Required>N</Required>
</dns_ovh_endpoint>
<dns_pdns_url type="TextField">
<Required>N</Required>
</dns_pdns_url>
<dns_pdns_serverid type="TextField">
<Required>N</Required>
</dns_pdns_serverid>
<dns_pdns_token type="TextField">
<Required>N</Required>
</dns_pdns_token>
</validation>
</validations>
</items>
</model>

View file

@ -0,0 +1,17 @@
<menu>
<Services>
<!-- using LE prefix for proper sorting -->
<LEAcmeClient VisibleName="Let's Encrypt" cssClass="fa fa-certificate fa-fw">
<Settings order="10" url="/ui/acmeclient/">
<GeneralSettings VisibleName="Service Settings" url="/ui/acmeclient/#general-settings"/>
</Settings>
<Accounts VisibleName="Accounts" order="20" url="/ui/acmeclient/accounts/">
</Accounts>
<Validations VisibleName="Validation Methods" order="30" url="/ui/acmeclient/validations/">
</Validations>
<Certificates order="40" url="/ui/acmeclient/certificates/">
</Certificates>
<Log VisibleName="Log File" order="50" url="/diag_logs_acmeclient.php"/>
</LEAcmeClient>
</Services>
</menu>

View file

@ -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.
#}
<script type="text/javascript">
$( document ).ready(function() {
/***********************************************************************
* link grid actions
**********************************************************************/
$("#grid-accounts").UIBootgrid(
{ search:'/api/acmeclient/accounts/search',
get:'/api/acmeclient/accounts/get/',
set:'/api/acmeclient/accounts/set/',
add:'/api/acmeclient/accounts/add/',
del:'/api/acmeclient/accounts/del/',
toggle:'/api/acmeclient/accounts/toggle/',
options: {
rowCount:[10,25,50,100,500,1000]
}
}
);
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#accounts">{{ lang._('Accounts') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="accounts" class="tab-pane fade in active">
<table id="grid-accounts" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogAccount">
<thead>
<tr>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="email" data-type="string">{{ lang._('E-Mail') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
{# include dialogs #}
{{ partial("layout_partials/base_dialog",['fields':formDialogAccount,'id':'DialogAccount','label':'Edit Account'])}}

View file

@ -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.
#}
<script type="text/javascript">
$( document ).ready(function() {
/***********************************************************************
* link grid actions
**********************************************************************/
var gridParams = {
search:'/api/acmeclient/certificates/search',
get:'/api/acmeclient/certificates/get/',
set:'/api/acmeclient/certificates/set/',
add:'/api/acmeclient/certificates/add/',
del:'/api/acmeclient/certificates/del/',
toggle:'/api/acmeclient/certificates/toggle/',
sign:'/api/acmeclient/certificates/sign/',
revoke:'/api/acmeclient/certificates/revoke/',
};
var gridopt = {
ajax: true,
selection: true,
multiSelect: true,
rowCount:[10,25,50,100,500,1000],
url: '/api/acmeclient/certificates/search',
formatters: {
"commands": function (column, row) {
return "<button type=\"button\" class=\"btn btn-xs btn-default command-edit\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-pencil\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default command-copy\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-clone\"></span></button>" +
"<button type=\"button\" class=\"btn btn-xs btn-default command-delete\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-trash-o\"></span></button>" +
"<button type=\"button\" class=\"btn btn-xs btn-default command-sign\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-repeat\"></span></button>" +
"<button type=\"button\" class=\"btn btn-xs btn-default command-revoke\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-power-off\"></span></button>";
},
"rowtoggle": function (column, row) {
if (parseInt(row[column.id], 2) == 1) {
return "<span style=\"cursor: pointer;\" class=\"fa fa-check-square-o command-toggle\" data-value=\"1\" data-row-id=\"" + row.uuid + "\"></span>";
} else {
return "<span style=\"cursor: pointer;\" class=\"fa fa-square-o command-toggle\" data-value=\"0\" data-row-id=\"" + row.uuid + "\"></span>";
}
}
},
};
/**
* reload bootgrid, return to current selected page
*/
function std_bootgrid_reload(gridId) {
var currentpage = $("#"+gridId).bootgrid("getCurrentPage");
$("#"+gridId).bootgrid("reload");
// absolutely not perfect, bootgrid.reload doesn't seem to support when().done()
setTimeout(function(){
$('#'+gridId+'-footer a[data-page="'+currentpage+'"]').click();
}, 400);
}
/**
* copy actions for selected items from opnsense_bootgrid_plugin.js
*/
var grid_certificates = $("#grid-certificates").bootgrid(gridopt).on("loaded.rs.jquery.bootgrid", function (e)
{
// scale footer on resize
$(this).find("tfoot td:first-child").attr('colspan',$(this).find("th").length - 1);
$(this).find('tr[data-row-id]').each(function(){
if ($(this).find('[class*="command-toggle"]').first().data("value") == "0") {
$(this).addClass("text-muted");
}
});
// edit dialog id to use
var editDlg = $(this).attr('data-editDialog');
var gridId = $(this).attr('id');
// link Add new to child button with data-action = add
$(this).find("*[data-action=add]").click(function(){
if ( gridParams['get'] != undefined && gridParams['add'] != undefined) {
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'];
mapDataToFormUI(urlMap).done(function(){
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
//
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
saveFormToEndpoint(url=gridParams['add'],
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
$("#"+gridId).bootgrid("reload");
}, true);
});
} else {
console.log("[grid] action add missing")
}
});
// link delete selected items action
$(this).find("*[data-action=deleteSelected]").click(function(){
if ( gridParams['del'] != undefined) {
stdDialogRemoveItem("Remove selected items?",function(){
var rows =$("#"+gridId).bootgrid('getSelectedRows');
if (rows != undefined){
var deferreds = [];
$.each(rows, function(key,uuid){
deferreds.push(ajaxCall(url=gridParams['del'] + uuid, sendData={},null));
});
// refresh after load
$.when.apply(null, deferreds).done(function(){
std_bootgrid_reload(gridId);
});
}
});
} else {
console.log("[grid] action del missing")
}
});
});
/**
* copy actions for items from opnsense_bootgrid_plugin.js
*/
grid_certificates.on("loaded.rs.jquery.bootgrid", function(){
// edit dialog id to use
var editDlg = $(this).attr('data-editDialog');
var gridId = $(this).attr('id');
// edit item
grid_certificates.find(".command-edit").on("click", function(e)
{
if (editDlg != undefined && gridParams['get'] != undefined) {
var uuid = $(this).data("row-id");
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'] + uuid;
mapDataToFormUI(urlMap).done(function () {
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for pipe edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
// define save action
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
if (gridParams['set'] != undefined) {
saveFormToEndpoint(url=gridParams['set']+uuid,
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
std_bootgrid_reload(gridId);
}, true);
} else {
console.log("[grid] action set missing")
}
});
} else {
console.log("[grid] action get or data-editDialog missing")
}
});
// copy item, save as new
grid_certificates.find(".command-copy").on("click", function(e)
{
if (editDlg != undefined && gridParams['get'] != undefined) {
var uuid = $(this).data("row-id");
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'] + uuid;
mapDataToFormUI(urlMap).done(function () {
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for pipe edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
// define save action
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
if (gridParams['add'] != undefined) {
saveFormToEndpoint(url=gridParams['add'],
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
std_bootgrid_reload(gridId);
}, true);
} else {
console.log("[grid] action add missing")
}
});
} else {
console.log("[grid] action get or data-editDialog missing")
}
});
// delete item
grid_certificates.find(".command-delete").on("click", function(e)
{
if (gridParams['del'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogRemoveItem('Remove selected item?',function() {
ajaxCall(url=gridParams['del'] + uuid,
sendData={},callback=function(data,status){
// reload grid after delete
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action del missing")
}
});
// toggle item
grid_certificates.find(".command-toggle").on("click", function(e)
{
if (gridParams['toggle'] != undefined) {
var uuid=$(this).data("row-id");
$(this).addClass("fa-spinner fa-pulse");
ajaxCall(url=gridParams['toggle'] + uuid,
sendData={},callback=function(data,status){
// reload grid after toggle
std_bootgrid_reload(gridId);
});
} else {
console.log("[grid] action toggle missing")
}
});
// sign cert
// TODO: this should block other sign/revoke actions
grid_certificates.find(".command-sign").on("click", function(e)
{
if (gridParams['sign'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogRemoveItem('Sign/renew selected certificate?',function() {
ajaxCall(url=gridParams['sign'] + uuid,
sendData={},callback=function(data,status){
// reload grid after sign
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action sign missing")
}
});
// revoke cert
// TODO: this should block other sign/revoke actions
grid_certificates.find(".command-revoke").on("click", function(e)
{
if (gridParams['revoke'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogRemoveItem('Revoke selected certificate?',function() {
ajaxCall(url=gridParams['revoke'] + uuid,
sendData={},callback=function(data,status){
// reload grid after sign
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action revoke missing")
}
});
});
/***********************************************************************
* Commands
**********************************************************************/
/**
* Sign or renew ALL certificates
* TODO: this should block other sign/revoke actions
*/
$("#signallcertsAct").click(function(){
//$("#signallcertsAct_progress").addClass("fa fa-spinner fa-pulse");
ajaxCall(url="/api/acmeclient/service/signallcerts", sendData={}, callback=function(data,status) {
// when done, disable progress animation.
//$("#signallcertsAct_progress").removeClass("fa fa-spinner fa-pulse");
});
});
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#certificates">{{ lang._('Certificates') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="certificates" class="tab-pane fade in active">
<table id="grid-certificates" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogCertificate">
<thead>
<tr>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Certificate Name') }}</th>
<th data-column-id="altNames" data-type="string">{{ lang._('Multi-Domain (SAN)') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="11em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="col-md-12">
<hr/>
<button class="btn btn-primary" id="signallcertsAct" type="button"><b>{{ lang._('Issue/Renew Certificates Now') }}</b><i id="signallcertsAct_progress" class=""></i></button>
<br/>
</div>
<div class="col-md-12">
{{ 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.") }} <b>{{ 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.") }}</b>
<br/><br/>
</div>
</div>
{# include dialogs #}
{{ partial("layout_partials/base_dialog",['fields':formDialogCertificate,'id':'DialogCertificate','label':'Edit Certificate'])}}

View file

@ -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.
#}
<script type="text/javascript">
$( document ).ready(function() {
// request service status on load and update status box
ajaxCall(url="/api/acmeclient/service/status", sendData={}, callback=function(data,status) {
updateServiceStatusUI(data['status']);
});
var data_get_map = {'frm_settings':"/api/acmeclient/settings/get"};
// load initial data
mapDataToFormUI(data_get_map).done(function(data){
// set schedule updates link to cron
$.each(data.frm_settings.acmeclient.settings.UpdateCron, function(key, value) {
if (value.selected == 1) {
$("#scheduled_updates").attr("href","/ui/cron/item/open/"+key);
$("#scheduled_updates").show();
}
});
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
});
// Save & reconfigure acme-client to activate changes
$("#reconfigureAct").click(function(){
// TODO: reload the page afterwards to show/hide the "Schedule" tab
// set progress animation
$('[id*="reconfigureAct_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
// save configuration
saveFormToEndpoint(url="/api/acmeclient/settings/set",formid='frm_settings',callback_ok=function(){
});
// first run syntax check to catch critical errors
ajaxCall(url="/api/acmeclient/service/configtest", sendData={}, callback=function(data,status) {
// show warning in case of critical errors
if (data['result'].indexOf('ALERT') > -1) {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('acme-client config contains critical errors') }}",
message: "{{ lang._('The acme-client service may not be able to start due to critical errors. Try anyway?') }}",
buttons: [{
label: '{{ lang._('Continue') }}',
cssClass: 'btn-primary',
action: function(dlg){
ajaxCall(url="/api/acmeclient/service/reconfigure", sendData={}, callback=function(data,status) {
if (status != "success" || data['status'] != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
title: "{{ lang._('Error reconfiguring acme-client') }}",
message: data['status'],
draggable: true
});
}
});
ajaxCall(url="/api/acmeclient/settings/fetchRBCron", sendData={}, callback=function(data,status) {
});
// when done, disable progress animation
$('[id*="reconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
dlg.close();
}
}, {
icon: 'fa fa-trash-o',
label: '{{ lang._('Abort') }}',
action: function(dlg){
// when done, disable progress animation
$('[id*="reconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
dlg.close();
}
}]
});
} else {
ajaxCall(url="/api/acmeclient/service/reconfigure", sendData={}, callback=function(data,status) {
if (status != "success" || data['status'] != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
title: "{{ lang._('Error reconfiguring acme-client') }}",
message: data['status'],
draggable: true
});
}
ajaxCall(url="/api/acmeclient/settings/fetchRBCron", sendData={}, callback=function(data,status) {
});
// when done, disable progress animation
$('[id*="reconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
});
}
});
});
// Test configuration file
$("#configtestAct").click(function(){
// set progress animation
$('[id*="configtestAct_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
// save configuration
saveFormToEndpoint(url="/api/acmeclient/settings/set",formid='frm_settings',callback_ok=function(){
});
// run syntax check to catch critical errors
ajaxCall(url="/api/acmeclient/service/configtest", sendData={}, callback=function(data,status) {
// when done, disable progress animation
$('[id*="configtestAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
if (data['result'].indexOf('ALERT') > -1) {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('acme-client config contains critical errors') }}",
message: data['result'],
draggable: true
});
} else if (data['result'].indexOf('WARNING') > -1) {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
title: "{{ lang._('acme-client config contains minor errors') }}",
message: data['result'],
draggable: true
});
} else {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
title: "{{ lang._('acme-client config test result') }}",
message: "{{ lang._('Your acme-client config contains no errors.') }}",
draggable: true
});
}
});
});
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#settings">{{ lang._('Settings') }}</a></li>
<li><a href="" id="scheduled_updates" style="display:none">{{ lang._('Update Schedule') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="settings" class="tab-pane fade in active">
{{ partial("layout_partials/base_form",['fields':settingsForm,'id':'frm_settings'])}}
</div>
<div class="col-md-12">
<hr/>
<button class="btn btn-primary" id="reconfigureAct" type="button"><b>{{ lang._('Apply') }}</b><i id="reconfigureAct_progress" class=""></i></button>
<button class="btn btn-primary" id="configtestAct" type="button"><b>{{ lang._('Test Config') }}</b><i id="configtestAct_progress" class=""></i></button>
<br/>
</div>
<div class="col-md-12">
<b>{{ lang._("Please read the official ") }}<a href="https://letsencrypt.org/how-it-works/">{{ lang._("Let's Encrypt documentation") }}</a>{{ lang._(" before using this plugin. Otherwise you will easily hit it's ") }}<a href="https://letsencrypt.org/docs/rate-limits/">{{ lang._("rate limits") }}</a>{{ lang._(" and thus all your attempts to issue a certificate will fail. ") }}</b>{{ lang._("Please use Let's Encrypts ") }}<a href="https://letsencrypt.org/docs/staging-environment/">{{ lang._("Staging servers") }}</a>{{ 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.") }}
<br/>
{{ lang._("Please use the ") }}<a href="https://github.com/opnsense/plugins/issues">{{ lang._("GitHub Issue Tracker ") }}</a>{{ lang._("to report bugs or request new features.") }}
<br/>
<br/>
<p>Includes code from the <a href="https://github.com/Neilpang/acme.sh">Neilpang/acme.sh</a> project. Licensed under GPLv3.<br/>Let's Encrypt™ is a trademark of the Internet Security Research Group. All rights reserved.</p>
<br/>
</div>
</div>

View file

@ -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.
#}
<script type="text/javascript">
$( document ).ready(function() {
/***********************************************************************
* link grid actions
**********************************************************************/
$("#grid-validations").UIBootgrid(
{ search:'/api/acmeclient/validations/search',
get:'/api/acmeclient/validations/get/',
set:'/api/acmeclient/validations/set/',
add:'/api/acmeclient/validations/add/',
del:'/api/acmeclient/validations/del/',
toggle:'/api/acmeclient/validations/toggle/',
options: {
rowCount:[10,25,50,100,500,1000]
}
}
);
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#validations">{{ lang._('Validation Methods') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="validations" class="tab-pane fade in active">
<table id="grid-validations" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogValidation">
<thead>
<tr>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
{# include dialogs #}
{{ partial("layout_partials/base_dialog",['fields':formDialogValidation,'id':'DialogValidation','label':'Edit Validation Method'])}}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,872 @@
#!/usr/local/bin/php
<?php
/**
* Based in parts on certs.inc (thus the extended copyright notice).
*
* Copyright (C) 2017 Frank Wall
* Copyright (C) 2015 Deciso B.V.
* Copyright (C) 2010 Jim Pingle <jimp@pfsense.org>
* 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;

View file

@ -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
}

View file

@ -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 </dev/urandom | _digest "sha256" hex | cut -c 1-31
#Not so good...
date +"%s%N"
}
_check_exist_query() {
query=''
query=$query'AccessKeyId='$Ali_Key
query=$query'&Action=DescribeDomainRecords'
query=$query'&DomainName='$1
query=$query'&Format=json'
query=$query'&RRKeyWord=_acme-challenge'
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&TypeKeyWord=TXT'
query=$query'&Version=2015-01-09'
}
_add_record_query() {
query=''
query=$query'AccessKeyId='$Ali_Key
query=$query'&Action=AddDomainRecord'
query=$query'&DomainName='$1
query=$query'&Format=json'
query=$query'&RR='$2
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Type=TXT'
query=$query'&Value='$3
query=$query'&Version=2015-01-09'
}
_delete_record_query() {
query=''
query=$query'AccessKeyId='$Ali_Key
query=$query'&Action=DeleteDomainRecord'
query=$query'&Format=json'
query=$query'&RecordId='$1
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Version=2015-01-09'
}
_describe_records_query() {
query=''
query=$query'AccessKeyId='$Ali_Key
query=$query'&Action=DescribeDomainRecords'
query=$query'&DomainName='$1
query=$query'&Format=json'
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Version=2015-01-09'
}
_clean() {
_check_exist_query "$_domain"
if ! _ali_rest "Check exist records" "ignore"; then
return 1
fi
records="$(echo "$response" -n | _egrep_o "\"RecordId\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
printf "%s" "$records" \
| while read -r record_id; do
_delete_record_query "$record_id"
_ali_rest "Delete record $record_id" "ignore"
done
}
_timestamp() {
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
}

View file

@ -0,0 +1,227 @@
#!/usr/bin/env sh
#
#AWS_ACCESS_KEY_ID="sdfsdfsdfljlbjkljlkjsdfoiwje"
#
#AWS_SECRET_ACCESS_KEY="xxxxxxx"
#This is the Amazon Route53 api wrapper for acme.sh
AWS_HOST="route53.amazonaws.com"
AWS_URL="https://$AWS_HOST"
AWS_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Amazon-Route53-API"
######## Public functions #####################
#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_aws_add() {
fulldomain=$1
txtvalue=$2
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
_err "You don't specify aws route53 api key id and and api key secret yet."
_err "Please create you key and try again. see $(__green $AWS_WIKI)"
return 1
fi
if [ -z "$AWS_SESSION_TOKEN" ]; then
_saveaccountconf AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID"
_saveaccountconf AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY"
fi
_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="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>UPSERT</Action><ResourceRecordSet><Name>$fulldomain</Name><Type>TXT</Type><TTL>300</TTL><ResourceRecords><ResourceRecord><Value>\"$txtvalue\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
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="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>DELETE</Action><ResourceRecordSet><ResourceRecords><ResourceRecord><Value>\"$txtvalue\"</Value></ResourceRecord></ResourceRecords><Name>$fulldomain.</Name><Type>TXT</Type><TTL>300</TTL></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
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" "<Name>$h.</Name>"; then
hostedzone="$(echo "$response" | sed 's/<HostedZone>/\n&/g' | _egrep_o "<HostedZone>.*?<Name>$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>.*<.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" "<ErrorResponse"; then
_err "Response error:$response"
return 1
fi
fi
return "$_ret"
}

View file

@ -0,0 +1,183 @@
#!/usr/bin/env sh
#
#CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
#
#CF_Email="xxxx@sss.com"
CF_Api="https://api.cloudflare.com/client/v4"
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_cf_add() {
fulldomain=$1
txtvalue=$2
if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
CF_Key=""
CF_Email=""
_err "You don't specify cloudflare api key and email yet."
_err "Please create you key and try again."
return 1
fi
if ! _contains "$CF_Email" "@"; then
_err "It seems that the CF_Email=$CF_Email is not a valid email address."
_err "Please check and retry."
return 1
fi
#save the api key and email to the account conf file.
_saveaccountconf CF_Key "$CF_Key"
_saveaccountconf CF_Email "$CF_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"
_cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain"
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 "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
}

View file

@ -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
}

View file

@ -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 '<type>TXT</type>' | tr -d ' ')
record_id=$(printf "%s" "$response" | grep '^<id>' | 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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 ##################################

View file

@ -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}" <<EOF
server ${NSUPDATE_SERVER}
update add ${fulldomain}. 60 in txt "${txtvalue}"
send
EOF
if [ $? -ne 0 ]; then
_err "error updating domain"
return 1
fi
return 0
}
#Usage: dns_nsupdate_rm _acme-challenge.www.domain.com
dns_nsupdate_rm() {
fulldomain=$1
_checkKeyFile || return 1
[ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
_info "removing ${fulldomain}. txt"
nsupdate -k "${NSUPDATE_KEY}" <<EOF
server ${NSUPDATE_SERVER}
update delete ${fulldomain}. txt
send
EOF
if [ $? -ne 0 ]; then
_err "error updating domain"
return 1
fi
return 0
}
#################### Private functions below ##################################
_checkKeyFile() {
if [ -z "${NSUPDATE_KEY}" ]; then
_err "you must specify a path to the nsupdate key file"
return 1
fi
if [ ! -r "${NSUPDATE_KEY}" ]; then
_err "key ${NSUPDATE_KEY} is unreadable"
return 1
fi
}

View file

@ -0,0 +1,295 @@
#!/usr/bin/env sh
#Applcation Key
#OVH_AK="sdfsdfsdfljlbjkljlkjsdfoiwje"
#
#Application Secret
#OVH_AS="sdfsafsdfsdfdsfsdfsa"
#
#Consumer Key
#OVH_CK="sdfsdfsdfsdfsdfdsf"
#OVH_END_POINT=ovh-eu
#'ovh-eu'
OVH_EU='https://eu.api.ovh.com/1.0'
#'ovh-ca':
OVH_CA='https://ca.api.ovh.com/1.0'
#'kimsufi-eu'
KSF_EU='https://eu.api.kimsufi.com/1.0'
#'kimsufi-ca'
KSF_CA='https://ca.api.kimsufi.com/1.0'
#'soyoustart-eu'
SYS_EU='https://eu.api.soyoustart.com/1.0'
#'soyoustart-ca'
SYS_CA='https://ca.api.soyoustart.com/1.0'
#'runabove-ca'
RAV_CA='https://api.runabove.com/1.0'
wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-OVH-domain-api"
ovh_success="https://github.com/Neilpang/acme.sh/wiki/OVH-Success"
_ovh_get_api() {
_ogaep="$1"
case "${_ogaep}" in
ovh-eu | ovheu)
printf "%s" $OVH_EU
return
;;
ovh-ca | ovhca)
printf "%s" $OVH_CA
return
;;
kimsufi-eu | kimsufieu)
printf "%s" $KSF_EU
return
;;
kimsufi-ca | kimsufica)
printf "%s" $KSF_CA
return
;;
soyoustart-eu | soyoustarteu)
printf "%s" $SYS_EU
return
;;
soyoustart-ca | soyoustartca)
printf "%s" $SYS_CA
return
;;
runabove-ca | runaboveca)
printf "%s" $RAV_CA
return
;;
*)
_err "Unknown parameter : $1"
return 1
;;
esac
}
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_ovh_add() {
fulldomain=$1
txtvalue=$2
if [ -z "$OVH_AK" ] || [ -z "$OVH_AS" ]; then
OVH_AK=""
OVH_AS=""
_err "You don't specify OVH application key and application 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 OVH_AK "$OVH_AK"
_saveaccountconf OVH_AS "$OVH_AS"
if [ -z "$OVH_END_POINT" ]; then
OVH_END_POINT="ovh-eu"
fi
_info "Using OVH endpoint: $OVH_END_POINT"
if [ "$OVH_END_POINT" != "ovh-eu" ]; then
_saveaccountconf OVH_END_POINT "$OVH_END_POINT"
fi
OVH_API="$(_ovh_get_api $OVH_END_POINT)"
_debug OVH_API "$OVH_API"
if [ -z "$OVH_CK" ]; then
_info "OVH consumer key is empty, Let's get one:"
if ! _ovh_authentication; then
_err "Can not get consumer key."
fi
#return and wait for retry.
return 1
fi
_info "Checking authentication"
response="$(_ovh_rest GET "domain/")"
if _contains "$response" "INVALID_CREDENTIAL"; then
_err "The consumer key is invalid: $OVH_CK"
_err "Please retry to create a new one."
_clearaccountconf OVH_CK
return 1
fi
_info "Consumer key is ok."
_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"
_debug "Getting txt records"
_ovh_rest GET "domain/zone/$_domain/record?fieldType=TXT&subDomain=$_sub_domain"
if _contains "$response" '\[\]' || _contains "$response" "This service does not exist"; then
_info "Adding record"
if _ovh_rest POST "domain/zone/$_domain/record" "{\"fieldType\":\"TXT\",\"subDomain\":\"$_sub_domain\",\"target\":\"$txtvalue\",\"ttl\":60}"; then
if _contains "$response" "$txtvalue"; then
_ovh_rest POST "domain/zone/$_domain/refresh"
_debug "Refresh:$response"
_info "Added, sleeping 10 seconds"
sleep 10
return 0
fi
fi
_err "Add txt record error."
else
_info "Updating record"
record_id=$(printf "%s" "$response" | tr -d "[]" | cut -d , -f 1)
if [ -z "$record_id" ]; then
_err "Can not get record id."
return 1
fi
_debug "record_id" "$record_id"
if _ovh_rest PUT "domain/zone/$_domain/record/$record_id" "{\"target\":\"$txtvalue\",\"subDomain\":\"$_sub_domain\",\"ttl\":60}"; then
if _contains "$response" "null"; then
_ovh_rest POST "domain/zone/$_domain/refresh"
_debug "Refresh:$response"
_info "Updated, sleeping 10 seconds"
sleep 10
return 0
fi
fi
_err "Update error"
return 1
fi
}
#fulldomain
dns_ovh_rm() {
fulldomain=$1
}
#################### Private functions below ##################################
_ovh_authentication() {
_H1="X-Ovh-Application: $OVH_AK"
_H2="Content-type: application/json"
_H3=""
_H4=""
_ovhdata='{"accessRules": [{"method": "GET","path": "/*"},{"method": "POST","path": "/*"},{"method": "PUT","path": "/*"},{"method": "DELETE","path": "/*"}],"redirection":"'$ovh_success'"}'
response="$(_post "$_ovhdata" "$OVH_API/auth/credential")"
_debug3 response "$response"
validationUrl="$(echo "$response" | _egrep_o "validationUrl\":\"[^\"]*\"" | _egrep_o "http.*\"" | tr -d '"')"
if [ -z "$validationUrl" ]; then
_err "Unable to get validationUrl"
return 1
fi
_debug validationUrl "$validationUrl"
consumerKey="$(echo "$response" | _egrep_o "consumerKey\":\"[^\"]*\"" | cut -d : -f 2 | tr -d '"')"
if [ -z "$consumerKey" ]; then
_err "Unable to get consumerKey"
return 1
fi
_debug consumerKey "$consumerKey"
OVH_CK="$consumerKey"
_saveaccountconf OVH_CK "$OVH_CK"
_info "Please open this link to do authentication: $(__green "$validationUrl")"
_info "Here is a guide for you: $(__green "$wiki")"
_info "Please retry after the authentication is done."
}
#_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 ! _ovh_rest GET "domain/zone/$h"; then
return 1
fi
if ! _contains "$response" "This service does not exist" >/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
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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" )

View file

@ -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"

View file

@ -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 %}

View file

@ -0,0 +1,6 @@
<?php
$logfile = '/var/log/acme.sh.log';
$logclog = false;
require_once 'diag_logs_template.inc';