import configparser import http.cookiejar import ssl import urllib.parse import urllib.request class Untangle: """ This class wraps one instance of Untangle. It gets initialized with some configuration information, and then provides the methods to retreive a backup. """ 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.timeout = s.get('timeout', 300) self.base_url = 'https://' + self.host + '/' # This never changes # Sanity check the numerical version. self.version = s.get('version', '14.1') if self.version not in ['9', '10', '11', '12', '13', '13.1', '14', '14.1']: msg = 'Invalid version "' + 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) # Sanity check the integer "timeout" parameter. We want to # bail if either the given parameter is not an integer, or if # it's negative. To handle both at the same time, we try to # parse an integer... timeout = s.get('timeout', 300) try: self.timeout = int(timeout) except: # ...and set self.timeout to a negative value if we can't... self.timeout = -1 # Now we check to see if the timeout value is negative. That # will happen if it was either negative to begin with, or # non-integer (and we set it negative). if self.timeout < 0: msg = 'Invalid value "' + timeout + '" for timeout ' 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. ssl_ctx = ssl.create_default_context() if not self.verify_cert: ssl_ctx.check_hostname = False ssl_ctx.verify_mode = ssl.CERT_NONE 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 open(self, url, data=None): """ A wrapper around ``self.opener.open()`` that uses our configurable socket "timeout" value. Without the timeout parameter, it seems we can wait forever in some corner cases. """ return self.opener.open(url, data, self.timeout) def login(self): """ Perform the HTTPS request to log in to the Untangle web admin UI. The resulting session cookie is stored by our ``self.opener``. """ 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.open(url, post_data) def get_backup(self): """ Version-agnostic get-me-a-backup method. Dispatches to the actual implementation based on ``self.version``. """ if self.version == '9': return self.get_backup_v9() elif self.version in ['10', '11', '12', '13']: # The procedure for v11, v12, or v13 is the same as for v10. return self.get_backup_v10() elif self.version in ['13.1', '14', '14.1']: # But the minor update v13.1 moved the backup URL. return self.get_backup_v13_1() else: raise ValueError('unknown version %s' % self.version) def get_backup_v9(self): """ Retrieve a backup from Untangle version 9. This requires two requests; the first just hits the page, and the second actually retrieves the backup file. Returns the binary HTTPS response (i.e. the file). """ url = self.base_url + '/webui/backup' post_vars = {'action': 'requestBackup'} post_data = urllib.parse.urlencode(post_vars).encode('ascii') self.open(url, post_data) url = self.base_url + 'webui/backup?action=initiateDownload' with self.open(url) as response: return response.read() def get_backup_v10(self): """ Retrieve a backup from Untangle version 10. Returns the binary HTTPS response (i.e. the file). """ url = self.base_url + '/webui/download' post_vars = {'type': 'backup'} post_data = urllib.parse.urlencode(post_vars).encode('ascii') with self.open(url, post_data) as response: return response.read() def get_backup_v13_1(self): """ Retrieve a backup from Untangle version 13.1. This differs from v13 (and v12, and v11,...) by only one word in the URL: "webui" becomes "admin". Returns the binary HTTPS response (i.e. the file). """ url = self.base_url + '/admin/download' post_vars = {'type': 'backup'} post_data = urllib.parse.urlencode(post_vars).encode('ascii') with self.open(url, post_data) as response: return response.read()