#!/usr/bin/python3 """ Back up Untangle configurations over HTTPS. """ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser import configparser import http.cookiejar from os import chmod import ssl from sys import stderr import urllib.parse import urllib.request # Define a few exit codes. EXIT_OK = 0 EXIT_BACKUPS_FAILED = 1 class Untangle: def __init__(self, s): """ Initialize this Untangle object with a ConfigParser section. """ self.name = s.name self.host = s['host'] self.username = s.get('username', 'admin') self.password = s['password'] self.version = int(s.get('version', '11')) self.base_url = 'https://' + self.host + '/' # This never changes # Sanity check the numerical version. if self.version not in [9, 11]: msg = 'Invalid version "' + str(self.version) + '" ' msg += 'in section "' + s.name + '"' raise configparser.ParsingError(msg) # Sanity check the boolean verify_cert parameter. vc = s.get('verify_cert', 'False') if vc == 'True': self.verify_cert = True elif vc == 'False': self.verify_cert = False else: msg = 'Invalid value "' + vc + '" for verify_cert ' msg += 'in section "' + s.name + '"' raise configparser.ParsingError(msg) # # Finally, create a URL opener to make HTTPS requests. # # First, create a cookie jar that we'll attach to our URL # opener thingy. cj = http.cookiejar.CookieJar() cookie_proc = urllib.request.HTTPCookieProcessor(cj) # SSL mumbo jumbo to make it ignore the certificate's hostname # when verify_cert = False. if self.verify_cert: ssl_ctx = ssl.create_default_context() else: ssl_ctx = ssl._create_unverified_context() https_handler = urllib.request.HTTPSHandler(context=ssl_ctx) # Now Create a URL opener, and tell it to use our cookie jar # and SSL context. We keep this around for future requests. self.opener = urllib.request.build_opener(https_handler, cookie_proc) def login(self): login_path = 'auth/login?url=/setup/welcome.do&realm=Administrator' url = self.base_url + login_path post_vars = {'username': self.username, 'password': self.password } post_data = urllib.parse.urlencode(post_vars).encode('ascii') self.opener.open(url, post_data) def get_backup(self): if self.version == 9: return self.get_backup_v9() elif self.version == 11: return self.get_backup_v11() def get_backup_v9(self): url = self.base_url + '/webui/backup' post_vars = {'action': 'requestBackup'} post_data = urllib.parse.urlencode(post_vars).encode('ascii') self.opener.open(url, post_data) url = self.base_url + 'webui/backup?action=initiateDownload' with self.opener.open(url) as response: return response.read() def get_backup_v11(self): url = self.base_url + '/webui/download?type=backup' post_vars = {'type': 'backup'} post_data = urllib.parse.urlencode(post_vars).encode('ascii') with self.opener.open(url, post_data) as response: return response.read() # Create an argument parser using our docsctring as its description. parser = ArgumentParser(description = __doc__, formatter_class = ArgumentDefaultsHelpFormatter) parser.add_argument('-c', '--config-file', default='/etc/untangle-https-backup.ini', help='path to configuration file') args = parser.parse_args() # Default to success, change it if anything fails. status = EXIT_OK config = configparser.ConfigParser() config.read(args.config_file) for section in config.sections(): untangle = Untangle(config[section]) try: untangle.login() backup = untangle.get_backup() filename = untangle.name + '.backup' with open(filename, 'wb') as f: f.write(backup) chmod(filename, 0o600) except urllib.error.URLError as e: msg = untangle.name + ': ' + str(e.reason) msg += ' from ' + untangle.host print(msg, file=stderr) status = EXIT_BACKUPS_FAILED except urllib.error.HTTPError as e: msg = untangle.name + ': ' + 'HTTP error ' + str(e.code) msg += ' from ' + untangle.host print(msg, file=stderr) status = EXIT_BACKUPS_FAILED exit(status)