diff --git a/README.md b/README.md index e195524f7..c3b757df1 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ sysutils/apuled -- PC Engine APU LED control (development only) sysutils/cpu-microcode -- CPU microcode updates sysutils/dec-hw -- Deciso hardware specific information sysutils/dmidecode -- Display hardware information on the dashboard +sysutils/gdrive-backup -- Backup configurations using Google Drive sysutils/git-backup -- Track config changes using git sysutils/hw-probe -- Collect hardware diagnostics sysutils/lcdproc-sdeclcd -- LCDProc for SDEC LCD devices diff --git a/sysutils/gdrive-backup/Makefile b/sysutils/gdrive-backup/Makefile new file mode 100644 index 000000000..4372dbfc8 --- /dev/null +++ b/sysutils/gdrive-backup/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= gdrive-backup +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= Backup configurations using Google Drive +PLUGIN_DEPENDS= php${PLUGIN_PHP}-google-api-php-client +PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/sysutils/gdrive-backup/pkg-descr b/sysutils/gdrive-backup/pkg-descr new file mode 100644 index 000000000..68fc6ad30 --- /dev/null +++ b/sysutils/gdrive-backup/pkg-descr @@ -0,0 +1,4 @@ +This plugin adds a backup option using Google Drive. + +Due to the sensitive nature of the data being send to the backup, +we strongly advise to not use a public service to send backups to. diff --git a/sysutils/gdrive-backup/src/etc/inc/plugins.inc.d/gdrive.inc b/sysutils/gdrive-backup/src/etc/inc/plugins.inc.d/gdrive.inc new file mode 100644 index 000000000..bad2cfa6b --- /dev/null +++ b/sysutils/gdrive-backup/src/etc/inc/plugins.inc.d/gdrive.inc @@ -0,0 +1,40 @@ + gettext('Backup - Google Drive'), + 'section' => 'system.remotebackup', + 'id' => 'remotebackup', + ]]; +} diff --git a/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php new file mode 100644 index 000000000..ef1ffb437 --- /dev/null +++ b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php @@ -0,0 +1,148 @@ +client = new \Google_Client(); + + $service_account = [ + "type" => "service_account", + "private_key" => $certinfo['pkey'], + "client_email" => $client_id, + "client_id" => $client_id, + "auth_uri" => "https://accounts.google.com/o/oauth2/auth", + "token_uri" => "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url" => "https://www.googleapis.com/oauth2/v1/certs" + ]; + + $this->client->setAuthConfig($service_account); + $this->client->addScope("https://www.googleapis.com/auth/drive"); + $this->client->setApplicationName("OPNsense"); + + $this->service = new \Google_Service_Drive($this->client); + } + + /** + * retrieve directory listing + * @param $directoryId parent directory id + * @param $filename title/filename of object + * @return mixed list of files + */ + public function listFiles($directoryId, $filename = null) + { + $query = "'" . $directoryId . "' in parents "; + if ($filename != null) { + $query .= " and title in '" . $filename . "'"; + } + return $this->service->files->listFiles(['q' => $query, 'supportsAllDrives' => true]); + } + + + /** + * download a file by given GDrive file handle + * @param $fileHandle (object from listFiles) + * @return null|string + */ + public function download($fileHandle) + { + $response = $this->service->files->get($fileHandle->id, ['alt' => 'media', 'supportsAllDrives' => true]); + return $response->getBody()->getContents(); + } + + /** + * Upload file + * @param string $directoryId (parent id) + * @param string $filename + * @param string $content + * @param string $mimetype + * @return \Google_Service_Drive_DriveFile handle + */ + public function upload($directoryId, $filename, $content, $mimetype = 'text/plain') + { + + $file = new \Google_Service_Drive_DriveFile(); + $file->setName($filename); + $file->setDescription($filename); + $file->setMimeType('text/plain'); + $file->setParents([$directoryId]); + + $createdFile = $this->service->files->create($file, [ + 'data' => $content, + 'mimeType' => $mimetype, + 'uploadType' => 'media', + 'supportsAllDrives' => true + ]); + + return $createdFile; + } + + /** + * delete file + * @param $fileHandle (object from listFiles) + */ + public function delete($fileHandle) + { + $this->service->files->delete($fileHandle['id'], ['supportsAllDrives' => true]); + } +} diff --git a/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php new file mode 100644 index 000000000..089ff08cd --- /dev/null +++ b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php @@ -0,0 +1,306 @@ + "GDriveEnabled", + "type" => "checkbox", + "label" => gettext("Enable"), + "value" => null + ); + $fields[] = array( + "name" => "GDriveEmail", + "type" => "text", + "label" => gettext("Email Address"), + "help" => gettext("Client-ID in the Google cloud console"), + "value" => null + ); + $fields[] = array( + "name" => "GDriveP12key", + "type" => "file", + "label" => gettext("P12 key"), + "help" => sprintf( + gettext('You need a private key in p12 format to use Google Drive, ' . + 'instructions on how to acquire one can be found %shere%s.'), + '', + '' + ), + "value" => null + ); + $fields[] = array( + "name" => "GDriveFolderID", + "type" => "text", + "label" => gettext("Folder ID"), + "value" => null + ); + $fields[] = array( + "name" => "GDrivePrefixHostname", + "type" => "checkbox", + "label" => gettext("Prefix hostname to backupfile"), + "help" => gettext("Normally the config xml will be written as config-stamp.xml, with this option set " . + "the filename will use the systems host and domain name."), + "value" => null + ); + $fields[] = array( + "name" => "GDriveBackupCount", + "type" => "text", + "label" => gettext("Backup Count"), + "value" => 60 + ); + $fields[] = array( + "name" => "GDrivePassword", + "type" => "password", + "label" => gettext("Password"), + "value" => null + ); + $fields[] = array( + "name" => "GDrivePasswordConfirm", + "type" => "password", + "label" => gettext("Confirm"), + "value" => null + ); + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + foreach ($fields as &$field) { + $fieldname = $field['name']; + if (isset($config->system->remotebackup->$fieldname)) { + $field['value'] = (string)$config->system->remotebackup->$fieldname; + } elseif ( + $fieldname == "GDrivePasswordConfirm" && + isset($config->system->remotebackup->GDrivePassword) + ) { + $field['value'] = (string)$config->system->remotebackup->GDrivePassword; + } + } + } + + return $fields; + } + + /** + * backup provider name + * @return string user friendly name + */ + public function getName() + { + return gettext("Google Drive"); + } + + /** + * validate and set configuration + * @param array $conf configuration array + * @return array of validation errors when not saved + */ + public function setConfiguration($conf) + { + $input_errors = array(); + if ($conf['GDrivePasswordConfirm'] != $conf['GDrivePassword']) { + $input_errors[] = gettext("The supplied 'Password' and 'Confirm' field values must match."); + } + if (count($input_errors) == 0) { + $config = Config::getInstance()->object(); + if (!isset($config->system->remotebackup)) { + $config->system->addChild('remotebackup'); + } + foreach ($this->getConfigurationFields() as $field) { + $fieldname = $field['name']; + if ($field['type'] == 'file') { + if (!empty($conf[$field['name']])) { + $config->system->remotebackup->$fieldname = base64_encode($conf[$field['name']]); + } + } elseif ($field['name'] == 'GDrivePasswordConfirm') { + /* skip password confirm field */ + } elseif (!empty($conf[$field['name']])) { + $config->system->remotebackup->$fieldname = $conf[$field['name']]; + } else { + unset($config->system->remotebackup->$fieldname); + } + } + // remove private key when disabled + if ( + empty($config->system->remotebackup->GDriveEnabled) && + isset($config->system->remotebackup->GDriveP12key) + ) { + unset($config->system->remotebackup->GDriveP12key); + } + Config::getInstance()->save(); + } + + return $input_errors; + } + + /** + * @return array filelist + */ + public function backup() + { + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + if ( + isset($config->system->remotebackup) && isset($config->system->remotebackup->GDriveEnabled) + && !empty($config->system->remotebackup->GDriveEnabled) + ) { + if (!empty($config->system->remotebackup->GDrivePrefixHostname)) { + $fileprefix = (string)$config->system->hostname . "." . (string)$config->system->domain . "-"; + } else { + $fileprefix = "config-"; + } + try { + $client = new \Google\API\Drive(); + $client->login( + (string)$config->system->remotebackup->GDriveEmail, + (string)$config->system->remotebackup->GDriveP12key + ); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "error connecting to Google Drive"); + return array(); + } + + // backup source data to local strings (plain/encrypted) + $confdata = file_get_contents('/conf/config.xml'); + $confdata_enc = $this->encrypt($confdata, (string)$config->system->remotebackup->GDrivePassword); + + // read filelist ({prefix}*.xml) + try { + $files = $client->listFiles((string)$config->system->remotebackup->GDriveFolderID); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "error while fetching filelist from Google Drive"); + return array(); + } + + $configfiles = array(); + foreach ($files as $file) { + if (fnmatch("{$fileprefix}*.xml", $file['name'])) { + $configfiles[$file['name']] = $file; + } + } + krsort($configfiles); + + + // backup new file if changed (or if first in backup) + $target_filename = $fileprefix . time() . ".xml"; + if (count($configfiles) > 1) { + // compare last backup with current, only save new + try { + $bck_data_enc = $client->download($configfiles[array_keys($configfiles)[0]]); + if (strpos(substr($bck_data_enc, 0, 100), '---') !== false) { + // base64 string is wrapped into tags + $start_at = strpos($bck_data_enc, "---\n") + 4; + $end_at = strpos($bck_data_enc, "\n---"); + $bck_data_enc = substr($bck_data_enc, $start_at, ($end_at - $start_at)); + } + $bck_data = $this->decrypt( + $bck_data_enc, + (string)$config->system->remotebackup->GDrivePassword + ); + if ($bck_data == $confdata) { + $target_filename = null; + } + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "unable to download " . + $configfiles[array_keys($configfiles)[0]]->description . " from Google Drive (" . $e . ")"); + } + } + if (!is_null($target_filename)) { + syslog(LOG_NOTICE, "backup configuration as " . $target_filename); + try { + $configfiles[$target_filename] = $client->upload( + (string)$config->system->remotebackup->GDriveFolderID, + $target_filename, + $confdata_enc + ); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "unable to upload " . $target_filename . " to Google Drive (" . $e . ")"); + return array(); + } + + krsort($configfiles); + } + + // cleanup old files + if ( + isset($config->system->remotebackup->GDriveBackupCount) + && is_numeric((string)$config->system->remotebackup->GDriveBackupCount) + ) { + $fcount = 0; + foreach ($configfiles as $filename => $file) { + if ($fcount >= (string)$config->system->remotebackup->GDriveBackupCount) { + syslog(LOG_NOTICE, "remove " . $filename . " from Google Drive"); + try { + $client->delete($file); + } catch (Google_Service_Exception $e) { + syslog(LOG_ERR, "unable to remove " . $filename . " from Google Drive"); + } + } + $fcount++; + } + } + + // return filelist + return array_keys($configfiles); + } + } + + // not configured / issue, return empty list + return array(); + } + + /** + * Is this provider enabled + * @return boolean enabled status + */ + public function isEnabled() + { + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + return isset($config->system->remotebackup) && isset($config->system->remotebackup->GDriveEnabled) + && !empty($config->system->remotebackup->GDriveEnabled); + } + return false; + } +}