mirror of
https://github.com/netbirdio/plugins.git
synced 2026-05-22 18:44:07 -07:00
AcmeClient: Enhanced sftp upload with configurable filenames.
Filename templates for cert, key and ca-chain can be configured in the UI and user input is sanitized to ensure valid file names are produced. New and some existing fields are using "advanced" markers to reduce amount of fields visible in the default UI. Also fixed minor bugs including one with unicode characters in names.
This commit is contained in:
+31
@@ -56,6 +56,7 @@
|
||||
<label>SFTP Port</label>
|
||||
<type>text</type>
|
||||
<help>SFTP server port. Leave blank to use default "22".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_host_key</id>
|
||||
@@ -89,18 +90,48 @@
|
||||
<label>Permission (Public Keys)</label>
|
||||
<type>text</type>
|
||||
<help>Unix permission to apply to uploaded public keys. Leave blank to use default "0440".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_chmod_key</id>
|
||||
<label>Permission (Private Keys)</label>
|
||||
<type>text</type>
|
||||
<help>Unix permission to apply to uploaded private keys. Leave blank to use default "0400".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_chgrp</id>
|
||||
<label>Group</label>
|
||||
<type>text</type>
|
||||
<help>Unix group id to apply to all uploaded files. Leave blank to not change the group.</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_filename_cert</id>
|
||||
<label>Naming "cert.pem"</label>
|
||||
<type>text</type>
|
||||
<help>Name template for the public certificate.
|
||||
Placeholders "{{name}}" and "%s" are replaced by the name of the certificate being uploaded.
|
||||
Leave blank to use default "{{name}}/cert.pem".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_filename_key</id>
|
||||
<label>Naming "key.pem"</label>
|
||||
<type>text</type>
|
||||
<help>Name template for the certificate's private key.
|
||||
Placeholders "{{name}}" and "%s" are replaced by the name of the certificate being uploaded.
|
||||
Leave blank to use default "{{name}}/key.pem".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.sftp_filename_ca</id>
|
||||
<label>Naming "ca.pem"</label>
|
||||
<type>text</type>
|
||||
<help>Name template for the public certificate chain file.
|
||||
Placeholders "{{name}}" and "%s" are replaced by the name of the certificate being uploaded.
|
||||
Leave blank to use default "{{name}}/ca.pem".</help>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<label>Required Parameters</label>
|
||||
|
||||
@@ -804,6 +804,24 @@
|
||||
<mask>/^0[0-9]{3}$/u</mask>
|
||||
<ValidationMessage>A unix permission, 4 digits (e.g. 0400).</ValidationMessage>
|
||||
</sftp_chmod_key>
|
||||
<sftp_filename_cert type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui</mask>
|
||||
<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_cert>
|
||||
<sftp_filename_key type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui</mask>
|
||||
<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_key>
|
||||
<sftp_filename_ca type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui</mask>
|
||||
<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_ca>
|
||||
<configd type="ConfigdActionsField">
|
||||
<filters>
|
||||
<description>/^(?!.*(Let\'s\ Encrypt|acme|[fF]irmware))([\S\s]{1,255})/</description>
|
||||
|
||||
@@ -64,7 +64,8 @@ const COMMANDS = [
|
||||
"description" => "transfers certificates to the specified target host",
|
||||
"options" => [
|
||||
"host::", "port::", "host-key::", "user::", "identity-type::", "remote-path::",
|
||||
"certificates::", "files::", "chgrp::", "chmod::", "chmod-key::"],
|
||||
"certificates::", "files::", "chgrp::", "chmod::", "chmod-key::",
|
||||
"cert-name::", "key-name::", "ca-name::"],
|
||||
"implementation" => "commandUpload",
|
||||
"default" => true,
|
||||
],
|
||||
@@ -117,6 +118,13 @@ const KNOWN_HOSTS_FILE_CREATE_MODE = 0640;
|
||||
const DEFAULT_CERT_MODE = 0440;
|
||||
const DEFAULT_KEY_MODE = 0400;
|
||||
|
||||
// Names
|
||||
const UPLOAD_NAME_TEMPLATES = [
|
||||
"cert" => ["default" => "{{name}}/cert.pem", "option" => "cert-name"],
|
||||
"key" => ["default" => "{{name}}/key.pem", "option" => "key-name"],
|
||||
"ca" => ["default" => "{{name}}/ca.pem", "option" => "ca-name"],
|
||||
];
|
||||
|
||||
// Keys & bits
|
||||
const DEFAULT_KEY_TYPE = "ecdsa";
|
||||
|
||||
@@ -249,7 +257,7 @@ function uploadCertificatesToHost(array $options): int
|
||||
|
||||
$username = $options["user"];
|
||||
|
||||
$remote_home = $sftp->pwd();
|
||||
$remote_base_path = $sftp->pwd();
|
||||
$remote_files = [];
|
||||
$remote_path = ".";
|
||||
|
||||
@@ -272,22 +280,37 @@ function uploadCertificatesToHost(array $options): int
|
||||
}
|
||||
|
||||
// Changing remote directory if required.
|
||||
if (($dir = dirname($file["target"])) !== $remote_path) {
|
||||
$target_dir = $sftp->resolve($dir, $remote_home);
|
||||
if (($target_dir = dirname($file["target"])) !== $remote_path) {
|
||||
|
||||
$error = $sftp->cd($target_dir)->lastError();
|
||||
if ($error["file_not_found"]) {
|
||||
logger()->info("Creating remote directory: $target_dir");
|
||||
$sftp->clearError()
|
||||
->mkdir($target_dir)
|
||||
->cd($target_dir);
|
||||
$absolute_target_dir = $sftp->resolve($target_dir, $remote_base_path);
|
||||
requireThat(
|
||||
$absolute_target_dir && strpos($absolute_target_dir, $remote_base_path) === 0,
|
||||
"Illegal target directory '$absolute_target_dir' is not below '$remote_base_path'");
|
||||
|
||||
$dir_names = preg_split('-/+-', substr($absolute_target_dir, strlen($remote_base_path)), 0, PREG_SPLIT_NO_EMPTY);
|
||||
if (count($dir_names) == 1)
|
||||
$dir_names[0] = $absolute_target_dir; // Single dir: Use absolute path
|
||||
else
|
||||
$sftp->cd($remote_base_path); // None or multiple directories: Start from base path and create one by one as needed
|
||||
|
||||
foreach ($dir_names as $dir) {
|
||||
if ($error = $sftp->cd($dir)->lastError()) {
|
||||
if ($error["file_not_found"]) {
|
||||
logger()->info("Creating remote directory: $dir");
|
||||
$sftp->clearError()
|
||||
->mkdir($dir)
|
||||
->cd($dir);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($error = $sftp->lastError()) {
|
||||
logger()->error("Failed to cd into '$dir'. Cause: " . json_encode($error, JSON_UNESCAPED_SLASHES));
|
||||
logger()->error("Failed to cd into '$target_dir'. Cause: " . json_encode($error, JSON_UNESCAPED_SLASHES));
|
||||
return EXITCODE_ERROR;
|
||||
} else {
|
||||
$remote_path = $dir;
|
||||
$remote_path = $target_dir;
|
||||
$remote_files = $sftp->ls();
|
||||
if ($error = $sftp->lastError()) {
|
||||
logger()->error("Failed listing remote files. Cause: " . json_encode($error, JSON_UNESCAPED_SLASHES));
|
||||
@@ -316,7 +339,8 @@ function uploadCertificatesToHost(array $options): int
|
||||
|
||||
// Initial upload when permissions are properly set.
|
||||
if (!$remote_is_readonly) {
|
||||
if ($error = $sftp->put($file["source"], $remote_filename)->lastError()) {
|
||||
$preserve_times_and_mod = $file_chmod !== false;
|
||||
if ($error = $sftp->put($file["source"], $remote_filename, $preserve_times_and_mod)->lastError()) {
|
||||
logger()->error("Failed uploading file '{$file["source"]}' to '{$file["target"]}'. Cause: " . json_encode($error, JSON_UNESCAPED_SLASHES));
|
||||
|
||||
if ($error["permission_denied"] !== true)
|
||||
@@ -451,6 +475,9 @@ 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),
|
||||
"cert-name" => trim((string)$action->sftp_filename_cert),
|
||||
"key-name" => trim((string)$action->sftp_filename_key),
|
||||
"ca-name" => trim((string)$action->sftp_filename_ca),
|
||||
];
|
||||
} else if (!$silent) {
|
||||
logger()->error("Ignoring disabled or invalid automation '$automation_id'");
|
||||
@@ -482,12 +509,40 @@ function getFilesToUpload(array $options)
|
||||
if (($time = intval($cert["updated"])) && $time > 0)
|
||||
touch($source, $time);
|
||||
|
||||
// Sanitize user input (allow unicode chars, numbers and some special characters)
|
||||
$context_path = preg_replace('/[^\w\d_\-@.]+/uim', "-", $cert["name"]);
|
||||
// Build the upload name
|
||||
$target_path = requireThat(UPLOAD_NAME_TEMPLATES[$name], "No upload template defined for '{$name}'");
|
||||
$target_path = stripcslashes($options[$target_path["option"]] ?: $target_path["default"]);
|
||||
|
||||
$file = ["source" => $source, "target" => "{$context_path}/{$name}.pem"];
|
||||
$file["is_key"] = ($name === "key");
|
||||
$files[] = $file;
|
||||
$target_path = join("/", array_map(
|
||||
function ($path_part) use (&$cert) {
|
||||
// Replace template params "{{.+}}" & "%s"
|
||||
$path_part = preg_replace_callback(
|
||||
['/%s/', '/{{([^}]+?)}}/'],
|
||||
function ($m) use (&$cert) {
|
||||
$index = $m[0] == '%s' ? "name" : trim($m[1]);
|
||||
|
||||
return in_array($index, ["name", "id", "updated"])
|
||||
? stripcslashes($cert[$index])
|
||||
: "__unknown-template-param__{$index}__";
|
||||
},
|
||||
$path_part);
|
||||
|
||||
// Sanitize user input. Allow unicode chars, numbers and some special characters [_-@.].
|
||||
// Also replace all ".." with "." to avoid upwards tree traversal.
|
||||
return preg_replace(['/\.+/', '/[^\w\d_\-@.]+/uim'], ['.', '-'], trim($path_part));
|
||||
},
|
||||
preg_split('-[/\\\\]+-', $target_path, 0, PREG_SPLIT_NO_EMPTY)));
|
||||
|
||||
// Add the file to upload (if valid)
|
||||
if (!empty($target_path)
|
||||
&& preg_match('-^(?!/).+?(?<!/)$-', $target_path) /* must neither begin nor end with '/' */
|
||||
&& !preg_match('-^[/.]+$-', $target_path) /* must not only consist of '/' and '.' */) {
|
||||
$file = ["source" => $source, "target" => $target_path];
|
||||
$file["is_key"] = ($name === "key");
|
||||
$files[] = $file;
|
||||
} else {
|
||||
logger()->error("Cannot add '{$name}.pem' since the upload path '$target_path' is invalid.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger()->error("Ignoring upload for cert '{$cert["name"]}', since it is not available in trust storage.");
|
||||
@@ -534,9 +589,10 @@ function findCertificates(array $certificate_ids_or_names, $load_content = true)
|
||||
continue;
|
||||
}
|
||||
|
||||
$item["id"] = $id;
|
||||
$item["name"] = $name;
|
||||
$item["updated"] = intval($cert->lastUpdate);
|
||||
$item["automations"] = preg_split('/[\s*,]+/', $cert->restartActions);
|
||||
$item["automations"] = preg_split('/[\s,]+/', $cert->restartActions);
|
||||
if (isset($cert->certRefId)) {
|
||||
$refids[] = $item['content_id'] = (string)$cert->certRefId;
|
||||
}
|
||||
@@ -684,6 +740,7 @@ function requireThat($expression, $message)
|
||||
logger()->error("FATAL: $message");
|
||||
exit(EXITCODE_ERROR);
|
||||
}
|
||||
return $expression;
|
||||
}
|
||||
|
||||
function resolvePath($file, $base = ".")
|
||||
@@ -744,6 +801,8 @@ class SftpClient
|
||||
private $ssh_keys;
|
||||
/* @var null|Process */
|
||||
private $process = null;
|
||||
/* @var null|string */
|
||||
private $pwd = null;
|
||||
|
||||
public function __construct($config_path, $identity_type = DEFAULT_IDENTITY_TYPE)
|
||||
{
|
||||
@@ -808,7 +867,7 @@ class SftpClient
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processAvailableInput(float $timeout = 0, $expected_lines = 0, Callable $lines_consumer = null)
|
||||
private function processAvailableInput(float $timeout = 0, $expected_lines = 0, Callable $lines_consumer = null, $remaining_timeout = 0)
|
||||
{
|
||||
requireThat($this->process !== null, "SFTP: process not connected");
|
||||
|
||||
@@ -825,9 +884,6 @@ class SftpClient
|
||||
];
|
||||
|
||||
while (($line = $this->process->get($timeout)) !== false) {
|
||||
if (--$expected_lines <= 0)
|
||||
$timeout = 0;
|
||||
|
||||
foreach ($expected_errors as $ee) {
|
||||
if (preg_match($ee[1], $line)) {
|
||||
$this->failed_status = [$ee[0] => true, "error" => trim($line)];
|
||||
@@ -835,9 +891,13 @@ class SftpClient
|
||||
}
|
||||
}
|
||||
|
||||
$hide = ($lines_consumer && $lines_consumer($line) === true);
|
||||
if (!$hide)
|
||||
$consumed = ($lines_consumer && $lines_consumer($line) === true);
|
||||
if (!$consumed)
|
||||
logger()->info("SFTP: " . rtrim($line));
|
||||
|
||||
if (!$lines_consumer || $consumed) {
|
||||
if (--$expected_lines <= 0) $timeout = $remaining_timeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,7 +939,8 @@ class SftpClient
|
||||
$regex = '/^([bcdlsp\-][rwx\-]{9}[+@]?)\s+[0-9]+\s+([^\s]+)\s+([^\s]+)\s+([0-9]+)\s+(\w+\s+[0-9]+\s+[0-9:]+)\s+(.+)$/';
|
||||
$this->processAvailableInput(30, 2, function ($line) use (&$files, $regex) {
|
||||
if (preg_match($regex, $line, $matches)) {
|
||||
$files[trim($matches[6])] = [
|
||||
$filename = trim(stripcslashes($matches[6])); // decodes octal UTF-8 sequences
|
||||
$files[$filename] = [
|
||||
"type" => $matches[1][0],
|
||||
"permissions" => $matches[1],
|
||||
"owner" => $matches[2],
|
||||
@@ -890,21 +951,30 @@ class SftpClient
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, 1);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
public function pwd()
|
||||
{
|
||||
$remote_path = false;
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("pwd");
|
||||
$this->processAvailableInput(30, 2, function ($line) use (&$remote_path) {
|
||||
if (preg_match('/^.+directory:\s(.+)$/i', $line, $matches))
|
||||
$remote_path = trim($matches[1]);
|
||||
});
|
||||
return $remote_path;
|
||||
if ($this->pwd === null) {
|
||||
$remote_path = false;
|
||||
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("pwd");
|
||||
$this->processAvailableInput(30, 1, function ($line) use (&$remote_path) {
|
||||
if (preg_match('/^.+directory:\s(.+)$/i', $line, $matches)) {
|
||||
$remote_path = trim(stripcslashes($matches[1]));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
$this->pwd = $remote_path;
|
||||
}
|
||||
|
||||
return $this->pwd;
|
||||
}
|
||||
|
||||
public function resolve($remote_path, $remote_pwd = null)
|
||||
@@ -920,16 +990,20 @@ class SftpClient
|
||||
public function get($remote_file, $local_file = "")
|
||||
{
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("get " . escapeshellarg($remote_file) . " " . (empty($local_file) ? "" : escapeshellarg($local_file)));
|
||||
$this->process->put("get "
|
||||
. escapeshellarg($remote_file)
|
||||
. (empty($local_file) ? "" : " " . escapeshellarg($local_file)));
|
||||
$this->processAvailableInput(30, 2);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function put($local_file, $remote_file = "")
|
||||
public function put($local_file, $remote_file = "", $preserve = true)
|
||||
{
|
||||
if (is_file($local_file)) {
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("put -p " . escapeshellarg($local_file) . " " . (empty($remote_file) ? "" : escapeshellarg($remote_file)));
|
||||
$this->process->put("put " . ($preserve ? "-p " : "")
|
||||
. escapeshellarg($local_file)
|
||||
. (empty($remote_file) ? "" : " " . escapeshellarg($remote_file)));
|
||||
$this->processAvailableInput(30, 2);
|
||||
} else {
|
||||
logger()->info("put: File $local_file doesn't exist.");
|
||||
@@ -957,6 +1031,7 @@ class SftpClient
|
||||
$this->processAvailableInput(30, 1);
|
||||
$error = $this->lastError();
|
||||
$pwd = false;
|
||||
$this->pwd = null;
|
||||
|
||||
if ($error || $remote_path !== ($pwd = $this->pwd())) {
|
||||
$this->failed_status = array_merge(($error ?: []), [
|
||||
@@ -970,25 +1045,31 @@ class SftpClient
|
||||
|
||||
public function chmod($remote_file, $mode)
|
||||
{
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("chmod " . escapeshellarg($mode) . " " . escapeshellarg($remote_file));
|
||||
$this->processAvailableInput(30, 2);
|
||||
if (($remote_file = $this->resolve($remote_file)) !== false) {
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("chmod " . escapeshellarg($mode) . " " . escapeshellarg($remote_file));
|
||||
$this->processAvailableInput(30, 2);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function chgrp($remote_file, $group_id)
|
||||
{
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("chgrp " . escapeshellarg($group_id) . " " . escapeshellarg($remote_file));
|
||||
$this->processAvailableInput(30, 2);
|
||||
if (($remote_file = $this->resolve($remote_file)) !== false) {
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("chgrp " . escapeshellarg($group_id) . " " . escapeshellarg($remote_file));
|
||||
$this->processAvailableInput(30, 2);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function rm($remote_file)
|
||||
{
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("rm " . escapeshellarg($remote_file));
|
||||
$this->processAvailableInput(30, 2);
|
||||
if (($remote_file = $this->resolve($remote_file)) !== false) {
|
||||
$this->processAvailableInput();
|
||||
$this->process->put("rm " . escapeshellarg($remote_file));
|
||||
$this->processAvailableInput(30, 2);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1335,7 +1416,7 @@ class Process
|
||||
public $exitCode = null;
|
||||
|
||||
// Starts the specified process and returns an object to manage it.
|
||||
public static function open(array $cmd, $cwd = null, $env = null): Process
|
||||
public static function open(array $cmd, $cwd = null, $env = null): ?Process
|
||||
{
|
||||
global $__process_terminate_hook, $__open_processes;
|
||||
if (!is_array($__open_processes))
|
||||
|
||||
Reference in New Issue
Block a user