mirror of
https://github.com/opnsense/plugins.git
synced 2026-06-04 22:33:07 -04:00
security/acme-client: add acme.sh to plugins, closes #6
This commit is contained in:
parent
928228a860
commit
dd4853d09f
47 changed files with 11147 additions and 0 deletions
7
security/acme-client/Makefile
Normal file
7
security/acme-client/Makefile
Normal 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"
|
||||
|
|
@ -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);
|
||||
}
|
||||
*/
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'])}}
|
||||
|
|
@ -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'])}}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'])}}
|
||||
4649
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh
Executable file
4649
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh
Executable file
File diff suppressed because it is too large
Load diff
872
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php
Executable file
872
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php
Executable 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;
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
183
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cf.sh
Executable file
183
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cf.sh
Executable 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
|
||||
}
|
||||
216
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh
Executable file
216
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh
Executable 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
|
||||
}
|
||||
223
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_dp.sh
Executable file
223
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_dp.sh
Executable 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
|
||||
}
|
||||
117
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh
Executable file
117
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh
Executable 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
143
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh
Executable file
143
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh
Executable 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
|
||||
}
|
||||
146
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh
Executable file
146
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh
Executable 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
|
||||
}
|
||||
|
|
@ -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 ##################################
|
||||
|
|
@ -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
|
||||
}
|
||||
295
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ovh.sh
Executable file
295
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ovh.sh
Executable 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
|
||||
}
|
||||
184
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_pdns.sh
Executable file
184
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_pdns.sh
Executable 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
|
||||
}
|
||||
14
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh
Executable file
14
security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh
Executable 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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" )
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 %}
|
||||
6
security/acme-client/src/www/diag_logs_acmeclient.php
Normal file
6
security/acme-client/src/www/diag_logs_acmeclient.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
$logfile = '/var/log/acme.sh.log';
|
||||
$logclog = false;
|
||||
|
||||
require_once 'diag_logs_template.inc';
|
||||
Loading…
Reference in a new issue