mirror of
https://github.com/netbirdio/plugins.git
synced 2026-05-22 18:44:07 -07:00
Merge pull request #2904 from jkellerer/ft-ssh-automation
security/acme-client: SSH remote command automation (#2757)
This commit is contained in:
+26
@@ -101,6 +101,32 @@ class ActionsController extends ApiMutableModelControllerBase
|
||||
return ["status" => "unavailable"];
|
||||
}
|
||||
|
||||
public function sshGetIdentityAction()
|
||||
{
|
||||
$result = ["status" => "unavailable"];
|
||||
|
||||
if ($response = $this->callBackend(["show-remote-ssh-identity"], ["remote_ssh_identity_type", "remote_ssh_host"])) {
|
||||
$result["status"] = "ok";
|
||||
$result["identity"] = $response;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function sshTestConnectionAction()
|
||||
{
|
||||
if (
|
||||
$response = $this->callBackend(
|
||||
["test-remote-ssh-connection"],
|
||||
["remote_ssh_host", "remote_ssh_host_key", "remote_ssh_port", "remote_ssh_user", "remote_ssh_identity_type"]
|
||||
)
|
||||
) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return ["status" => "unavailable"];
|
||||
}
|
||||
|
||||
private function callBackend(array $command, array $arguments = [])
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
|
||||
+43
@@ -142,6 +142,49 @@
|
||||
Leave blank to use default "{{name}}/fullchain.pem".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<label>Required Parameters</label>
|
||||
<type>header</type>
|
||||
<style>method_table method_table_configd_remote_ssh</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.remote_ssh_host</id>
|
||||
<label>SSH Host</label>
|
||||
<type>text</type>
|
||||
<help>IP address or hostname of the SSH server.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.remote_ssh_port</id>
|
||||
<label>SSH Port</label>
|
||||
<type>text</type>
|
||||
<help>SSH server port. Leave blank to use default "22".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.remote_ssh_key</id>
|
||||
<label>Host Key</label>
|
||||
<type>text</type>
|
||||
<help>SSH server host key, formatted as in 'known_hosts'.
|
||||
Leave blank to auto accept host key on first connect (not as secure as specifying it).</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.remote_ssh_user</id>
|
||||
<label>Username</label>
|
||||
<type>text</type>
|
||||
<help>The username to login to the SSH server.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.remote_ssh_identity_type</id>
|
||||
<label>Identity Type</label>
|
||||
<type>dropdown</type>
|
||||
<help>The type of identify to present to the SSH server for authorization. Select 'none' to use default "ECDSA".</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.remote_ssh_command</id>
|
||||
<label>Command</label>
|
||||
<type>text</type>
|
||||
<help>The command to execute on the SSH server.</help>
|
||||
</field>
|
||||
<field>
|
||||
<label>Required Parameters</label>
|
||||
<type>header</type>
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020-2022 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
namespace OPNsense\AcmeClient\LeAutomation;
|
||||
|
||||
use OPNsense\AcmeClient\LeAutomationInterface;
|
||||
|
||||
/**
|
||||
* Run remote command on arbitrary hosts using SSH
|
||||
* @package OPNsense\AcmeClient
|
||||
*/
|
||||
class ConfigdRemoteSsh extends Base implements LeAutomationInterface
|
||||
{
|
||||
public function prepare()
|
||||
{
|
||||
$command = 'acmeclient run-remote-ssh-command ' . $this->config->id;
|
||||
$this->command = $command;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -115,21 +115,49 @@ class Process
|
||||
}
|
||||
}
|
||||
|
||||
public function get($timeout = 5, $max_length = 8192, $ending = PHP_EOL)
|
||||
private $linesBuffer = [];
|
||||
|
||||
private function nextBufferedLine()
|
||||
{
|
||||
$readables = array_filter($this->outputs, function ($stream) {
|
||||
return is_resource($stream) && !feof($stream);
|
||||
});
|
||||
return empty($this->linesBuffer)
|
||||
? false
|
||||
: array_shift($this->linesBuffer);
|
||||
}
|
||||
|
||||
$micros = intval(($timeout - floor($timeout)) * 1000000);
|
||||
$can_read = !empty($readables) && stream_select($readables, $w = [], $e = [], $timeout, $micros);
|
||||
$stream = array_reduce(($can_read ? $readables : []), function ($a, $b) {
|
||||
return is_resource($a) && !feof($a) ? $a : $b;
|
||||
}, null);
|
||||
/**
|
||||
* Returns one line from stdout or stdin as it gets available. May return 'false' when no line became available
|
||||
* within the specified $timeout or when another stream events occurred that returned no new content.
|
||||
* @param $timeout float timeout in seconds
|
||||
* @param $max_length int max length of a single line
|
||||
* @return false|string One line of stdout/err (merged) or false when no new line exists.
|
||||
*/
|
||||
public function get($timeout = 5, $max_length = 64 * 1024)
|
||||
{
|
||||
if (($line = $this->nextBufferedLine()) !== false) {
|
||||
return $line;
|
||||
}
|
||||
|
||||
return is_resource($stream)
|
||||
? stream_get_line($stream, $max_length, $ending)
|
||||
: false;
|
||||
$readables = array_filter($this->outputs, fn($stream) => is_resource($stream) && !feof($stream));
|
||||
$micros = intval(($timeout - floor($timeout)) * 1000000) + 100;
|
||||
$timeout = floor($timeout);
|
||||
$__ = null;
|
||||
|
||||
$can_read = !empty($readables)
|
||||
&& stream_select($readables, $__, $__, $timeout, $micros) !== false;
|
||||
|
||||
if ($can_read) {
|
||||
foreach ($readables as $stream) {
|
||||
$content = fread($stream, $max_length);
|
||||
if ($content !== false) {
|
||||
array_push($this->linesBuffer, ...preg_split('/\r\n|\n|\r/', $content));
|
||||
if (empty($this->linesBuffer[-1])) {
|
||||
array_pop($this->linesBuffer); // remove trailing empty newline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->nextBufferedLine();
|
||||
}
|
||||
|
||||
public function put($data, $append = PHP_EOL)
|
||||
|
||||
@@ -122,7 +122,7 @@ class SSHKeys
|
||||
|
||||
|
||||
// Check our current known_host file
|
||||
$addKeyInfo = function (array &$key_list) {
|
||||
$addKeyInfo = function (array $key_list) {
|
||||
foreach ($key_list as &$item) {
|
||||
$item["key_info"] = self::getHostKeyInfo($item["host_key"]);
|
||||
}
|
||||
@@ -182,7 +182,7 @@ class SSHKeys
|
||||
if (
|
||||
empty($remote_host_keys)
|
||||
&& $query_error
|
||||
&& $query_error["connection_refused"]
|
||||
&& ($query_error["connection_refused"] ?? false)
|
||||
&& !$host_key
|
||||
&& self::ALTERNATE_DEFAULT_KEY_TYPE != self::DEFAULT_KEY_TYPE
|
||||
) {
|
||||
@@ -451,7 +451,7 @@ class SSHKeys
|
||||
{
|
||||
Utils::requireThat(in_array($identity_type, self::IDENTITY_TYPES), "Identity type '$identity_type' unknown.");
|
||||
|
||||
list($key_type, $key_size) = explode('_', $identity_type, 2);
|
||||
list($key_type, $key_size) = explode('_', "{$identity_type}_", 2);
|
||||
if (!$key_size && self::DEFAULT_IDENTITY_KEY_BITS[$key_type] > 0) {
|
||||
$key_size = self::DEFAULT_IDENTITY_KEY_BITS[$key_type];
|
||||
}
|
||||
|
||||
+1
-1
@@ -173,7 +173,7 @@ class SftpClient
|
||||
|
||||
$this->process = null;
|
||||
|
||||
if ($this->failed_status && $this->failed_status["connection_closed"]) {
|
||||
if ($this->failed_status && ($this->failed_status["connection_closed"] ?? false)) {
|
||||
$this->clearError();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -236,9 +236,9 @@ class SftpUploader
|
||||
// Preparing upload
|
||||
$username = $connection["user"];
|
||||
$remote_filename = basename((empty($file["target"]) ? $local_file : $file["target"]));
|
||||
$remote_file = $remote_files[$remote_filename] ?: ["type" => "-", "owner" => $username];
|
||||
$remote_file = $remote_files[$remote_filename] ?? ["type" => "-", "owner" => $username];
|
||||
$remote_is_file = $remote_file["type"] === "-";
|
||||
$remote_is_readonly = preg_match('/^[^wW]+$/', $remote_file["permissions"] ?: "");
|
||||
$remote_is_readonly = preg_match('/^[^wW]+$/', $remote_file["permissions"] ?? "");
|
||||
|
||||
// Check if a folder/socket/symlink, etc is in the way
|
||||
if (!$remote_is_file) {
|
||||
@@ -246,10 +246,10 @@ class SftpUploader
|
||||
return self::UPLOAD_ERROR_NO_OVERWRITE;
|
||||
}
|
||||
|
||||
$chgrp = $file["group"] ?: "";
|
||||
$chgrp = $file["group"] ?? "";
|
||||
$chgrp = preg_match('/^\d+$/', $chgrp) ? (string)$chgrp : false;
|
||||
|
||||
$chmod = $file["mode"] ?: "";
|
||||
$chmod = $file["mode"] ?? "";
|
||||
$chmod = preg_match('/^0\d{3}$/', $chmod) ? (string)$chmod : false;
|
||||
|
||||
|
||||
@@ -347,7 +347,7 @@ class SftpUploader
|
||||
Utils::log()->error("Failed uploading 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];
|
||||
$file_info = $this->sftp->ls()[$remote_test_file] ?? ["owner" => -1];
|
||||
// Cleanup
|
||||
$this->sftp->rm($remote_test_file);
|
||||
$this->sftp->clearError();
|
||||
@@ -370,7 +370,7 @@ class SftpUploader
|
||||
if (
|
||||
isset($this->pending_files[$file])
|
||||
&& is_array($existing = $this->pending_files[$file])
|
||||
&& $existing["delete_source"] === true
|
||||
&& ($existing["delete_source"] ?? false) === true
|
||||
) {
|
||||
unlink($existing["source"]);
|
||||
}
|
||||
@@ -408,7 +408,7 @@ class SftpUploader
|
||||
}
|
||||
|
||||
$index = $this->temporary_files_index;
|
||||
if ($index <= 0 || !is_array($shared_temporary_files[$index])) {
|
||||
if ($index <= 0 || !is_array($shared_temporary_files[$index] ?? null)) {
|
||||
$index = $this->temporary_files_index = ++$shared_temporary_files_index_sequence;
|
||||
$shared_temporary_files[$index] = [];
|
||||
}
|
||||
|
||||
@@ -142,4 +142,174 @@ class Utils
|
||||
|
||||
return DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string the path where acme.sh config is stored or null if not available.
|
||||
*/
|
||||
public static function configPath(): string
|
||||
{
|
||||
static $paths = [
|
||||
'/var/etc/acme-client',
|
||||
__DIR__
|
||||
];
|
||||
foreach ($paths as $path) {
|
||||
if (is_dir($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return self::requireThat(false, "No config path");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $automation_id the automation numeric id or UUID.
|
||||
* @return mixed|null the automation action when found.
|
||||
*/
|
||||
public static function getAutomationActionById($automation_id)
|
||||
{
|
||||
$config = \OPNsense\Core\Config::getInstance()->object();
|
||||
$client = $config->OPNsense->AcmeClient;
|
||||
|
||||
foreach ($client->actions->children() as $action) {
|
||||
if (
|
||||
$automation_id === (string)$action->attributes()["uuid"]
|
||||
|| $automation_id === (string)$action->id
|
||||
) {
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print CLI help.
|
||||
* @see Utils::runCLIMain
|
||||
* @param string $about about text
|
||||
* @param string $examples examples text
|
||||
* @param array $commands the commands
|
||||
* @return void
|
||||
*/
|
||||
public static function printCLIHelp($about, $examples, $commands = [])
|
||||
{
|
||||
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)",
|
||||
];
|
||||
|
||||
echo $about . PHP_EOL
|
||||
. "Usage: " . basename($GLOBALS["argv"][0]) . " [options] [--command=]COMMAND" . PHP_EOL
|
||||
. PHP_EOL . join(PHP_EOL, $options) . PHP_EOL;
|
||||
|
||||
foreach ($commands as $name => $cmd) {
|
||||
echo PHP_EOL . "COMMAND \"$name\" {$cmd["description"]}" . PHP_EOL . "Options:" . PHP_EOL;
|
||||
foreach ($cmd["options"] as $option) {
|
||||
$option = preg_replace(['/^([^:]+)$/', '/(.+)::$/', '/(.+):$/'], ['[$1]', '[$1=value]', '$1=value'], "--$option");
|
||||
echo " $option" . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
echo PHP_EOL . "Examples:" . PHP_EOL
|
||||
. preg_replace('/\r\n|\n|\r/', PHP_EOL, $examples)
|
||||
. PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper that implements `main();` for a CLI application following the command design.
|
||||
*
|
||||
* `$commands` follows the format:
|
||||
* ```php
|
||||
* [
|
||||
* "command-name" => [
|
||||
* "description" => "...",
|
||||
* "options" => ["arg1::", "arg2::", "arg3::"],
|
||||
* "implementation" => "commandImplementationFunction",
|
||||
* "default" => true | false,
|
||||
* ],
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param callable $help method that display's CLI help.
|
||||
* @param callable $optionsByActionId method that returns CLI args (assoc array) from an automation action id.
|
||||
* @param array $commands the list of commands that the CLI application can execute.
|
||||
* @param int $exit_success exit code for success
|
||||
* @param int $exit_unknown_command exit code for no matching command
|
||||
* @return void
|
||||
*/
|
||||
public static function runCLIMain(callable $help, callable $optionsByActionId, $commands = [], $exit_success = 0, $exit_unknown_command = 255)
|
||||
{
|
||||
global $argv;
|
||||
$command = self::getSelectedCLICommand($commands);
|
||||
$options = ["help", "log", "no-error"];
|
||||
|
||||
$has_automation_id = preg_match('/--automation-id=\S+/', join(" ", $argv));
|
||||
if ($has_automation_id) {
|
||||
$options = array_merge($options, ["automation-id:", "certificates::"]);
|
||||
} else {
|
||||
$options = array_merge($options, $command["options"]);
|
||||
}
|
||||
|
||||
$index = 0;
|
||||
if ($options = getopt("h", $options, $index)) {
|
||||
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");
|
||||
});
|
||||
|
||||
if (isset($options["automation-id"])) {
|
||||
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"]}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_callable($runner = $command["implementation"])) {
|
||||
$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));
|
||||
}
|
||||
|
||||
exit(isset($options["no-error"]) ? $exit_success : $code);
|
||||
} else {
|
||||
exit($exit_unknown_command);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (count($argv) < 2) {
|
||||
$help();
|
||||
} else {
|
||||
$cmd = join(" ", $argv);
|
||||
self::log()->error("Parsing of '$cmd' failed at argument '{$argv[$index]}'");
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static function getSelectedCLICommand($commands = [])
|
||||
{
|
||||
$default = null;
|
||||
$command = null;
|
||||
$parsed_args = getopt("", ["command::"]);
|
||||
foreach ($commands as $name => $cmd) {
|
||||
if (in_array($name, $GLOBALS["argv"]) || ($parsed_args["command"] ?? "") === $name) {
|
||||
$command = $cmd;
|
||||
}
|
||||
if (($cmd["default"] ?? false) === true) {
|
||||
$default = $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
return $command ?? $default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,6 +1113,7 @@
|
||||
<configd_restart_nginx>Restart Nginx (OPNsense plugin)</configd_restart_nginx>
|
||||
<configd_upload_highwinds>Upload certificate to Highwinds CDN</configd_upload_highwinds>
|
||||
<configd_upload_sftp>Upload certificate via SFTP</configd_upload_sftp>
|
||||
<configd_remote_ssh>Remote Command via SSH</configd_remote_ssh>
|
||||
<acme_fritzbox>Upload certificate to FRITZ!Box router</acme_fritzbox>
|
||||
<acme_synology_dsm>Upload certificate to Synology DSM</acme_synology_dsm>
|
||||
<acme_unifi>Update local Unifi keystore</acme_unifi>
|
||||
@@ -1205,6 +1206,43 @@
|
||||
<ValidationMessage>Should be a string between 1 and 255 characters.
|
||||
Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'.</ValidationMessage>
|
||||
</sftp_filename_fullchain>
|
||||
<remote_ssh_host type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^.{1,255}$/u</mask>
|
||||
<ValidationMessage>Should be a string between 1 and 255 characters.</ValidationMessage>
|
||||
</remote_ssh_host>
|
||||
<remote_ssh_host_key type="TextField">
|
||||
<Required>N</Required>
|
||||
<!-- Key format: (comment)? key-type :SPACE: key-base64 (:SPACE: comment)?
|
||||
Reference: https://stackoverflow.com/a/475217 -->
|
||||
<mask>/^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i</mask>
|
||||
<ValidationMessage>Should be a valid public SSH host key (see "known_hosts").</ValidationMessage>
|
||||
</remote_ssh_host_key>
|
||||
<remote_ssh_port type="IntegerField">
|
||||
<Required>N</Required>
|
||||
<MinimumValue>1</MinimumValue>
|
||||
<MaximumValue>49151</MaximumValue>
|
||||
<default>22</default>
|
||||
<ValidationMessage>Should be a valid port number between 1 and 49151.</ValidationMessage>
|
||||
</remote_ssh_port>
|
||||
<remote_ssh_user type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^.{1,128}$/u</mask>
|
||||
<ValidationMessage>Should be a string between 1 and 128 characters.</ValidationMessage>
|
||||
</remote_ssh_user>
|
||||
<remote_ssh_identity_type type="OptionField">
|
||||
<Required>N</Required>
|
||||
<OptionValues>
|
||||
<ecdsa>ECDSA</ecdsa>
|
||||
<rsa>RSA</rsa>
|
||||
<ed25519>ed25519</ed25519>
|
||||
</OptionValues>
|
||||
</remote_ssh_identity_type>
|
||||
<remote_ssh_command type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^.{1,1024}$/u</mask>
|
||||
<ValidationMessage>Should be a shell command between 1 and 1024 characters.</ValidationMessage>
|
||||
</remote_ssh_command>
|
||||
<!-- old value, should be removed in next major release -->
|
||||
<configd type="ConfigdActionsField">
|
||||
<filters>
|
||||
|
||||
@@ -92,17 +92,21 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
.hide();
|
||||
}
|
||||
|
||||
// SFTP - Identity show button
|
||||
(function ($identityType) {
|
||||
// SFTP/SSH - Identity show button
|
||||
[
|
||||
{selector: '#action\\.sftp_identity_type', group: "configd_upload_sftp", action: "sftpGetIdentity"},
|
||||
{selector: '#action\\.remote_ssh_identity_type', group: "configd_remote_ssh", action: "sshGetIdentity"},
|
||||
].forEach(function(config) {
|
||||
var $identityType = $(config.selector);
|
||||
var identityDiv = makeStatusDiv($identityType);
|
||||
|
||||
makeButton("{{ lang._('Show Identity') }}", "configd_upload_sftp", "btn-info")
|
||||
makeButton("{{ lang._('Show Identity') }}", config.group, "btn-info")
|
||||
.click(function () {
|
||||
identityDiv.hide();
|
||||
var button = $(this);
|
||||
button.prop('disabled', true).find(".fa-spinner").show();
|
||||
|
||||
ajaxCall("/api/acmeclient/actions/sftpGetIdentity", getFormData("DialogAction").action, function (data, status) {
|
||||
ajaxCall("/api/acmeclient/actions/" + config.action, getFormData("DialogAction").action, function (data, status) {
|
||||
button.prop('disabled', false).find(".fa-spinner").hide();
|
||||
|
||||
if (status === "success" && data.status === "ok") {
|
||||
@@ -117,10 +121,15 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
$identityType.change(function() {
|
||||
identityDiv.hide();
|
||||
});
|
||||
})($('#action\\.sftp_identity_type'));
|
||||
});
|
||||
|
||||
// SFTP/SSH - Connection test button
|
||||
[
|
||||
{selector: '#action\\.sftp_user', group: "configd_upload_sftp", action: "sftpTestConnection", success: "{{ lang._('Connection and upload test succeeded.') }}"},
|
||||
{selector: '#action\\.remote_ssh_user', group: "configd_remote_ssh", action: "sshTestConnection", success: "{{ lang._('Connection test succeeded.') }}"},
|
||||
].forEach(function(config) {
|
||||
var $user = $(config.selector);
|
||||
|
||||
// SFTP - Connection test button
|
||||
(function ($user) {
|
||||
var statusDiv = makeStatusDiv($user, 'alert-success').html(
|
||||
'<div class="message"></div>'
|
||||
+ '<div class="detail-enabler" style="cursor: pointer"><i class="fa fa-plus-square"></i></div>'
|
||||
@@ -145,13 +154,13 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
{msg: "{{ lang._('Test failed, see details.') }}"},
|
||||
];
|
||||
|
||||
makeButton("{{ lang._('Test Connection') }}", "configd_upload_sftp")
|
||||
makeButton("{{ lang._('Test Connection') }}", config.group)
|
||||
.click(function () {
|
||||
statusDiv.hide();
|
||||
var button = $(this);
|
||||
button.prop('disabled', true).find(".fa-spinner").show();
|
||||
|
||||
ajaxCall("/api/acmeclient/actions/sftpTestConnection", getFormData("DialogAction").action, function (data, status) {
|
||||
ajaxCall("/api/acmeclient/actions/" + config.action, getFormData("DialogAction").action, function (data, status) {
|
||||
button.prop('disabled', false).find(".fa-spinner").hide();
|
||||
|
||||
var message = "",
|
||||
@@ -161,7 +170,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
if (status === "success") {
|
||||
if (data.success === true) {
|
||||
statusClass = "alert-success";
|
||||
message = "{{ lang._('Connection and upload test succeeded.') }}"
|
||||
message = config.success
|
||||
} else {
|
||||
detail = JSON.stringify(data, null, ' ').replace(/\\"/g, "'");
|
||||
|
||||
@@ -188,7 +197,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
statusDiv.removeClass("alert-success alert-warning").addClass(statusClass).show();
|
||||
});
|
||||
});
|
||||
})($('#action\\.sftp_user'));
|
||||
});
|
||||
|
||||
// Eagerly hiding method tables to avoid contents popping up when opening the dialog for the first time.
|
||||
$(".method_table").hide();
|
||||
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
#!/usr/local/bin/php
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Copyright (C) 2022 Juergen Kellerer
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
const ABOUT = <<<TXT
|
||||
|
||||
This script implements remote command execution using SSH. It reuses
|
||||
identities and "known_hosts" management from SFTP, located inside the
|
||||
local configuration folder (see "upload_sftp.php" for details).
|
||||
|
||||
Primary purpose is to support the creation of automation tasks that trigger
|
||||
actions after a certificate has been uploaded (requires that actions can
|
||||
have an execution order).
|
||||
|
||||
In addition to automations, all operations can also be triggered manually
|
||||
using simple CLI commands.
|
||||
|
||||
See: EXAMPLES & actions_acmeclient.conf
|
||||
|
||||
TXT;
|
||||
|
||||
// Commands & help
|
||||
const COMMANDS = [
|
||||
"run-command" => [
|
||||
"description" => "runs the a command on the specified target host",
|
||||
"options" => [
|
||||
"host::", "port::", "host-key::", "user::", "identity-type::", "run::"],
|
||||
"implementation" => "commandRunRemote",
|
||||
"default" => true,
|
||||
],
|
||||
|
||||
"test-connection" => [
|
||||
"description" => "connects to the host and returns results as JSON",
|
||||
"options" => ["host:", "port::", "host-key::", "user:", "identity-type::"],
|
||||
"implementation" => "commandTestConnection",
|
||||
],
|
||||
|
||||
"show-identity" => [
|
||||
"description" => "prints the ssh client identity (publickey)",
|
||||
"options" => ["identity-type::", "source-ip::", "host::", "unrestricted"],
|
||||
"implementation" => "commandShowIdentity",
|
||||
],
|
||||
];
|
||||
|
||||
const EXAMPLES = <<<TXT
|
||||
- Show the public key used to communicate with the SSH server
|
||||
./run_remote_ssh.php --log --identity-type=ecdsa show-identity
|
||||
|
||||
- Test connectivity with host
|
||||
./run_remote_ssh.php --log --host=sshpserver --user=name test-connection
|
||||
|
||||
- Run a command at the specific server
|
||||
./run_remote_ssh.php --log --host=sshserver --user=name --run='/bin/sh -c "pwd && ls -la"'
|
||||
|
||||
- Load settings from automation with ID and run the command
|
||||
./run_remote_ssh.php --log --automation-id=ID
|
||||
TXT;
|
||||
|
||||
// Connection test
|
||||
const CONNECTION_TEST_RESULT = 'OpnSense_ACME_SSH_Connected';
|
||||
const CONNECTION_TEST_COMMAND = 'echo "' . CONNECTION_TEST_RESULT . '"';
|
||||
|
||||
const CONNECTION_EXECUTE_TIMEOUT = 60 * 7; // Max seconds that a command may run
|
||||
|
||||
// Exit codes
|
||||
const EXITCODE_SUCCESS = 0;
|
||||
const EXITCODE_ERROR = 1;
|
||||
const EXITCODE_ERROR_UNKNOWN_COMMAND = 254;
|
||||
|
||||
// Optional imports
|
||||
@include_once("config.inc");
|
||||
@include_once("certs.inc");
|
||||
@include_once("util.inc");
|
||||
|
||||
// Optional autoloader (for local dev environment)
|
||||
if (!function_exists("log_error")) {
|
||||
spl_autoload_register(function ($class_name) {
|
||||
require_once(__DIR__ . "/../../../mvc/app/library/" . str_replace("\\", "/", $class_name) . ".php");
|
||||
});
|
||||
}
|
||||
|
||||
// Importing classes
|
||||
use OPNsense\AcmeClient\Process;
|
||||
use OPNsense\AcmeClient\SSHKeys;
|
||||
use OPNsense\AcmeClient\Utils;
|
||||
|
||||
// Implementing logic
|
||||
function commandShowIdentity(array &$options): int
|
||||
{
|
||||
$identity_type = trim(($options["identity-type"] ?? "")) ?: SSHKeys::DEFAULT_IDENTITY_TYPE;
|
||||
$source_ip = trim(($options["source-ip"] ?? ""));
|
||||
$host = trim(($options["host"] ?? ""));
|
||||
|
||||
$keys = new SSHKeys(configPath());
|
||||
if (($id_file = $keys->getIdentity($identity_type)) && is_readable($id_file)) {
|
||||
if (
|
||||
!isset($options["unrestricted"])
|
||||
&& ($restrictions = SSHKeys::getIdentityRestrictions($host, $source_ip, ""))
|
||||
) {
|
||||
echo "$restrictions ";
|
||||
}
|
||||
|
||||
echo file_get_contents($id_file);
|
||||
return EXITCODE_SUCCESS;
|
||||
} else {
|
||||
Utils::log()->error("Failed getting identity. See log output for details.");
|
||||
}
|
||||
return EXITCODE_ERROR;
|
||||
}
|
||||
|
||||
function commandTestConnection(array &$options): int
|
||||
{
|
||||
$result = ["actions" => ["connecting"], "success" => false];
|
||||
|
||||
$options["run"] = CONNECTION_TEST_COMMAND;
|
||||
$lines = runRemoteCommand($options, $error);
|
||||
|
||||
if (!$error) {
|
||||
$result["actions"][] = "connected";
|
||||
if (($result["success"] = in_array(CONNECTION_TEST_RESULT, $lines))) {
|
||||
$result["actions"][] = "echo-tested";
|
||||
}
|
||||
} else {
|
||||
$result = array_merge($result, ($error ?: []));
|
||||
}
|
||||
|
||||
echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
|
||||
|
||||
return $result["success"] ? EXITCODE_SUCCESS : EXITCODE_ERROR;
|
||||
}
|
||||
|
||||
function commandRunRemote(array &$options): int
|
||||
{
|
||||
if (empty($options["run"])) {
|
||||
Utils::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));
|
||||
return EXITCODE_SUCCESS;
|
||||
}
|
||||
|
||||
return EXITCODE_ERROR;
|
||||
}
|
||||
|
||||
function runRemoteCommand(array $options, &$error): ?array
|
||||
{
|
||||
static $expected_errors = [
|
||||
["host_not_resolved", /* -> */ '/.*not resolve.*/i'],
|
||||
["host_not_trusted", /* -> */ '/.*IDENTIFICATION HAS CHANGED.*/i'],
|
||||
["connection_refused", /* -> */ '/.*connection refused.*/i'],
|
||||
["connection_closed", /* -> */ '/.*connection closed.*/i'],
|
||||
["network_timeout", /* -> */ '/.*timed out.*/i'],
|
||||
["network_unreachable", /* -> */ '/.*network.+unreachable.*/i'],
|
||||
["permission_denied", /* -> */ '/.*permission denied.*/i'],
|
||||
["failure", /* -> */ '/.*(error|failure|you must supply).*/i'],
|
||||
];
|
||||
|
||||
$ssh_keys = new SSHKeys(configPath());
|
||||
|
||||
$identity_type = trim(($options["identity-type"] ?? ""));
|
||||
$host = trim(($options["host"] ?? ""));
|
||||
$host_key = ($options["host-key"] ?? "");
|
||||
$port = $options["port"] ?? 22;
|
||||
$username = $options["user"] ?? false;
|
||||
$command = $options["run"] ?? "";
|
||||
|
||||
list($ok, $cmd) = buildSSHArguments($ssh_keys, $host, $username, $identity_type, $host_key, $port);
|
||||
if ($ok) {
|
||||
if (empty($command)) {
|
||||
$error = ["no_command" => true];
|
||||
} else {
|
||||
$cmd[] = $command;
|
||||
}
|
||||
} else {
|
||||
$error = $cmd;
|
||||
$error["connect_failed"] = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$exit_code = null;
|
||||
$expected_error = null;
|
||||
|
||||
if ($process = Process::open($cmd)) {
|
||||
$process->closeInput();
|
||||
|
||||
$lines = 0;
|
||||
$start = time();
|
||||
$mustClose = fn($lines) => (time() - $start) > CONNECTION_EXECUTE_TIMEOUT || $lines > 10000;
|
||||
|
||||
while ($process->isRunning() && !$mustClose($lines)) {
|
||||
for (; ($line = $process->get()) !== false && !$mustClose($lines); $lines++) {
|
||||
if (!$expected_error) {
|
||||
foreach ($expected_errors as $ee) {
|
||||
if (preg_match($ee[1], $line)) {
|
||||
if ($ee[0] !== "connection_closed") {
|
||||
$expected_error = [$ee[0] => true, "error" => trim($line)];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$result[] = $line;
|
||||
}
|
||||
}
|
||||
$exit_code = $process->close();
|
||||
$ok = $exit_code === 0;
|
||||
} else {
|
||||
$ok = false;
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
$cl = join(" ", array_map(fn($v) => escapeshellarg($v), $cmd));
|
||||
$error = array_merge(($expected_error ?? []), [
|
||||
"result" => $result,
|
||||
"exit_code" => $exit_code
|
||||
]);
|
||||
$error["connect_failed"] = $exit_code == 255;
|
||||
Utils::log()->error("SSH failed with '$exit_code': $cl", $error);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
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.");
|
||||
return [false, ["invalid_parameters" => true]];
|
||||
}
|
||||
|
||||
if (empty($identity_type)) {
|
||||
$identity_type = SSHKeys::DEFAULT_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"]}");
|
||||
unset($trust["ok"]);
|
||||
return [false, array_merge($trust, ["host_not_trusted" => true])];
|
||||
} else {
|
||||
$host = $trust["host"];
|
||||
}
|
||||
|
||||
// Building ssh command.
|
||||
$cmd = [
|
||||
"ssh",
|
||||
"-p", $port,
|
||||
"-oUser=$username",
|
||||
"-oUserKnownHostsFile={$ssh_keys->knownHostsFile()}",
|
||||
];
|
||||
|
||||
// Handle client side identity
|
||||
$identity = $ssh_keys->getIdentity($identity_type, true);
|
||||
if (is_file($identity) && is_readable($identity)) {
|
||||
array_push(
|
||||
$cmd,
|
||||
"-i",
|
||||
$identity,
|
||||
"-oPreferredAuthentications=publickey"
|
||||
);
|
||||
} else {
|
||||
Utils::log()->error("Failed adding client identity ($identity). Connect will likely fail.");
|
||||
}
|
||||
|
||||
// Adding the host
|
||||
$cmd[] = "$host";
|
||||
|
||||
return [true, $cmd];
|
||||
}
|
||||
|
||||
function help()
|
||||
{
|
||||
Utils::printCLIHelp(ABOUT, EXAMPLES, COMMANDS);
|
||||
}
|
||||
|
||||
function getOptionsById($automation_id)
|
||||
{
|
||||
Utils::log()->info("Reading options from automation: $automation_id");
|
||||
|
||||
if (is_object($action = Utils::getAutomationActionById($automation_id))) {
|
||||
if ($action->enabled && "configd_remote_ssh" === (string)$action->type) {
|
||||
return [
|
||||
"host" => trim((string)$action->remote_ssh_host),
|
||||
"host-key" => trim((string)$action->remote_ssh_host_key),
|
||||
"port" => trim((string)$action->remote_ssh_port),
|
||||
"identity-type" => trim((string)$action->remote_ssh_identity_type),
|
||||
"user" => trim((string)$action->remote_ssh_user),
|
||||
"run" => trim((string)$action->remote_ssh_command),
|
||||
];
|
||||
} else {
|
||||
Utils::log()->error("Ignoring disabled or invalid automation '$automation_id'");
|
||||
}
|
||||
} else {
|
||||
Utils::log()->error("No upload automation found with uuid = '$automation_id'");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function configPath(): string
|
||||
{
|
||||
if (($path = Utils::configPath())) {
|
||||
return $path . DIRECTORY_SEPARATOR . "sftp-config"; // shared with sftp to have the same identities
|
||||
}
|
||||
die("Failed detecting config path");
|
||||
}
|
||||
|
||||
function requireThat($expression, $message)
|
||||
{
|
||||
try {
|
||||
Utils::requireThat($message, $message);
|
||||
} catch (\AssertionError $e) {
|
||||
exit(EXITCODE_ERROR);
|
||||
}
|
||||
return $expression;
|
||||
}
|
||||
|
||||
// Running the main script
|
||||
Utils::runCLIMain(
|
||||
"help",
|
||||
"getOptionsById",
|
||||
COMMANDS,
|
||||
EXITCODE_SUCCESS,
|
||||
EXITCODE_ERROR_UNKNOWN_COMMAND
|
||||
);
|
||||
@@ -86,13 +86,6 @@ const COMMANDS = [
|
||||
],
|
||||
];
|
||||
|
||||
const STATIC_OPTIONS = <<<TXT
|
||||
-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)
|
||||
TXT;
|
||||
|
||||
const EXAMPLES = <<<TXT
|
||||
- Show the public key used to communicate with the SFTP server
|
||||
./upload_sftp.php --log --identity-type=ecdsa show-identity
|
||||
@@ -150,9 +143,9 @@ use OPNsense\AcmeClient\Utils;
|
||||
// Implementing logic
|
||||
function commandShowIdentity(array &$options): int
|
||||
{
|
||||
$identity_type = trim(($options["identity-type"] ?: SSHKeys::DEFAULT_IDENTITY_TYPE));
|
||||
$source_ip = trim(($options["source-ip"] ?: ""));
|
||||
$host = trim(($options["host"] ?: ""));
|
||||
$identity_type = trim(($options["identity-type"] ?? "")) ?: SSHKeys::DEFAULT_IDENTITY_TYPE;
|
||||
$source_ip = trim(($options["source-ip"] ?? ""));
|
||||
$host = trim(($options["host"] ?? ""));
|
||||
|
||||
$keys = new SSHKeys(configPath());
|
||||
if (($id_file = $keys->getIdentity($identity_type)) && is_readable($id_file)) {
|
||||
@@ -190,7 +183,7 @@ function commandTestConnection(array &$options): int
|
||||
|
||||
$uploader = new SftpUploader($sftp);
|
||||
|
||||
$chgrp = $options["chgrp"] ?: false;
|
||||
$chgrp = ($options["chgrp"] ?? "") ?: false;
|
||||
$chmod = isset($options["chmod"]) ? ($options["chmod"] ?: DEFAULT_CERT_MODE) : false;
|
||||
$filename = $uploader->addContent("upload-test", "", 0, $chmod, $chgrp);
|
||||
|
||||
@@ -275,7 +268,7 @@ function uploadCertificatesToHost(array $options): int
|
||||
$sftp = connectWithServer($options, $error);
|
||||
if ($sftp === null) {
|
||||
Utils::log()->error("Aborting after connect failure.");
|
||||
return $error["connect_failed"]
|
||||
return ($error["connect_failed"] ?? false)
|
||||
? EXITCODE_ERROR
|
||||
: EXITCODE_ERROR_NO_PERMISSION;
|
||||
}
|
||||
@@ -321,10 +314,10 @@ function uploadCertificatesToHost(array $options): int
|
||||
|
||||
function connectWithServer(array $options, &$error): ?SftpClient
|
||||
{
|
||||
$identity_type = trim(($options["identity-type"] ?: SSHKeys::DEFAULT_IDENTITY_TYPE));
|
||||
$host = trim(($options["host"] ?: ""));
|
||||
$host_key = ($options["host-key"] ?: "");
|
||||
$port = $options["port"] ?: 22;
|
||||
$identity_type = trim(($options["identity-type"] ?? "")) ?: SSHKeys::DEFAULT_IDENTITY_TYPE;
|
||||
$host = trim(($options["host"] ?? ""));
|
||||
$host_key = ($options["host-key"] ?? "");
|
||||
$port = $options["port"] ?? 22;
|
||||
$username = $options["user"];
|
||||
|
||||
$sftp = new SftpClient(configPath(), $identity_type);
|
||||
@@ -336,7 +329,7 @@ function connectWithServer(array $options, &$error): ?SftpClient
|
||||
}
|
||||
|
||||
// Apply start path (if one was specified, defaults to home dir)
|
||||
if (($remote_path = $options["remote-path"])) {
|
||||
if (!empty($remote_path = ($options["remote-path"] ?? ""))) {
|
||||
if ($err = $sftp->cd($remote_path)->lastError()) {
|
||||
$error = $err;
|
||||
$error["change_home_dir_failed"] = true;
|
||||
@@ -350,55 +343,7 @@ function connectWithServer(array $options, &$error): ?SftpClient
|
||||
|
||||
function help()
|
||||
{
|
||||
echo ABOUT . PHP_EOL
|
||||
. "Usage: " . basename($GLOBALS["argv"][0]) . " [options] [--command=]COMMAND" . PHP_EOL
|
||||
. PHP_EOL . STATIC_OPTIONS . PHP_EOL;
|
||||
|
||||
foreach (COMMANDS as $name => $cmd) {
|
||||
echo PHP_EOL . "COMMAND \"$name\" {$cmd["description"]}" . PHP_EOL . "Options:" . PHP_EOL;
|
||||
foreach ($cmd["options"] as $option) {
|
||||
$option = preg_replace(['/^([^:]+)$/', '/(.+)::$/', '/(.+):$/'], ['[$1]', '[$1=value]', '$1=value'], "--$option");
|
||||
echo " $option" . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
echo PHP_EOL . "Examples:" . PHP_EOL
|
||||
. str_replace('/\r\n|\n|\r/g', PHP_EOL, EXAMPLES)
|
||||
. PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
function getCommand()
|
||||
{
|
||||
$default = null;
|
||||
$command = null;
|
||||
$parsed_args = getopt("", ["command::"]);
|
||||
foreach (COMMANDS as $name => $cmd) {
|
||||
if (in_array($name, $GLOBALS["argv"]) || $parsed_args["command"] === $name) {
|
||||
$command = $cmd;
|
||||
}
|
||||
if ($cmd["default"] === true) {
|
||||
$default = $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
return $command ?: $default;
|
||||
}
|
||||
|
||||
function getActionById($automation_id)
|
||||
{
|
||||
$config = OPNsense\Core\Config::getInstance()->object();
|
||||
$client = $config->OPNsense->AcmeClient;
|
||||
|
||||
foreach ($client->actions->children() as $action) {
|
||||
if (
|
||||
$automation_id === (string)$action->attributes()["uuid"]
|
||||
|| $automation_id === (string)$action->id
|
||||
) {
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
Utils::printCLIHelp(ABOUT, EXAMPLES, COMMANDS);
|
||||
}
|
||||
|
||||
function getOptionsById($automation_id, $silent = false)
|
||||
@@ -407,7 +352,7 @@ function getOptionsById($automation_id, $silent = false)
|
||||
Utils::log()->info("Reading options from automation: $automation_id");
|
||||
}
|
||||
|
||||
if (is_object($action = getActionById($automation_id))) {
|
||||
if (is_object($action = Utils::getAutomationActionById($automation_id))) {
|
||||
if ($action->enabled && "configd_upload_sftp" === (string)$action->type) {
|
||||
return [
|
||||
"host" => trim((string)$action->sftp_host),
|
||||
@@ -439,7 +384,7 @@ 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;
|
||||
$chgrp = ($options["chgrp"] ?? "") ?: false;
|
||||
|
||||
if (isset($options["certificates"])) {
|
||||
$cert_ids = preg_split('/[,;\s]+/', $options["certificates"] ?: "", 0, PREG_SPLIT_NO_EMPTY);
|
||||
@@ -571,7 +516,7 @@ function findCertificates(array $certificate_ids_or_names, $load_content = true)
|
||||
return $result;
|
||||
}
|
||||
|
||||
function exportCertificates(array $cert_refids)
|
||||
function exportCertificates(array $cert_refids): array
|
||||
{
|
||||
$result = [];
|
||||
$config = OPNsense\Core\Config::getInstance()->object();
|
||||
@@ -597,72 +542,12 @@ function exportCertificates(array $cert_refids)
|
||||
|
||||
function configPath(): string
|
||||
{
|
||||
static $paths = [
|
||||
'/var/etc/acme-client',
|
||||
__DIR__
|
||||
];
|
||||
foreach ($paths as $path) {
|
||||
if (is_dir($path)) {
|
||||
return $path . DIRECTORY_SEPARATOR . 'sftp-config';
|
||||
}
|
||||
if (($path = Utils::configPath())) {
|
||||
return $path . DIRECTORY_SEPARATOR . "sftp-config";
|
||||
}
|
||||
die("Failed detecting config path");
|
||||
}
|
||||
|
||||
function main()
|
||||
{
|
||||
global $argv;
|
||||
$command = getCommand();
|
||||
$options = ["help", "log", "no-error"];
|
||||
|
||||
$has_automation_id = preg_match('/--automation-id=\S+/', join(" ", $argv));
|
||||
if ($has_automation_id) {
|
||||
$options = array_merge($options, ["automation-id:", "certificates::"]);
|
||||
} else {
|
||||
$options = array_merge($options, $command["options"]);
|
||||
}
|
||||
|
||||
$index = 0;
|
||||
if ($options = getopt("h", $options, $index)) {
|
||||
if (isset($options["h"]) || isset($options["help"])) {
|
||||
help();
|
||||
} else {
|
||||
if (isset($options["log"])) {
|
||||
Utils::log(true)->info("Logging to stdout enabled");
|
||||
}
|
||||
|
||||
$options = array_filter($options, function ($value) {
|
||||
return !is_string($value)
|
||||
|| (!empty($value = trim($value)) && $value !== "__default_value");
|
||||
});
|
||||
|
||||
if (isset($options["automation-id"])) {
|
||||
$options = array_merge(getOptionsById($options["automation-id"]), $options);
|
||||
}
|
||||
|
||||
if (is_callable($runner = $command["implementation"])) {
|
||||
$code = $runner($options);
|
||||
|
||||
if ($code != EXITCODE_SUCCESS) {
|
||||
Utils::log()->error("Command execution failed, exit code $code. Last input was: " . json_encode($options, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
exit(isset($options["no-error"]) ? EXITCODE_SUCCESS : $code);
|
||||
} else {
|
||||
exit(EXITCODE_ERROR_UNKNOWN_COMMAND);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (count($argv) < 2) {
|
||||
help();
|
||||
} else {
|
||||
$cmd = join(" ", $argv);
|
||||
Utils::log()->error("Parsing of '$cmd' failed at argument '{$argv[$index]}'");
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function requireThat($expression, $message)
|
||||
{
|
||||
try {
|
||||
@@ -674,4 +559,10 @@ function requireThat($expression, $message)
|
||||
}
|
||||
|
||||
// Running the main script
|
||||
main();
|
||||
Utils::runCLIMain(
|
||||
"help",
|
||||
"getOptionsById",
|
||||
COMMANDS,
|
||||
EXITCODE_SUCCESS,
|
||||
EXITCODE_ERROR_UNKNOWN_COMMAND
|
||||
);
|
||||
|
||||
@@ -120,6 +120,24 @@ parameters:--identity-type=%s --host=%s show-identity
|
||||
type:script_output
|
||||
message:prints the public key used to connect to sftp server
|
||||
|
||||
[run-remote-ssh-command]
|
||||
command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php
|
||||
parameters:--automation-id=%s
|
||||
type:script
|
||||
message:running a command on the ssh server
|
||||
|
||||
[test-remote-ssh-connection]
|
||||
command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php
|
||||
parameters:--host=%s --host-key=%s --port=%s --user=%s --identity-type=%s --no-error test-connection
|
||||
type:script_output
|
||||
message:testing connection to ssh server
|
||||
|
||||
[show-remote-ssh-identity]
|
||||
command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php
|
||||
parameters:--identity-type=%s --host=%s show-identity
|
||||
type:script_output
|
||||
message:prints the public key used to connect to ssh server
|
||||
|
||||
[reset-acme-client]
|
||||
command:/usr/bin/find /var/etc/acme-client/home /var/etc/acme-client/configs /var/etc/acme-client/certs /var/etc/acme-client/keys /var/etc/acme-client/accounts -type f -delete
|
||||
parameters:
|
||||
|
||||
Reference in New Issue
Block a user