Skip to content

Instantly share code, notes, and snippets.

@cretl
Last active September 4, 2024 20:13
Show Gist options
  • Save cretl/9399900b4e623de4fcaab76592508ed0 to your computer and use it in GitHub Desktop.
Save cretl/9399900b4e623de4fcaab76592508ed0 to your computer and use it in GitHub Desktop.
OPNsense SFTP backup plugin
<?php
///usr/local/opnsense/mvc/app/library/OPNsense/Backup/SFTP.php
/*
* Copyright (C) 2018 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Backup;
use OPNsense\Core\Config;
use OPNsense\Backup\SFTPSettings;
/**
* Class SFTP backup
* @package OPNsense\Backup
*/
class SFTP extends Base implements IBackupProvider
{
/**
* get required (user interface) fields for backup connector
* @return array configuration fields, types and description
*/
public function getConfigurationFields()
{
$fields = array(
array(
"name" => "enabled",
"type" => "checkbox",
"label" => gettext("Enable"),
"value" => null
),
array(
"name" => "host",
"type" => "text",
"label" => gettext("SFTP Host"),
"help" => gettext("The hostname or IP address of the SFTP server"),
"value" => null
),
array(
"name" => "port",
"type" => "text",
"label" => gettext("Port"),
"help" => gettext("The port of the SFTP server, usually 22"),
"value" => '22'
),
array(
"name" => "user",
"type" => "text",
"label" => gettext("User Name"),
"help" => gettext("The username to log into the SFTP server"),
"value" => null
),
array(
"name" => "private_key",
"type" => "textarea",
"label" => gettext("Private Key"),
"help" => gettext("The private key for SFTP authentication in OpenSSH format"),
"value" => null
),
array(
"name" => "backupdir",
"type" => "text",
"label" => gettext("Remote Backup Directory"),
"value" => 'OPNsense-Backup'
)
);
$sftp = new SFTPSettings();
foreach ($fields as &$field) {
$field['value'] = (string)$sftp->getNodeByReference($field['name']);
}
return $fields;
}
/**
* backup provider name
* @return string user friendly name
*/
public function getName()
{
return gettext("SFTP");
}
/**
* validate and set configuration
* @param array $conf configuration array
* @return array of validation errors when not saved
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function setConfiguration($conf)
{
$sftp = new SFTPSettings();
$this->setModelProperties($sftp, $conf);
$validation_messages = $this->validateModel($sftp);
if (empty($validation_messages)) {
$sftp->serializeToConfig();
Config::getInstance()->save();
}
return $validation_messages;
}
/**
* perform backup
* @return array filelist
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function backup()
{
$cnf = Config::getInstance();
$sftp = new SFTPSettings();
if ($cnf->isValid() && !empty((string)$sftp->enabled)) {
$config = $cnf->object();
$host = (string)$sftp->host;
$port = (string)$sftp->port;
$username = (string)$sftp->user;
$private_key = (string)$sftp->private_key;
$backupdir = (string)$sftp->backupdir;
$hostname = $config->system->hostname . '.' . $config->system->domain;
$configname = 'config-' . $hostname . '-' . date('Y-m-d_H_i_s') . '.xml';
// backup source data to local strings
$confdata = file_get_contents('/conf/config.xml');
if (!empty((string)$sftp->password_encryption)) {
$confdata = $this->encrypt($confdata, (string)$sftp->password_encryption);
}
// Remove carriage return from private key string
$private_key = str_replace("\r", "", $private_key);
// Create temporary files for private key and config data
$private_key_file = tempnam(sys_get_temp_dir(), 'sftp_key');
$confdata_file = tempnam(sys_get_temp_dir(), 'sftp_conf');
file_put_contents($private_key_file, $private_key);
file_put_contents($confdata_file, $confdata);
// SFTP command
$sftp_command = sprintf(
'sftp -o StrictHostKeyChecking=no -i %s -P %s %s@%s << EOF
mkdir %s
put %s %s/%s
EOF',
escapeshellarg($private_key_file),
escapeshellarg($port),
escapeshellarg($username),
escapeshellarg($host),
escapeshellarg($backupdir),
escapeshellarg($confdata_file),
escapeshellarg($backupdir),
escapeshellarg($configname)
);
exec($sftp_command, $fileputoutput, $return_var);
if ($return_var !== 0) {
syslog(LOG_ERR, "SFTP backup failed with command: $sftp_command");
return array();
}
$sftp_command = sprintf(
'sftp -o StrictHostKeyChecking=no -i %s -P %s %s@%s << EOF
ls -1 %s
EOF',
escapeshellarg($private_key_file),
escapeshellarg($port),
escapeshellarg($username),
escapeshellarg($host),
escapeshellarg($backupdir)
);
exec($sftp_command, $filelistoutput, $return_var);
if ($return_var !== 0) {
syslog(LOG_ERR, "SFTP backup failed with command: $sftp_command");
return array();
}
// Cleanup temporary files
unlink($private_key_file);
unlink($confdata_file);
return array_filter(
$filelistoutput,
function ($filename) {
$extension = pathinfo($filename, PATHINFO_EXTENSION);
return strtolower($extension) === 'xml';
}
);
}
}
/**
* Is this provider enabled
* @return boolean enabled status
* @throws \OPNsense\Base\ModelException
* @throws \ReflectionException
*/
public function isEnabled()
{
$sftp = new SFTPSettings();
return (string)$sftp->enabled === "1";
}
}
<?php
///usr/local/opnsense/mvc/app/models/OPNsense/Backup/SFTPSettings.php
/*
* Copyright (C) 2018 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Backup;
use OPNsense\Base\BaseModel;
/**
* Class SFTP
* @package Backup
*/
class SFTPSettings extends BaseModel
{
}
<model>
<mount>//system/backup/sftp</mount>
<version>1.0.0</version>
<description>OPNsense SFTP Backup Settings</description>
<items>
<enabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
</enabled>
<host type="TextField">
<Required>Y</Required>
<ValidationMessage>The host for the SFTP server must be set.</ValidationMessage>
<Constraints>
<check001>
<ValidationMessage>A host for the SFTP server must be set.</ValidationMessage>
<type>DependConstraint</type>
<addFields>
<field1>enabled</field1>
</addFields>
</check001>
</Constraints>
</host>
<port type="IntegerField">
<Required>Y</Required>
<default>22</default>
<ValidationMessage>The port for the SFTP server must be set.</ValidationMessage>
</port>
<user type="TextField">
<Required>Y</Required>
<ValidationMessage>The user for the SFTP server must be set.</ValidationMessage>
<Constraints>
<check001>
<ValidationMessage>A user for the SFTP server must be set.</ValidationMessage>
<type>DependConstraint</type>
<addFields>
<field1>enabled</field1>
</addFields>
</check001>
</Constraints>
</user>
<private_key type="TextField">
<Required>Y</Required>
<ValidationMessage>The private key for the SFTP server must be set.</ValidationMessage>
<Constraints>
<check001>
<ValidationMessage>A private key for the SFTP server must be set.</ValidationMessage>
<type>DependConstraint</type>
<addFields>
<field1>enabled</field1>
</addFields>
</check001>
</Constraints>
</private_key>
<backupdir type="TextField">
<Required>Y</Required>
<default>OPNsense-Backup</default>
<mask>/^([\w%+\-]+\/)*[\w+%\-]+$/</mask>
<ValidationMessage>The Backup Directory can only consist of alphanumeric characters, dash, underscores and slash. No leading or trailing slash.</ValidationMessage>
</backupdir>
</items>
</model>
@cretl
Copy link
Author

cretl commented Jun 16, 2024

File locations:
/usr/local/opnsense/mvc/app/library/OPNsense/Backup/SFTP.php
/usr/local/opnsense/mvc/app/models/OPNsense/Backup/SFTPSettings.php
/usr/local/opnsense/mvc/app/models/OPNsense/Backup/SFTPSettings.xml

@akail
Copy link

akail commented Sep 3, 2024

Thank you for writing this! I have migrated from Nextcloud to using SFPTGo and was looking for a way to backup opnsense there.

One problem I did run into was with the generation of the temporary ssh key. It keeps appending the DOS newlines character '^M' to every line which in turn breaks the sftp commands. Not really a PHP guy but was able to add some sed commands to fix the file on the first sftp commands run. Not sure how else to approach it though.

@cretl
Copy link
Author

cretl commented Sep 4, 2024

Hi there,
Thanks for the info. I can reproduce this problem on my setup. But this doesn't break the script in my setup. I added a code line to remove the carriage return from the private key string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment