Merge pull request #4755 from fraenki/acme_4_10

security/acme-client: release 4.10
This commit is contained in:
Frank Wall
2025-07-02 14:44:24 +02:00
committed by GitHub
20 changed files with 340 additions and 212 deletions
+1 -1
View File
@@ -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
+24
View File
@@ -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:
@@ -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 = "";
@@ -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>
@@ -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>
@@ -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);
}
}
}
}
@@ -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 . ')');
}
}
}
@@ -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"
);
}
/**
@@ -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);
}
}
@@ -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(
@@ -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];
}
@@ -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);
}
@@ -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}(?&lt;![\/\\])$/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;