mirror of
https://github.com/netbirdio/plugins.git
synced 2026-05-22 18:44:07 -07:00
Merge pull request #4755 from fraenki/acme_4_10
security/acme-client: release 4.10
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
PLUGIN_NAME= acme-client
|
||||
PLUGIN_VERSION= 4.9
|
||||
PLUGIN_VERSION= 4.10
|
||||
PLUGIN_COMMENT= ACME Client
|
||||
PLUGIN_MAINTAINER= opnsense@moov.de
|
||||
PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon
|
||||
|
||||
@@ -8,6 +8,30 @@ WWW: https://github.com/acmesh-official/acme.sh
|
||||
Plugin Changelog
|
||||
================
|
||||
|
||||
4.10
|
||||
|
||||
Added:
|
||||
* new automation to reload www/caddy (#4692)
|
||||
* add support for Websupport.sk DNS API (#4540)
|
||||
* add SFTP option: Preserve Modification Time (#3862)
|
||||
|
||||
Changed:
|
||||
* automatically fix account config if CERT_HOME is set (#4622)
|
||||
* automatically resolve cron job mismatch (#4627)
|
||||
* change default SFTP options to NOT preserve modification time (#3862)
|
||||
* migrate SFTP/SSH operations to AcmeClient logging
|
||||
* add detailed SFTP/SSH logging when debug logging is enabled
|
||||
* add new log messages and improve existing ones
|
||||
|
||||
Fixed:
|
||||
* deploy hooks may use the old CERT_HOME (#4622)
|
||||
* acme.sh is always called with "--days 1" (#4711)
|
||||
* avoid startup error: "rmdir... Not a directory" (#4743)
|
||||
* fails to create/update cron job on UUID mismatch (#4627)
|
||||
|
||||
Removed:
|
||||
* remove stdout (CLI) logging from SFTP library
|
||||
|
||||
4.9
|
||||
|
||||
Added:
|
||||
|
||||
+70
-29
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2017-2021 Frank Wall
|
||||
* Copyright (C) 2017-2025 Frank Wall
|
||||
* Copyright (C) 2015 Deciso B.V.
|
||||
*
|
||||
* All rights reserved.
|
||||
@@ -59,39 +59,79 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
$mdlAcme = $this->getModel();
|
||||
$backend = new Backend();
|
||||
|
||||
// Setup cronjob if AcmeClient and AutoRenewal is enabled.
|
||||
// Setup cron job 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;
|
||||
// Check if a cron job UUID is available.
|
||||
$cron_uuid = (string)$mdlAcme->settings->UpdateCron;
|
||||
$cron_found = 0;
|
||||
if ($cron_uuid != "") {
|
||||
// Try to get cron job data from system config.
|
||||
$cron_job = (new Cron())->getNodeByReference('jobs.job.' . $cron_uuid);
|
||||
if ($cron_job != null) {
|
||||
// Cron job found, no changes required.
|
||||
$cron_found = 1;
|
||||
$this->getLogger()->notice("AcmeClient: successfully validated cron job");
|
||||
} else {
|
||||
// Cron job NOT found. This should not happen, try to fix
|
||||
// this automatically.
|
||||
$this->getLogger()->error("AcmeClient: cron job with stored UUID not found in system config: ${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();
|
||||
// Refresh the crontab
|
||||
$backend->configdRun('template reload OPNsense/Cron');
|
||||
// (res)start daemon
|
||||
$backend->configdRun("cron restart");
|
||||
$result['result'] = "new";
|
||||
$result['uuid'] = $cron_uuid;
|
||||
} else {
|
||||
$result['result'] = "unable to add cron";
|
||||
// Search for existing AcmeClient cron job.
|
||||
foreach ((new Cron())->getNodeByReference('jobs.job')->iterateItems() as $cron) {
|
||||
$_uuid = $cron->getAttributes()["uuid"];
|
||||
$_origin = (string)$cron->origin;
|
||||
if ($_origin == 'AcmeClient') {
|
||||
// Found a matching AcmeClient cron job.
|
||||
$cron_found = 1;
|
||||
$this->getLogger()->notice("AcmeClient: found existing AcmeClient cron job, fixing inconsistency in config (new UUID: ${_uuid})");
|
||||
// Update UUID in Acme Client config.
|
||||
$mdlAcme->settings->UpdateCron = $_uuid;
|
||||
// Save updated configuration.
|
||||
// TODO: need to disable validation?
|
||||
$mdlAcme->serializeToConfig();
|
||||
Config::getInstance()->save();
|
||||
$result['result'] = "new";
|
||||
$result['uuid'] = $_uuid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matching cron job found. Create a new one.
|
||||
if ($cron_found == 0) {
|
||||
$this->getLogger()->notice("AcmeClient: no cron job for AutoRenewal found, creating a new one");
|
||||
$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();
|
||||
// Refresh the crontab
|
||||
$backend->configdRun('template reload OPNsense/Cron');
|
||||
// (res)start daemon
|
||||
$backend->configdRun("cron restart");
|
||||
$result['result'] = "new";
|
||||
$result['uuid'] = $cron_uuid;
|
||||
} else {
|
||||
$result['result'] = "unable to add cron";
|
||||
}
|
||||
}
|
||||
// Delete cronjob if AcmeClient or AutoRenewal is disabled.
|
||||
} elseif (
|
||||
@@ -99,6 +139,7 @@ class SettingsController extends ApiMutableModelControllerBase
|
||||
((string)$mdlAcme->settings->autoRenewal == "0" or
|
||||
(string)$mdlAcme->settings->enabled == "0")
|
||||
) {
|
||||
$this->getLogger()->notice("AcmeClient: plugin or AutoRenewal is disabled, removing existing cron job");
|
||||
// Get UUID, clean existin entry
|
||||
$cron_uuid = (string)$mdlAcme->settings->UpdateCron;
|
||||
$mdlAcme->settings->UpdateCron = "";
|
||||
|
||||
+6
@@ -68,6 +68,12 @@
|
||||
The path can be absolute or relative to home and must exist.
|
||||
Leave blank to not change path after login.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_modtime</id>
|
||||
<label>Preserve Modification Time</label>
|
||||
<type>checkbox</type>
|
||||
<help>Preserves modification times from the source file. Note that this is not supported by all SFTP servers and may cause the upload to fail, e.g. on VMware</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_chmod</id>
|
||||
<label>Permission (Public Keys)</label>
|
||||
|
||||
+15
@@ -1779,6 +1779,21 @@
|
||||
<label>ClientSecret</label>
|
||||
<type>password</type>
|
||||
</field>
|
||||
<field>
|
||||
<label>Websupport.sk</label>
|
||||
<type>header</type>
|
||||
<style>table_dns table_dns_websupport</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_websupport_api_key</id>
|
||||
<label>Identifier</label>
|
||||
<type>text</type>
|
||||
</field>
|
||||
<field>
|
||||
<id>validation.dns_websupport_api_secret</id>
|
||||
<label>Secret key</label>
|
||||
<type>password</type>
|
||||
</field>
|
||||
<field>
|
||||
<label>World4You</label>
|
||||
<type>header</type>
|
||||
|
||||
+25
-22
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020-2024 Frank Wall
|
||||
* Copyright (C) 2020-2025 Frank Wall
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
@@ -94,17 +94,17 @@ class LeAccount extends LeCommon
|
||||
|
||||
// Check if account key already exists both in filesystem and in config
|
||||
if (!is_file($account_key_file) || empty((string)$this->config->key)) {
|
||||
LeUtils::log_debug('creating account key for ' . (string)$this->config->name, $this->debug);
|
||||
LeUtils::log_debug('creating account key for ' . (string)$this->config->name);
|
||||
|
||||
// Check if we have an account key in our configuration
|
||||
if (!empty((string)$this->config->key)) {
|
||||
LeUtils::log_debug('exporting existing account key to filesystem for ' . (string)$this->config->name, $this->debug);
|
||||
LeUtils::log_debug('exporting existing account key to filesystem for ' . (string)$this->config->name);
|
||||
// Write key to disk
|
||||
file_put_contents($account_key_file, (string)base64_decode((string)$this->config->key));
|
||||
chmod($account_key_file, 0600);
|
||||
return true;
|
||||
} else {
|
||||
LeUtils::log_debug('generating a new account key for ' . (string)$this->config->name, $this->debug);
|
||||
LeUtils::log_debug('generating a new account key for ' . (string)$this->config->name);
|
||||
|
||||
// Preparation to run acme client
|
||||
$proc_env = $this->acme_env; // add env variables
|
||||
@@ -116,7 +116,7 @@ class LeAccount extends LeCommon
|
||||
. implode(' ', $this->acme_args) . ' '
|
||||
. LeUtils::execSafe('--accountkeylength %s', self::ACME_ACCOUNT_KEY_LENGTH) . ' '
|
||||
. LeUtils::execSafe('--accountconf %s', $account_conf_file);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd);
|
||||
|
||||
// Run acme.sh command
|
||||
$result = LeUtils::run_shell_command($acmecmd, $proc_env);
|
||||
@@ -156,7 +156,7 @@ class LeAccount extends LeCommon
|
||||
LeUtils::log_error('failed to save account key for ' . (string)$this->config->name);
|
||||
return false;
|
||||
}
|
||||
LeUtils::log_debug('successfully created account key for ' . (string)$this->config->name, $this->debug);
|
||||
LeUtils::log_debug('successfully created account key for ' . (string)$this->config->name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -194,11 +194,11 @@ class LeAccount extends LeCommon
|
||||
|
||||
// Check if account is already registered
|
||||
if (!($this->isRegistered())) {
|
||||
LeUtils::log_debug('starting account registration for ' . (string)$this->config->name, $this->debug);
|
||||
LeUtils::log_debug('starting account registration for ' . (string)$this->config->name);
|
||||
|
||||
// Check if ACME External Account Binding (EAB) is enabled
|
||||
if (!empty((string)$this->config->eab_kid) && !empty((string)$this->config->eab_hmac)) {
|
||||
LeUtils::log_debug('enabling ACME EAB for this account', $this->debug);
|
||||
LeUtils::log_debug('enabling ACME EAB for this account');
|
||||
$this->acme_args[] = LeUtils::execSafe('--eab-kid %s', $this->config->eab_kid);
|
||||
$this->acme_args[] = LeUtils::execSafe('--eab-hmac-key %s', $this->config->eab_hmac);
|
||||
}
|
||||
@@ -212,7 +212,7 @@ class LeAccount extends LeCommon
|
||||
. '--registeraccount '
|
||||
. implode(' ', $this->acme_args) . ' '
|
||||
. LeUtils::execSafe('--accountconf %s', $this->account_conf_file);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd);
|
||||
|
||||
// Run acme.sh command
|
||||
$result = LeUtils::run_shell_command($acmecmd, $proc_env);
|
||||
@@ -224,16 +224,16 @@ class LeAccount extends LeCommon
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fix account config
|
||||
$this->fixConfig();
|
||||
|
||||
// Update account status.
|
||||
LeUtils::log('account registration successful for ' . $this->config->name);
|
||||
$this->setStatus(200);
|
||||
} else {
|
||||
LeUtils::log_debug('account already registered: ' . (string)$this->config->name, $this->debug);
|
||||
LeUtils::log_debug('account already registered: ' . (string)$this->config->name);
|
||||
}
|
||||
|
||||
// Always check (and fix) account config
|
||||
$this->fixConfig();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -251,18 +251,21 @@ class LeAccount extends LeCommon
|
||||
// Parse config file and remove property
|
||||
$account_conf = parse_ini_file($account_conf_file);
|
||||
if (isset($account_conf['CERT_HOME'])) {
|
||||
LeUtils::log('fixing invalid account config (CERT_HOME): ' . $this->config->name);
|
||||
unset($account_conf['CERT_HOME']);
|
||||
}
|
||||
|
||||
// Convert array back to ini file format
|
||||
$new_account_conf = array();
|
||||
foreach ($account_conf as $key => $value) {
|
||||
$new_account_conf[] = "{$key}='{$value}'";
|
||||
}
|
||||
// Convert array back to ini file format
|
||||
$new_account_conf = array();
|
||||
foreach ($account_conf as $key => $value) {
|
||||
$new_account_conf[] = "{$key}='{$value}'";
|
||||
}
|
||||
|
||||
// Write changes back to file
|
||||
file_put_contents($account_conf_file, implode("\n", $new_account_conf) . "\n");
|
||||
chmod($account_conf_file, 0600);
|
||||
// Write changes back to file
|
||||
file_put_contents($account_conf_file, implode("\n", $new_account_conf) . "\n");
|
||||
chmod($account_conf_file, 0600);
|
||||
} else {
|
||||
LeUtils::log('account config is valid (CERT_HOME): ' . $this->config->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020-2024 Frank Wall
|
||||
* Copyright (C) 2020-2025 Frank Wall
|
||||
* Copyright (C) 2018 Deciso B.V.
|
||||
* Copyright (C) 2018 Franco Fichtner <franco@opnsense.org>
|
||||
* All rights reserved.
|
||||
@@ -131,7 +131,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
|
||||
. implode(' ', $this->acme_args);
|
||||
|
||||
// Run acme.sh command
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd);
|
||||
$result = LeUtils::run_shell_command($acmecmd, $proc_env);
|
||||
|
||||
// acme.sh records the last used deploy hook and would automatically
|
||||
@@ -156,7 +156,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
|
||||
if (!file_put_contents($filename, $contents)) {
|
||||
LeUtils::log_error('clearing recorded deploy hook from acme.sh failed (' . $filename . ')');
|
||||
} else {
|
||||
LeUtils::log_debug('cleared recorded deploy deploy hook from acme.sh (' . $filename . ')', $this->debug);
|
||||
LeUtils::log_debug('cleared recorded deploy deploy hook from acme.sh (' . $filename . ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -358,7 +358,7 @@ class LeCertificate extends LeCommon
|
||||
$configdir = (string)sprintf(self::ACME_CONFIG_DIR, (string)$this->config->id);
|
||||
foreach (array($certdir, $keydir, $configdir) as $dir) {
|
||||
if (!is_dir($dir)) {
|
||||
LeUtils::log_debug("creating directory: {$dir}", $this->debug);
|
||||
LeUtils::log_debug("creating directory: {$dir}");
|
||||
mkdir($dir, 0700, true);
|
||||
}
|
||||
}
|
||||
@@ -467,7 +467,7 @@ class LeCertificate extends LeCommon
|
||||
. '--remove '
|
||||
. implode(' ', $this->acme_args) . ' '
|
||||
. LeUtils::execSafe('--domain %s', (string)$this->config->name);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd);
|
||||
|
||||
// Run acme.sh command
|
||||
$result = LeUtils::run_shell_command($acmecmd, $proc_env);
|
||||
@@ -542,7 +542,7 @@ class LeCertificate extends LeCommon
|
||||
. implode(' ', $this->acme_args) . ' '
|
||||
. LeUtils::execSafe('--domain %s', (string)$this->config->name) . ' '
|
||||
. LeUtils::execSafe('--accountconf %s', $account_conf_file);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd);
|
||||
|
||||
// Run acme.sh command
|
||||
$result = LeUtils::run_shell_command($acmecmd, $proc_env);
|
||||
@@ -618,6 +618,8 @@ class LeCertificate extends LeCommon
|
||||
$this->loadConfig(self::CONFIG_PATH, $this->uuid);
|
||||
}
|
||||
LeUtils::log('account is registered: ' . (string)$account->config->name);
|
||||
// Always check (and fix) account config
|
||||
$account->fixConfig();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -642,7 +644,8 @@ class LeCertificate extends LeCommon
|
||||
|
||||
// Configure validation object
|
||||
$val->setNames($this->config->name, $this->config->altNames, $this->config->aliasmode, $this->config->domainalias, $this->config->challengealias);
|
||||
$val->setRenewal((int)$this->config->renewInterval);
|
||||
$renewInterval = (string)$this->config->renewInterval;
|
||||
$val->setRenewal((int)$renewInterval);
|
||||
$val->setForce($this->force);
|
||||
$val->setOcsp((string)$this->config->ocsp == 1 ? true : false);
|
||||
// strip prefix from key value
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
namespace OPNsense\AcmeClient;
|
||||
|
||||
use OPNsense\Core\Config;
|
||||
use OPNsense\AcmeClient\AcmeClient;
|
||||
|
||||
/**
|
||||
* Helper functions for LeAcme
|
||||
@@ -81,8 +82,13 @@ class LeUtils
|
||||
/**
|
||||
* log additional debug output
|
||||
*/
|
||||
public static function log_debug($msg, bool $debug = false)
|
||||
public static function log_debug($msg)
|
||||
{
|
||||
$log_config = (new AcmeClient())->getNodeByReference('settings.logLevel');
|
||||
if (strpos($log_config, "debug") !== false) {
|
||||
$debug = true;
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
syslog(LOG_NOTICE, "AcmeClient: {$msg}");
|
||||
}
|
||||
@@ -91,9 +97,13 @@ class LeUtils
|
||||
/**
|
||||
* log error messages
|
||||
*/
|
||||
public static function log_error($msg)
|
||||
public static function log_error($msg, $error = null)
|
||||
{
|
||||
syslog(LOG_ERR, "AcmeClient: {$msg}");
|
||||
syslog(LOG_ERR,
|
||||
$error
|
||||
? ("AcmeClient: $msg; Trace: " . json_encode($error, JSON_UNESCAPED_SLASHES))
|
||||
: "AcmeClient: $msg"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020-2024 Frank Wall
|
||||
* Copyright (C) 2020-2025 Frank Wall
|
||||
* Copyright (C) 2018 Deciso B.V.
|
||||
* Copyright (C) 2018 Franco Fichtner <franco@opnsense.org>
|
||||
* All rights reserved.
|
||||
@@ -167,7 +167,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
|
||||
. "--{$acme_action} "
|
||||
. implode(' ', $this->acme_args) . ' '
|
||||
. LeUtils::execSafe('--accountconf %s', $account_conf_file);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug);
|
||||
LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd);
|
||||
|
||||
// Run acme.sh command
|
||||
$result = LeUtils::run_shell_command($acmecmd, $proc_env);
|
||||
@@ -286,6 +286,6 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
|
||||
*/
|
||||
public function setRenewal(int $interval = 60)
|
||||
{
|
||||
$this->acme_args[] = LeUtils::execSafe('--days %s', (string)$interval);
|
||||
$this->acme_args[] = LeUtils::execSafe('--days %s', $interval);
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Frank Wall
|
||||
*
|
||||
* 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\LeValidation;
|
||||
|
||||
use OPNsense\AcmeClient\LeValidationInterface;
|
||||
use OPNsense\Core\Config;
|
||||
|
||||
/**
|
||||
* Websupport DNS API
|
||||
* @package OPNsense\AcmeClient
|
||||
*/
|
||||
class DnsWebsupport extends Base implements LeValidationInterface
|
||||
{
|
||||
public function prepare()
|
||||
{
|
||||
$this->acme_env['WS_ApiKey'] = (string)$this->config->dns_websupport_api_key;
|
||||
$this->acme_env['WS_ApiSecret'] = (string)$this->config->dns_websupport_api_secret;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Frank Wall
|
||||
* Copyright (C) 2019 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -28,6 +29,8 @@
|
||||
|
||||
namespace OPNsense\AcmeClient;
|
||||
|
||||
use OPNsense\AcmeClient\LeUtils;
|
||||
|
||||
/**
|
||||
* Utility class to execute shell processes and handle their IO.
|
||||
* @package OPNsense\AcmeClient
|
||||
@@ -64,7 +67,7 @@ class Process
|
||||
register_shutdown_function(function () use (&$open_processes) {
|
||||
foreach ($open_processes as $handle) {
|
||||
if (is_resource($handle)) {
|
||||
Utils::log()->error("Terminating process: " . json_encode(proc_get_status($handle)));
|
||||
LeUtils::log_error("Terminating process: " . json_encode(proc_get_status($handle)));
|
||||
@proc_terminate($handle);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +105,7 @@ class Process
|
||||
|
||||
self::manageOpenedProcess($this->handle);
|
||||
} else {
|
||||
Utils::log()->error("Failed opening '$cmd' in '$cwd'");
|
||||
LeUtils::log_error("Failed opening '$cmd' in '$cwd'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +184,7 @@ class Process
|
||||
{
|
||||
// Read up-to 10k remaining lines from STDOUT/ERR to release locks before closing.
|
||||
for ($i = 0; ($line = $this->get(0)) && $i < 10000; $i++) {
|
||||
Utils::log()->error("WARN: process: $line");
|
||||
LeUtils::log_error("WARN: process: $line");
|
||||
}
|
||||
|
||||
if ($this->isRunning()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Frank Wall
|
||||
* Copyright (C) 2019 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -28,6 +29,8 @@
|
||||
|
||||
namespace OPNsense\AcmeClient;
|
||||
|
||||
use OPNsense\AcmeClient\LeUtils;
|
||||
|
||||
/**
|
||||
* Utility class for managing SSH host-keys (known_hosts) and identity keys.
|
||||
* @package OPNsense\AcmeClient
|
||||
@@ -153,10 +156,10 @@ class SSHKeys
|
||||
// Updating $host and $host_key from known_hosts and check if we need to update known_hosts.
|
||||
if ($host_key === false && $known_by_host) {
|
||||
if ($known_by_host_matches_port) {
|
||||
Utils::log()->info("No host key specified, using existing known_hosts entry for '$host'");
|
||||
LeUtils::log_debug("No host key specified, using existing known_hosts entry for '$host'");
|
||||
$host_key = $known_by_host["key_info"];
|
||||
} else {
|
||||
Utils::log()->info("No host key specified and existing entry for '$host' cannot be used as isn't matching port $port.");
|
||||
LeUtils::log_debug("No host key specified and existing entry for '$host' cannot be used as isn't matching port $port.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +168,7 @@ class SSHKeys
|
||||
$is_key_known = true;
|
||||
} elseif ($known_by_key) {
|
||||
if (strcasecmp(trim($host), trim($known_by_key["host"])) != 0) {
|
||||
Utils::log()->info("Host key is in known_hosts but hostname differs. Changing '$host' to '{$known_by_key["host"]}'.");
|
||||
LeUtils::log_debug("Host key is in known_hosts but hostname differs. Changing '$host' to '{$known_by_key["host"]}'.");
|
||||
$host = $known_by_key["host"];
|
||||
}
|
||||
$is_key_known = true;
|
||||
@@ -196,15 +199,15 @@ class SSHKeys
|
||||
|
||||
if (!empty($matching_remote_host_keys)) {
|
||||
if ($known_by_host && $known_by_host_matches_port) {
|
||||
Utils::log()->info("Removing known_hosts entry with differing key for '{$known_by_host["host_query"]}' as it is in the way.");
|
||||
LeUtils::log_debug("Removing known_hosts entry with differing key for '{$known_by_host["host_query"]}' as it is in the way.");
|
||||
$this->removeKnownHost($known_by_host["host_query"]);
|
||||
}
|
||||
|
||||
foreach ($matching_remote_host_keys as $key) {
|
||||
Utils::log()->info("Adding known_hosts entry: " . json_encode($key["key_info"], JSON_UNESCAPED_SLASHES));
|
||||
LeUtils::log_debug("Adding known_hosts entry: " . json_encode($key["key_info"], JSON_UNESCAPED_SLASHES));
|
||||
$ok = file_put_contents($this->knownHostsFile(), $key["host_key"] . PHP_EOL, FILE_APPEND);
|
||||
if (!$ok) {
|
||||
Utils::log()->error("Failed adding known_hosts entry {$key["host_key"]}");
|
||||
LeUtils::log_error("Failed adding SSH known_hosts entry {$key["host_key"]}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,12 +334,12 @@ class SSHKeys
|
||||
? ""
|
||||
: PHP_EOL . "ssh-keyscan: " . join(PHP_EOL . "ssh-keyscan: ", $lines);
|
||||
|
||||
Utils::log()->error("Failed querying host keys ($key_type) for [$names] port $port. Exit code: {$p->exitCode} (error-marker: $marker) $output");
|
||||
LeUtils::log_error("Failed querying SSH host keys ($key_type) for [$names] port $port. Exit code: {$p->exitCode} (error-marker: $marker) $output");
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($keys)) {
|
||||
Utils::log()->info("Couldn't fetch public host key ($key_type) from {$host}:{$port}");
|
||||
LeUtils::log_debug("Couldn't fetch public host key ($key_type) from {$host}:{$port}");
|
||||
|
||||
if (!is_array($error) || empty($error)) {
|
||||
$error = ["connection_refused" => true];
|
||||
@@ -384,13 +387,13 @@ class SSHKeys
|
||||
? ""
|
||||
: PHP_EOL . join(PHP_EOL, $lines);
|
||||
|
||||
Utils::log()->error("Failed querying known hosts for $name_or_ip ($host). Exit code: {$p->exitCode} $output");
|
||||
LeUtils::log_error("Failed querying SSH known hosts for $name_or_ip ($host). Exit code: {$p->exitCode} $output");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($keys)) {
|
||||
Utils::log()->info("Didn't find $host in known_hosts");
|
||||
LeUtils::log_debug("Could not find $host in known_hosts");
|
||||
}
|
||||
|
||||
return $keys;
|
||||
@@ -408,7 +411,7 @@ class SSHKeys
|
||||
if ($p = Process::open(["ssh-keygen", "-R", $host, "-f", $this->knownHostsFile()])) {
|
||||
$ok = $p->close() === 0;
|
||||
if (!$ok) {
|
||||
Utils::log()->error("Failed removing known hosts for $host. Return code was: {$p->exitCode}");
|
||||
LeUtils::log_error("Failed removing SSH known hosts for $host. Return code was: {$p->exitCode}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,11 +436,11 @@ class SSHKeys
|
||||
"key_length" => $matches[1]
|
||||
];
|
||||
} else {
|
||||
Utils::log()->error("Unsupported hash type: $hash");
|
||||
LeUtils::log_error("Unsupported hash type: $hash");
|
||||
}
|
||||
}
|
||||
|
||||
Utils::log()->error("Failed getting hash for host_key");
|
||||
LeUtils::log_error("Failed getting hash for host_key");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -472,7 +475,7 @@ class SSHKeys
|
||||
|
||||
if ($p = Process::open($generate_key)) {
|
||||
while (($line = $p->get(10)) !== false) {
|
||||
Utils::log()->info("SSH keygen: $line");
|
||||
LeUtils::log_debug("SSH keygen: $line");
|
||||
}
|
||||
|
||||
Utils::requireThat(
|
||||
|
||||
+9
-6
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Frank Wall
|
||||
* Copyright (C) 2019 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -28,6 +29,8 @@
|
||||
|
||||
namespace OPNsense\AcmeClient;
|
||||
|
||||
use OPNsense\AcmeClient\LeUtils;
|
||||
|
||||
/**
|
||||
* Wrapper around the 'sftp' commandline client.
|
||||
* @package OPNsense\AcmeClient
|
||||
@@ -71,13 +74,13 @@ class SftpClient
|
||||
{
|
||||
if (empty(trim($host)) || empty(trim($username))) {
|
||||
$this->failed_status = ["invalid_parameters" => true];
|
||||
Utils::log()->error("Failed connecting to '$host'. Hostname or username is missing.");
|
||||
LeUtils::log_error("Failed connecting to '$host'. Hostname or username is missing.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$trust = $this->ssh_keys->trustHost($host, $host_key, $port);
|
||||
if ($trust["ok"] !== true) {
|
||||
Utils::log()->error("Failed establishing trust in '$host'; Cause: {$trust["error"]}");
|
||||
LeUtils::log_error("Failed establishing trust in '$host'; Cause: {$trust["error"]}");
|
||||
unset($trust["ok"]);
|
||||
$this->failed_status = array_merge($trust, ["host_not_trusted" => true]);
|
||||
return false;
|
||||
@@ -103,7 +106,7 @@ class SftpClient
|
||||
"-oPreferredAuthentications=publickey"
|
||||
);
|
||||
} else {
|
||||
Utils::log()->error("Failed adding client identity ($identity). Connect will likely fail.");
|
||||
LeUtils::log_error("Failed adding client identity ($identity). Connect will likely fail.");
|
||||
}
|
||||
|
||||
// Adding the host
|
||||
@@ -113,7 +116,7 @@ class SftpClient
|
||||
if ($this->process = Process::open($cmd)) {
|
||||
$this->processAvailableInput(self::CONNECT_REPLY_TIMEOUT, 1, null, 0.75);
|
||||
if (($error = $this->lastError()) || !$this->process->isRunning()) {
|
||||
Utils::log()->error("Failed connecting to '$host' (user: '$username')", $error);
|
||||
LeUtils::log_error("Failed connecting to '$host' (user: '$username')", $error);
|
||||
return false;
|
||||
}
|
||||
$this->connection_info = ["host" => $host, "port" => $port, "user" => $username];
|
||||
@@ -151,7 +154,7 @@ class SftpClient
|
||||
|
||||
$consumed = ($lines_consumer && $lines_consumer($line) === true);
|
||||
if (!$consumed) {
|
||||
Utils::log()->info("SFTP: " . rtrim($line));
|
||||
LeUtils::log_debug("SFTP: " . rtrim($line));
|
||||
}
|
||||
|
||||
if (!$lines_consumer || $consumed) {
|
||||
@@ -286,7 +289,7 @@ class SftpClient
|
||||
. (empty($remote_file) ? "" : " " . escapeshellarg($remote_file)));
|
||||
$this->processAvailableInput(self::COMMAND_REPLY_TIMEOUT, 2);
|
||||
} else {
|
||||
Utils::log()->info("put: File $local_file doesn't exist.");
|
||||
LeUtils::log_debug("put: File $local_file doesn't exist.");
|
||||
$this->failed_status = ["file_not_found" => true, "error" => $local_file];
|
||||
}
|
||||
|
||||
|
||||
+41
-22
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Frank Wall
|
||||
* Copyright (C) 2019 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -28,6 +29,8 @@
|
||||
|
||||
namespace OPNsense\AcmeClient;
|
||||
|
||||
use OPNsense\AcmeClient\LeUtils;
|
||||
|
||||
/**
|
||||
* Handles file uploads via SFTP.
|
||||
* @package OPNsense\AcmeClient
|
||||
@@ -67,9 +70,10 @@ class SftpUploader
|
||||
* @param string $remote_file the remote path to copy the file to or empty to use the files name.
|
||||
* @param bool $chmod the 4 digit unix permission to apply or false to leave it unchanged.
|
||||
* @param bool $chgrp the numeric group on the remote server to apply or false to leave it unchanged.
|
||||
* @param bool $modtime a boolean to indicate that the modification times should be preserved.
|
||||
* @return string the name of the normalized local file.
|
||||
*/
|
||||
public function addFile(string $local_file, $remote_file = "", $chmod = false, $chgrp = false): string
|
||||
public function addFile(string $local_file, $remote_file = "", $chmod = false, $chgrp = false, $modtime = false): string
|
||||
{
|
||||
Utils::requireThat(is_file($local_file) && is_readable($local_file), "Not a file or not readable: '$local_file'");
|
||||
$local_file = realpath($local_file);
|
||||
@@ -80,7 +84,8 @@ class SftpUploader
|
||||
"source" => $local_file,
|
||||
"target" => $remote_file,
|
||||
"mode" => $chmod,
|
||||
"group" => $chgrp
|
||||
"group" => $chgrp,
|
||||
"modtime" => $modtime
|
||||
];
|
||||
|
||||
return $local_file;
|
||||
@@ -94,9 +99,10 @@ class SftpUploader
|
||||
* @param int $content_last_modified the unix timestamp when the content was last modified (is preserved when chmod is also specified).
|
||||
* @param bool $chmod the 4 digit unix permission to apply or false to leave it unchanged.
|
||||
* @param bool $chgrp the numeric group on the remote server to apply or false to leave it unchanged.
|
||||
* @param bool $modtime a boolean to indicate that the modification times should be preserved.
|
||||
* @return string the name of the remote file.
|
||||
*/
|
||||
public function addContent(string $content, string $remote_file = "", $content_last_modified = 0, $chmod = false, $chgrp = false): string
|
||||
public function addContent(string $content, string $remote_file = "", $content_last_modified = 0, $chmod = false, $chgrp = false, $modtime = false): string
|
||||
{
|
||||
$local_file = $this->temporaryFile();
|
||||
Utils::requireThat($local_file, "Failed creating temporary file for '$remote_file'");
|
||||
@@ -113,7 +119,7 @@ class SftpUploader
|
||||
$remote_file = basename($local_file);
|
||||
}
|
||||
|
||||
$local_file = $this->addFile($local_file, $remote_file, $chmod, $chgrp);
|
||||
$local_file = $this->addFile($local_file, $remote_file, $chmod, $chgrp, $modtime);
|
||||
$this->pending_files[$local_file]["delete_source"] = true;
|
||||
|
||||
return $remote_file;
|
||||
@@ -153,7 +159,7 @@ class SftpUploader
|
||||
->lastError();
|
||||
|
||||
if ($error) {
|
||||
Utils::log()->error("Cannot continue since changing to initial remote path '{$this->pending_base_path}' failed", $error);
|
||||
LeUtils::log_error("Cannot continue with SFTP upload since changing to initial remote path '{$this->pending_base_path}' failed", $error);
|
||||
return self::UPLOAD_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -183,7 +189,7 @@ class SftpUploader
|
||||
try {
|
||||
$connection = $this->sftp->connected();
|
||||
if (!$connection) {
|
||||
Utils::log()->error("The sftp client is not connected, upload stopped.");
|
||||
LeUtils::log_error("The SFTP client is not connected, upload stopped.");
|
||||
return self::UPLOAD_ERROR;
|
||||
}
|
||||
|
||||
@@ -205,7 +211,7 @@ class SftpUploader
|
||||
foreach ($dir_names as $dir) {
|
||||
if ($error = $this->sftp->cd($dir)->lastError()) {
|
||||
if ($error["file_not_found"]) {
|
||||
Utils::log()->info("Creating remote directory: $dir");
|
||||
LeUtils::log_debug("SFTP creating remote directory: $dir");
|
||||
$this->sftp->clearError()
|
||||
->mkdir($dir)
|
||||
->cd($dir);
|
||||
@@ -216,7 +222,7 @@ class SftpUploader
|
||||
}
|
||||
|
||||
if ($error = $this->sftp->lastError()) {
|
||||
Utils::log()->error("Failed to cd into '$target_dir'.", $error);
|
||||
LeUtils::log_error("SFTP failed to cd into '$target_dir'", $error);
|
||||
return self::UPLOAD_ERROR;
|
||||
}
|
||||
|
||||
@@ -228,7 +234,7 @@ class SftpUploader
|
||||
if (empty($remote_files)) {
|
||||
$remote_files = $this->sftp->clearError()->ls();
|
||||
if ($error = $this->sftp->lastError()) {
|
||||
Utils::log()->error("Failed listing remote files.", $error);
|
||||
LeUtils::log_error("SFTP failed listing remote files", $error);
|
||||
return self::UPLOAD_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -239,10 +245,11 @@ class SftpUploader
|
||||
$remote_file = $remote_files[$remote_filename] ?? ["type" => "-", "owner" => $username];
|
||||
$remote_is_file = $remote_file["type"] === "-";
|
||||
$remote_is_readonly = preg_match('/^[^wW]+$/', $remote_file["permissions"] ?? "");
|
||||
LeUtils::log_debug("SFTP current remote file permissions: '{$remote_file["permissions"]}'");
|
||||
|
||||
// Check if a folder/socket/symlink, etc is in the way
|
||||
if (!$remote_is_file) {
|
||||
Utils::log()->error("Failed uploading file '{$local_file}' as there is a non-file in the way at '{$file["target"]}'");
|
||||
LeUtils::log_error("SFTP failed uploading file '{$local_file}' as there is a non-file in the way at '{$file["target"]}'");
|
||||
return self::UPLOAD_ERROR_NO_OVERWRITE;
|
||||
}
|
||||
|
||||
@@ -252,16 +259,25 @@ class SftpUploader
|
||||
$chmod = $file["mode"] ?? "";
|
||||
$chmod = preg_match('/^0\d{3}$/', $chmod) ? (string)$chmod : false;
|
||||
|
||||
// Preserving the modification time is not supported by all SFTP servers.
|
||||
$preserve = $file["modtime"];
|
||||
if ($preserve !== false) {
|
||||
LeUtils::log("SFTP upload will try to preserve file modification time for '{$file["target"]}'");
|
||||
} else {
|
||||
LeUtils::log("SFTP upload will not preserve file modification time for '{$file["target"]}'");
|
||||
}
|
||||
|
||||
// Initial upload when permissions are properly set.
|
||||
$should_upload_with_permission_change =
|
||||
$chmod !== false
|
||||
&& isset($remote_files[$remote_filename]);
|
||||
|
||||
// Upload file.
|
||||
if (!$remote_is_readonly) {
|
||||
LeUtils::log("Uploading file '{$local_file}' to '{$file["target"]}'");
|
||||
$preserve_times_and_mod = $chmod !== false;
|
||||
|
||||
if ($error = $this->sftp->put($local_file, $remote_filename, $preserve_times_and_mod)->lastError()) {
|
||||
if ($error = $this->sftp->put($local_file, $remote_filename, $preserve)->lastError()) {
|
||||
if ($error["permission_denied"] !== true) {
|
||||
$should_upload_with_permission_change = false;
|
||||
}
|
||||
@@ -269,7 +285,7 @@ class SftpUploader
|
||||
if ($should_upload_with_permission_change) {
|
||||
$this->sftp->clearError();
|
||||
} else {
|
||||
Utils::log()->error("Failed uploading file '{$local_file}' to '{$file["target"]}'", $error);
|
||||
LeUtils::log_error("SFTP failed uploading file '{$local_file}' to '{$file["target"]}'", $error);
|
||||
return self::UPLOAD_ERROR_NO_PERMISSION;
|
||||
}
|
||||
} else {
|
||||
@@ -279,19 +295,21 @@ class SftpUploader
|
||||
|
||||
// Second attempt when initial failed or was skipped due to write protection (only possible if we have chmod defined to reset permissions later)
|
||||
if ($should_upload_with_permission_change && $this->isFileOwnedByConnection($remote_file, $connection)) {
|
||||
Utils::log()->info("Trying to upload file '{$local_file}' to '{$file["target"]}' with adjusted permissions");
|
||||
LeUtils::log("Trying to upload file '{$local_file}' to '{$file["target"]}' with adjusted permissions");
|
||||
|
||||
// Change file permission to make it writable.
|
||||
if ($error = $this->sftp->chmod($remote_filename, '0600')->lastError()) {
|
||||
Utils::log()->error("Failed changing permission to '0600' for '{$file["target"]}'. ", $error);
|
||||
LeUtils::log_error("SFTP failed changing permission to '0600' for '{$file["target"]}'", $error);
|
||||
$this->sftp->clearError();
|
||||
}
|
||||
|
||||
if ($error = $this->sftp->put($local_file, $remote_filename)->lastError()) {
|
||||
Utils::log()->error("Failed uploading file (with adjusted permissions) '{$local_file}' to '{$file["target"]}'", $error);
|
||||
// Try again to upload file.
|
||||
if ($error = $this->sftp->put($local_file, $remote_filename, $preserve)->lastError()) {
|
||||
LeUtils::log_error("SFTP failed uploading file (with adjusted permissions) '{$local_file}' to '{$file["target"]}'", $error);
|
||||
return self::UPLOAD_ERROR_NO_PERMISSION;
|
||||
}
|
||||
} elseif ($remote_is_readonly) {
|
||||
Utils::log()->error("Failed uploading file '{$local_file}' to '{$file["target"]}'. Existing file is write protected.");
|
||||
LeUtils::log_error("SFTP failed uploading file '{$local_file}' to '{$file["target"]}'. Existing file is write protected.");
|
||||
return self::UPLOAD_ERROR_NO_PERMISSION;
|
||||
}
|
||||
|
||||
@@ -299,14 +317,14 @@ class SftpUploader
|
||||
// Applying chmod / chgrp if requested.
|
||||
if ($chmod) {
|
||||
if ($error = $this->sftp->chmod($remote_filename, $chmod)->lastError()) {
|
||||
Utils::log()->error("Failed chmod ($chmod) for '{$file["target"]}'", $error);
|
||||
LeUtils::log_error("SFTP failed chmod ($chmod) for '{$file["target"]}'", $error);
|
||||
return self::UPLOAD_ERROR_CHMOD_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
if ($chgrp) {
|
||||
if ($error = $this->sftp->chgrp($remote_filename, $chgrp)->lastError()) {
|
||||
Utils::log()->error("Failed chgrp ($chgrp) for '{$file["target"]}'", $error);
|
||||
LeUtils::log_error("SFTP failed chgrp ($chgrp) for '{$file["target"]}'", $error);
|
||||
return self::UPLOAD_ERROR_CHGRP_FAILED;
|
||||
}
|
||||
}
|
||||
@@ -343,8 +361,9 @@ class SftpUploader
|
||||
|
||||
$this->sftp->clearError();
|
||||
|
||||
if ($error = $this->sftp->put($local_test_file, $remote_test_file)->lastError()) {
|
||||
Utils::log()->error("Failed uploading test file to detect ownership. Next uploads may fail as well.", $error);
|
||||
// Perform test upload without preserving file modification time (extra safekeeping).
|
||||
if ($error = $this->sftp->put($local_test_file, $remote_test_file, false)->lastError()) {
|
||||
LeUtils::log_error("Failed uploading SFTP test file to detect ownership. Next uploads may fail as well", $error);
|
||||
} else {
|
||||
// Get owner of the test file
|
||||
$file_info = $this->sftp->ls()[$remote_test_file] ?? ["owner" => -1];
|
||||
@@ -400,7 +419,7 @@ class SftpUploader
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
Utils::log()->info("Removed $count files in shutdown hook instead of object destruction.");
|
||||
LeUtils::log_debug("Removed $count files in shutdown hook instead of object destruction.");
|
||||
}
|
||||
|
||||
$shared_temporary_files = [];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Frank Wall
|
||||
* Copyright (C) 2019 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -28,23 +29,7 @@
|
||||
|
||||
namespace OPNsense\AcmeClient;
|
||||
|
||||
// Optional include to get "log_error"
|
||||
@include_once("util.inc");
|
||||
|
||||
// Syslog level used for verbose info log.
|
||||
// Change to "LOG_NOTICE" to make log output visible in the UI.
|
||||
const SYSLOG_INFO_LEVEL = LOG_INFO;
|
||||
|
||||
/**
|
||||
* Interface for logging.
|
||||
* @package OPNsense\AcmeClient
|
||||
*/
|
||||
interface ILogger
|
||||
{
|
||||
function info($message);
|
||||
|
||||
function error($message, $error = null);
|
||||
}
|
||||
use OPNsense\AcmeClient\LeUtils;
|
||||
|
||||
/**
|
||||
* Shared utilities.
|
||||
@@ -52,55 +37,10 @@ interface ILogger
|
||||
*/
|
||||
class Utils
|
||||
{
|
||||
public static function &log($reconfigure_to_stdout = false): ILogger
|
||||
{
|
||||
static $logger;
|
||||
|
||||
if (!$logger || $reconfigure_to_stdout) {
|
||||
if (!$reconfigure_to_stdout && function_exists("log_error")) {
|
||||
$logger = new class implements ILogger
|
||||
{
|
||||
function info($message)
|
||||
{
|
||||
syslog(SYSLOG_INFO_LEVEL, basename(__FILE__) . ": INFO: $message");
|
||||
}
|
||||
|
||||
function error($message, $error = null)
|
||||
{
|
||||
log_error(
|
||||
$error
|
||||
? ("$message ; Cause: " . json_encode($error, JSON_UNESCAPED_SLASHES))
|
||||
: $message
|
||||
);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
$logger = new class implements ILogger
|
||||
{
|
||||
function info($message)
|
||||
{
|
||||
echo "INFO: {$message}" . PHP_EOL;
|
||||
}
|
||||
|
||||
function error($message, $error = null)
|
||||
{
|
||||
echo "ERROR: "
|
||||
. ($error
|
||||
? ("$message ; Cause: " . json_encode($error, JSON_UNESCAPED_SLASHES))
|
||||
: $message)
|
||||
. PHP_EOL;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return $logger;
|
||||
}
|
||||
|
||||
public static function requireThat($expression, $message)
|
||||
{
|
||||
if (!$expression) {
|
||||
self::log()->error("FATAL: $message");
|
||||
LeUtils::log_error("FATAL: $message");
|
||||
throw new \AssertionError($message);
|
||||
}
|
||||
return $expression;
|
||||
@@ -193,7 +133,6 @@ class Utils
|
||||
{
|
||||
static $options = [
|
||||
"-h, --help Print commandline help",
|
||||
"--log Enable log to stdout (instead of syslog)",
|
||||
"--automation-id Read options from the action specified by id or uuid",
|
||||
"--no-error Always exit with 0 (original exit codes are still logged)",
|
||||
];
|
||||
@@ -241,7 +180,7 @@ class Utils
|
||||
{
|
||||
global $argv;
|
||||
$command = self::getSelectedCLICommand($commands);
|
||||
$options = ["help", "log", "no-error"];
|
||||
$options = ["help", "no-error"];
|
||||
|
||||
$has_automation_id = preg_match('/--automation-id=\S+/', join(" ", $argv));
|
||||
if ($has_automation_id) {
|
||||
@@ -255,10 +194,6 @@ class Utils
|
||||
if (isset($options["h"]) || isset($options["help"])) {
|
||||
$help();
|
||||
} else {
|
||||
if (isset($options["log"])) {
|
||||
self::log(true)->info("Logging to stdout enabled");
|
||||
}
|
||||
|
||||
$options = array_filter($options, function ($value) {
|
||||
return !is_string($value)
|
||||
|| (!empty($value = trim($value)) && $value !== "__default_value");
|
||||
@@ -268,7 +203,7 @@ class Utils
|
||||
if (is_array($config = $optionsByActionId($options["automation-id"]))) {
|
||||
$options = array_merge($config, $options);
|
||||
} else {
|
||||
self::log()->error("No usable config found for automation-id {$options["automation-id"]}");
|
||||
LeUtils::log_error("No usable config found for automation-id {$options["automation-id"]}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -277,7 +212,7 @@ class Utils
|
||||
$code = $runner($options);
|
||||
|
||||
if ($code != $exit_success) {
|
||||
self::log()->error("Command execution failed, exit code $code. Last input was: " . json_encode($options, JSON_UNESCAPED_SLASHES));
|
||||
LeUtils::log_error("Command execution failed, exit code $code. Last input was: " . json_encode($options, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
exit(isset($options["no-error"]) ? $exit_success : $code);
|
||||
@@ -290,7 +225,7 @@ class Utils
|
||||
$help();
|
||||
} else {
|
||||
$cmd = join(" ", $argv);
|
||||
self::log()->error("Parsing of '$cmd' failed at argument '{$argv[$index]}'");
|
||||
LeUtils::log_error("Parsing of '$cmd' failed at argument '{$argv[$index]}'");
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,6 +1,6 @@
|
||||
<model>
|
||||
<mount>//OPNsense/AcmeClient</mount>
|
||||
<version>4.2.0</version>
|
||||
<version>4.3.0</version>
|
||||
<description>A secure ACME Client plugin</description>
|
||||
<items>
|
||||
<settings>
|
||||
@@ -525,6 +525,7 @@
|
||||
<dns_variomedia>Variomedia.de</dns_variomedia>
|
||||
<dns_vscale>Vscale</dns_vscale>
|
||||
<dns_vultr>Vultr</dns_vultr>
|
||||
<dns_websupport>Websupport.sk</dns_websupport>
|
||||
<dns_world4you>World4You</dns_world4you>
|
||||
<dns_yandex>Yandex PDD</dns_yandex>
|
||||
<dns_zilore>Zilore</dns_zilore>
|
||||
@@ -1247,6 +1248,12 @@
|
||||
<dns_nic_secret type="TextField">
|
||||
<Required>N</Required>
|
||||
</dns_nic_secret>
|
||||
<dns_websupport_api_key type="TextField">
|
||||
<Required>N</Required>
|
||||
</dns_websupport_api_key>
|
||||
<dns_websupport_api_secret type="TextField">
|
||||
<Required>N</Required>
|
||||
</dns_websupport_api_secret>
|
||||
<dns_world4you_username type="TextField">
|
||||
<Required>N</Required>
|
||||
</dns_world4you_username>
|
||||
@@ -1403,6 +1410,10 @@
|
||||
<Mask>/^0[0-9]{3}$/u</Mask>
|
||||
<ValidationMessage>A unix permission, 4 digits (e.g. 0400).</ValidationMessage>
|
||||
</sftp_chmod_key>
|
||||
<sftp_modtime type="BooleanField">
|
||||
<Required>N</Required>
|
||||
<default>0</default>
|
||||
</sftp_modtime>
|
||||
<sftp_filename_cert type="TextField">
|
||||
<Required>N</Required>
|
||||
<Mask>/^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui</Mask>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Frank Wall
|
||||
* Copyright (C) 2022 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -107,6 +108,7 @@ if (!function_exists("log_error")) {
|
||||
use OPNsense\AcmeClient\Process;
|
||||
use OPNsense\AcmeClient\SSHKeys;
|
||||
use OPNsense\AcmeClient\Utils;
|
||||
use OPNsense\AcmeClient\LeUtils;
|
||||
|
||||
// Implementing logic
|
||||
function commandShowIdentity(array &$options): int
|
||||
@@ -127,7 +129,7 @@ function commandShowIdentity(array &$options): int
|
||||
echo file_get_contents($id_file);
|
||||
return EXITCODE_SUCCESS;
|
||||
} else {
|
||||
Utils::log()->error("Failed getting identity. See log output for details.");
|
||||
LeUtils::log_error("SSH failed getting identity. See log output for details.");
|
||||
}
|
||||
return EXITCODE_ERROR;
|
||||
}
|
||||
@@ -156,14 +158,14 @@ function commandTestConnection(array &$options): int
|
||||
function commandRunRemote(array &$options): int
|
||||
{
|
||||
if (empty($options["run"])) {
|
||||
Utils::log()->error("SSH: Command is empty, nothing to do.");
|
||||
LeUtils::log_error("SSH: Command is empty, nothing to do.");
|
||||
return EXITCODE_ERROR;
|
||||
}
|
||||
|
||||
$lines = runRemoteCommand($options, $error);
|
||||
if (!$error) {
|
||||
$host = $options["host"] . (($port = ($options["port"] ?? false)) ? ":$port" : "");
|
||||
Utils::log()->info("SSH [$host]> {$options["run"]}:" . PHP_EOL . join(PHP_EOL, $lines));
|
||||
LeUtils::log_debug("SSH [$host]> {$options["run"]}:" . PHP_EOL . join(PHP_EOL, $lines));
|
||||
return EXITCODE_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -244,7 +246,7 @@ function runRemoteCommand(array $options, &$error): ?array
|
||||
"exit_code" => $exit_code
|
||||
]);
|
||||
$error["connect_failed"] = $exit_code == 255;
|
||||
Utils::log()->error("SSH failed with '$exit_code': $cl", $error);
|
||||
LeUtils::log_error("SSH failed with '$exit_code': $cl", $error);
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -253,7 +255,7 @@ function runRemoteCommand(array $options, &$error): ?array
|
||||
function buildSSHArguments(SSHKeys $ssh_keys, $host, $username, $identity_type = "", $host_key = "", $port = SSHKeys::DEFAULT_PORT): array
|
||||
{
|
||||
if (empty(trim($host)) || empty(trim($username))) {
|
||||
Utils::log()->error("Failed connecting to '$host'. Hostname or username is missing.");
|
||||
LeUtils::log_error("Failed connecting to '$host'. Hostname or username is missing.");
|
||||
return [false, ["invalid_parameters" => true]];
|
||||
}
|
||||
|
||||
@@ -263,7 +265,7 @@ function buildSSHArguments(SSHKeys $ssh_keys, $host, $username, $identity_type =
|
||||
|
||||
$trust = $ssh_keys->trustHost($host, $host_key, $port);
|
||||
if ($trust["ok"] !== true) {
|
||||
Utils::log()->error("Failed establishing trust in '$host'; Cause: {$trust["error"]}");
|
||||
LeUtils::log_error("Failed establishing trust in '$host'; Cause: {$trust["error"]}");
|
||||
unset($trust["ok"]);
|
||||
return [false, array_merge($trust, ["host_not_trusted" => true])];
|
||||
} else {
|
||||
@@ -288,7 +290,7 @@ function buildSSHArguments(SSHKeys $ssh_keys, $host, $username, $identity_type =
|
||||
"-oPreferredAuthentications=publickey"
|
||||
);
|
||||
} else {
|
||||
Utils::log()->error("Failed adding client identity ($identity). Connect will likely fail.");
|
||||
LeUtils::log_error("Failed adding SSH client identity ($identity). Connect will likely fail.");
|
||||
}
|
||||
|
||||
// Adding the host
|
||||
@@ -304,7 +306,7 @@ function help()
|
||||
|
||||
function getOptionsById($automation_id)
|
||||
{
|
||||
Utils::log()->info("Reading options from automation: $automation_id");
|
||||
LeUtils::log_debug("Reading options from automation: $automation_id");
|
||||
|
||||
if (is_object($action = Utils::getAutomationActionById($automation_id))) {
|
||||
if ($action->enabled && "configd_remote_ssh" === (string)$action->type) {
|
||||
@@ -317,10 +319,10 @@ function getOptionsById($automation_id)
|
||||
"run" => trim((string)$action->remote_ssh_command),
|
||||
];
|
||||
} else {
|
||||
Utils::log()->error("Ignoring disabled or invalid automation '$automation_id'");
|
||||
LeUtils::log_error("Ignoring disabled or invalid automation '$automation_id'");
|
||||
}
|
||||
} else {
|
||||
Utils::log()->error("No upload automation found with uuid = '$automation_id'");
|
||||
LeUtils::log_error("No upload automation found with uuid = '$automation_id'");
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -20,7 +20,10 @@ chmod 750 ${ACME_BASE} ${ACME_BASE}/*
|
||||
# This should guard against manual misconfiguration.
|
||||
for link in ${ACME_LINKS}; do
|
||||
# First remove any existing file/directory.
|
||||
if [ -f "${ACME_BASE}/home/${link}" ]; then
|
||||
if [ -L "${ACME_BASE}/home/${link}" ]; then
|
||||
# Already a symlink, skip this enty.
|
||||
continue
|
||||
elif [ -f "${ACME_BASE}/home/${link}" ]; then
|
||||
rm ${ACME_BASE}/home/${link}
|
||||
elif [ -d "${ACME_BASE}/home/${link}" ]; then
|
||||
rmdir ${ACME_BASE}/home/${link}
|
||||
|
||||
@@ -142,6 +142,7 @@ use OPNsense\AcmeClient\SftpUploader;
|
||||
use OPNsense\AcmeClient\SftpClient;
|
||||
use OPNsense\AcmeClient\SSHKeys;
|
||||
use OPNsense\AcmeClient\Utils;
|
||||
use OPNsense\AcmeClient\LeUtils;
|
||||
|
||||
// Implementing logic
|
||||
function commandShowIdentity(array &$options): int
|
||||
@@ -162,7 +163,7 @@ function commandShowIdentity(array &$options): int
|
||||
echo file_get_contents($id_file);
|
||||
return EXITCODE_SUCCESS;
|
||||
} else {
|
||||
Utils::log()->error("Failed getting identity. See log output for details.");
|
||||
LeUtils::log_error("SFTP failed getting identity. See log output for details.");
|
||||
}
|
||||
return EXITCODE_ERROR;
|
||||
}
|
||||
@@ -214,7 +215,7 @@ function commandTestConnection(array &$options): int
|
||||
|
||||
if ($remove_file) {
|
||||
if ($error = $sftp->clearError()->rm($filename)->lastError(3)) {
|
||||
Utils::log()->error("Failed removing upload test file '$filename'", $error);
|
||||
LeUtils::log_error("SFTP failed removing upload test file '$filename'", $error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +262,7 @@ function commandUpload(array &$options): int
|
||||
} elseif (isset($options["host"])) {
|
||||
return uploadCertificatesToHost($options);
|
||||
} else {
|
||||
Utils::log()->error("No work to do, neither --host nor --certificates is present.");
|
||||
LeUtils::log_error("No work to do, neither --host nor --certificates is present.");
|
||||
return EXITCODE_ERROR_NOTHING_TO_UPLOAD;
|
||||
}
|
||||
}
|
||||
@@ -270,7 +271,7 @@ function uploadCertificatesToHost(array $options): int
|
||||
{
|
||||
$sftp = connectWithServer($options, $error);
|
||||
if ($sftp === null) {
|
||||
Utils::log()->error("Aborting after connect failure.");
|
||||
LeUtils::log_error("SFTP aborting after connect failure.");
|
||||
return ($error["connect_failed"] ?? false)
|
||||
? EXITCODE_ERROR
|
||||
: EXITCODE_ERROR_NO_PERMISSION;
|
||||
@@ -289,7 +290,7 @@ function uploadCertificatesToHost(array $options): int
|
||||
$result = $uploader->upload();
|
||||
|
||||
if ($result != SftpUploader::UPLOAD_SUCCESS) {
|
||||
Utils::log()->error("Failed on " . json_encode($uploader->current(), JSON_UNESCAPED_SLASHES));
|
||||
LeUtils::log_error("SFTP failed on " . json_encode($uploader->current(), JSON_UNESCAPED_SLASHES));
|
||||
|
||||
switch ($result) {
|
||||
case SftpUploader::UPLOAD_ERROR_NO_PERMISSION:
|
||||
@@ -336,7 +337,7 @@ function connectWithServer(array $options, &$error): ?SftpClient
|
||||
if ($err = $sftp->cd($remote_path)->lastError()) {
|
||||
$error = $err;
|
||||
$error["change_home_dir_failed"] = true;
|
||||
Utils::log()->error("Failed cd into '{$remote_path}'", $err);
|
||||
LeUtils::log_error("SFTP failed cd into '{$remote_path}'", $err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -352,7 +353,7 @@ function help()
|
||||
function getOptionsById($automation_id, $silent = false)
|
||||
{
|
||||
if (!$silent) {
|
||||
Utils::log()->info("Reading options from automation: $automation_id");
|
||||
LeUtils::log_debug("Reading options from automation: $automation_id");
|
||||
}
|
||||
|
||||
if (is_object($action = Utils::getAutomationActionById($automation_id))) {
|
||||
@@ -367,6 +368,7 @@ function getOptionsById($automation_id, $silent = false)
|
||||
"chgrp" => trim((string)$action->sftp_chgrp),
|
||||
"chmod" => trim((string)$action->sftp_chmod),
|
||||
"chmod-key" => trim((string)$action->sftp_chmod_key),
|
||||
"modtime" => trim((string)$action->sftp_modtime),
|
||||
"cert-name" => trim((string)$action->sftp_filename_cert),
|
||||
"key-name" => trim((string)$action->sftp_filename_key),
|
||||
"ca-name" => trim((string)$action->sftp_filename_ca),
|
||||
@@ -374,10 +376,10 @@ function getOptionsById($automation_id, $silent = false)
|
||||
"certificates" => "", // defaults to all (= empty), may be overridden via CLI
|
||||
];
|
||||
} elseif (!$silent) {
|
||||
Utils::log()->error("Ignoring disabled or invalid automation '$automation_id'");
|
||||
LeUtils::log_error("SFTP ignoring disabled or invalid automation '$automation_id'");
|
||||
}
|
||||
} else {
|
||||
Utils::log()->error("No upload automation found with uuid = '$automation_id'");
|
||||
LeUtils::log_error("No SFTP upload automation found with uuid = '$automation_id'");
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -388,19 +390,20 @@ function addFilesToUpload(array $options, SftpUploader &$uploader)
|
||||
$chmod = isset($options["chmod"]) ? ($options["chmod"] ?: DEFAULT_CERT_MODE) : false;
|
||||
$chmod_key = isset($options["chmod-key"]) ? ($options["chmod-key"] ?: DEFAULT_KEY_MODE) : false;
|
||||
$chgrp = ($options["chgrp"] ?? "") ?: false;
|
||||
$modtime = ($options["modtime"] ?? "") ?: false;
|
||||
|
||||
if (isset($options["certificates"])) {
|
||||
$cert_ids = preg_split('/[,;\s]+/', $options["certificates"] ?: "", 0, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
foreach (findCertificates($cert_ids) as $cert) {
|
||||
if (!isset($cert["content"])) {
|
||||
Utils::log()->error("Ignoring upload for cert '{$cert["name"]}', since it is not available in trust storage.");
|
||||
LeUtils::log_error("Ignoring SFTP upload for cert '{$cert["name"]}', since it is not available in trust storage.");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($cert["content"] as $name => $content) {
|
||||
if (empty($content)) {
|
||||
Utils::log()->error("Content for '{$name}.pem' in cert '{$cert["name"]}' is empty, skipping it.");
|
||||
LeUtils::log_error("Content for '{$name}.pem' in cert '{$cert["name"]}' is empty, skipping SFTP upload.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -441,27 +444,27 @@ function addFilesToUpload(array $options, SftpUploader &$uploader)
|
||||
? $chmod_key
|
||||
: $chmod;
|
||||
|
||||
$uploader->addContent($content, $target_path, $cert["updated"], $mod, $chgrp);
|
||||
$uploader->addContent($content, $target_path, $cert["updated"], $mod, $chgrp, $modtime);
|
||||
} else {
|
||||
Utils::log()->error("Cannot add '{$name}.pem' since the upload path '$target_path' is invalid.");
|
||||
LeUtils::log_error("Cannot add '{$name}.pem' to SFTP upload since the upload path '$target_path' is invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($uploader->pending())) {
|
||||
Utils::log()->error("Didn't find any certificates to upload (cert-ids: " . (empty($cert_ids) ? "*all*" : join(", ", $cert_ids)) . ").");
|
||||
LeUtils::log_error("Could not find any certificates for SFTP upload (cert-ids: " . (empty($cert_ids) ? "*all*" : join(", ", $cert_ids)) . ").");
|
||||
}
|
||||
} elseif (isset($options["files"])) {
|
||||
$files = preg_split('/[,;\s]+/', $options["files"] ?: "", 0, PREG_SPLIT_NO_EMPTY);
|
||||
foreach ($files as $file) {
|
||||
$uploader->addFile($file, "", $chmod, $chgrp);
|
||||
$uploader->addFile($file, "", $chmod, $chgrp, $modtime);
|
||||
}
|
||||
|
||||
if (empty($uploader->pending())) {
|
||||
Utils::log()->error("Didn't files to upload (files: " . join(", ", $files) . ").");
|
||||
LeUtils::log_error("Could not find files for SFTP upload (files: " . join(", ", $files) . ").");
|
||||
}
|
||||
} else {
|
||||
Utils::log()->error("Neither '--certificates' nor '--files' was specified. Have nothing to upload.");
|
||||
LeUtils::log_error("Neither '--certificates' nor '--files' was specified. Have nothing to upload.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +492,7 @@ function findCertificates(array $certificate_ids_or_names, $load_content = true)
|
||||
) {
|
||||
if ($cert->enabled == 0) {
|
||||
if (!empty($certificate_ids_or_names)) {
|
||||
Utils::log()->error("Certificate '{$name}' (id: $id) is disabled, skipping it.");
|
||||
LeUtils::log_error("Certificate '{$name}' (id: $id) is disabled, skipping SFTP upload.");
|
||||
}
|
||||
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user