From e02090fba5b9138ff73c8683d73f3ca9ccdc25e3 Mon Sep 17 00:00:00 2001 From: Jiri Jaburek Date: Mon, 15 Jul 2024 17:07:50 +0200 Subject: [PATCH] add explitit .start() and .stop() functions to BackgroundHTTPServer This should hopefully be less confusing in terms of usage with a Context Manager. Signed-off-by: Jiri Jaburek --- hardening/anaconda/test.py | 11 +++--- lib/osbuild.py | 5 +-- lib/util/httpsrv.py | 57 +++++++++++++++++++++++------ lib/virt.py | 5 +-- scanning/disa-alignment/anaconda.py | 11 +++--- 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/hardening/anaconda/test.py b/hardening/anaconda/test.py index dbb214f4..f3834947 100755 --- a/hardening/anaconda/test.py +++ b/hardening/anaconda/test.py @@ -21,11 +21,12 @@ ks.add_package_group('Server with GUI') # host a HTTP server with a datastream and let the guest download it -srv = util.BackgroundHTTPServer(virt.NETWORK_HOST, 0) -oscap.unselect_rules(util.get_datastream(), 'remediation-ds.xml', remediation.excludes()) -srv.add_file('remediation-ds.xml') -with srv: - host, port = srv.server.server_address +with util.BackgroundHTTPServer(virt.NETWORK_HOST, 0) as srv: + oscap.unselect_rules(util.get_datastream(), 'remediation-ds.xml', remediation.excludes()) + srv.add_file('remediation-ds.xml') + + host, port = srv.start() + oscap_conf = { 'content-type': 'datastream', 'content-url': f'http://{host}:{port}/remediation-ds.xml', diff --git a/lib/osbuild.py b/lib/osbuild.py index b977ce62..d5cdc323 100644 --- a/lib/osbuild.py +++ b/lib/osbuild.py @@ -335,14 +335,13 @@ def create(self, *, blueprint=None, bp_verbatim=None, profile=None): # osbuild-composer doesn't support file:// repos, so host # the custom RPM on a HTTP server - srv = util.BackgroundHTTPServer('127.0.0.1', 0) + srv = stack.enter_context(util.BackgroundHTTPServer('127.0.0.1', 0)) srv.add_dir(repo, 'repo') - stack.enter_context(srv) + http_host, http_port = srv.start() # overwrite default Red Hat CDN host repos via a custom HTTP server repos = ComposerRepos() repos.add_host_repos() - http_host, http_port = srv.server.server_address repos.repos.append({ 'name': 'contest-rpmpack', 'baseurl': f'http://{http_host}:{http_port}/repo', diff --git a/lib/util/httpsrv.py b/lib/util/httpsrv.py index ac0744ac..7685ba3e 100644 --- a/lib/util/httpsrv.py +++ b/lib/util/httpsrv.py @@ -3,10 +3,10 @@ backed by (different) filesystem paths. Ie. - srv = BackgroundHTTPServer('127.0.0.1', 8080) - srv.add_file('/on/disk/file.txt', '/visible.txt') - srv.add_file('/on/disk/dir', '/somedir') - with srv: + with BackgroundHTTPServer('127.0.0.1', 8080) as srv: + srv.add_file('/on/disk/file.txt', '/visible.txt') + srv.add_file('/on/disk/dir', '/somedir') + srv.start() ... Any HTTP GET requests for '/visible.txt' will receive the contents of @@ -14,19 +14,28 @@ Any HTTP GET requests for '/somedir/aa/bb' will receive the contents of '/on/disk/dir/aa/bb' (or 404). -You can call 'srv.add_*' functions inside the context manager, however +You can call 'srv.add_*' functions even after calling .start(), however be aware that this creates a potential race condition with the request- handling code, so make sure nothing queries the server while new entries are being added. Port can also be specified as 0 (just like for Python's socketserver) which will cause a random unused port to be allocated by the OS kernel. -You can then retrieve it from server_address (just like socketserver): +You can then retrieve it from the return tuple of .start(): + + with BackgroundHTTPServer('127.0.0.1', 0) as srv: + host, port = srv.start() + # 'host' will be '127.0.0.1' with port being >0 + +You can also use the server without a context manager, assuming you take +care of stopping it (manually, via try/finally, etc.): srv = BackgroundHTTPServer('127.0.0.1', 0) - with srv: - host, port = srv.server.server_address - # 'port' here will be >0 + try: + host, port = srv.start() + ... + finally: + srv.stop() """ import shutil @@ -79,6 +88,7 @@ def log_message(self, form, *args): class BackgroundHTTPServer: def __init__(self, host, port): + self.server = None self.file_mapping = {} self.dir_mapping = {} self.requested_address = (host, port) @@ -120,7 +130,15 @@ def add_dir(self, fs_path, url_path=None): url_path = Path(fs_path) if url_path is None else Path(url_path.lstrip('/')) self.dir_mapping[url_path] = Path(fs_path) - def __enter__(self): + def start(self): + """ + Start the HTTP server - open a listening socket, start serving requests. + + The server can be shut down either by exiting a context manager (if it + was created by the context manager), or by calling .stop() manually. + + Returns a (host, port) tuple the server is listening on. + """ server = HTTPServer(self.requested_address, _BackgroundHTTPServerHandler) host, port = server.server_address @@ -145,11 +163,20 @@ def __enter__(self): ['firewall-cmd', f'--zone={zone}', f'--add-port={port}/tcp'], stdout=subprocess.DEVNULL, check=True) - self.server = server self.thread = threading.Thread(target=server.serve_forever) self.thread.start() - def __exit__(self, exc_type, exc_value, traceback): + self.server = server + return server.server_address + + def stop(self): + """ + Stop the HTTP server, close the listening socket. + """ + # if it was never started + if self.server is None: + return + host, port = self.server.server_address util.log(f"ending: {host}:{port}") @@ -162,3 +189,9 @@ def __exit__(self, exc_type, exc_value, traceback): util.subprocess_run( ['firewall-cmd', f'--zone={zone}', f'--remove-port={port}/tcp'], stdout=subprocess.DEVNULL, check=True) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.stop() diff --git a/lib/virt.py b/lib/virt.py index f9d86d2f..93510706 100644 --- a/lib/virt.py +++ b/lib/virt.py @@ -424,13 +424,12 @@ def install(self, location=None, kickstart=None, rpmpack=None, disk_format='raw' # host the custom RPM on a HTTP server, as Anaconda needs a YUM repo # to pull packages from - srv = util.BackgroundHTTPServer(NETWORK_HOST, 0) + srv = stack.enter_context(util.BackgroundHTTPServer(NETWORK_HOST, 0)) srv.add_dir(repo, 'repo') - stack.enter_context(srv) + http_host, http_port = srv.start() # now that we know the address/port of the HTTP server, add it to # the kickstart as well - http_host, http_port = srv.server.server_address kickstart.add_install_only_repo( 'contest-rpmpack', f'http://{http_host}:{http_port}/repo', diff --git a/scanning/disa-alignment/anaconda.py b/scanning/disa-alignment/anaconda.py index 11adc86f..df2e622a 100644 --- a/scanning/disa-alignment/anaconda.py +++ b/scanning/disa-alignment/anaconda.py @@ -14,11 +14,12 @@ ks = virt.translate_ssg_kickstart(shared.profile) # host a HTTP server with a datastream and let the guest download it -srv = util.BackgroundHTTPServer(virt.NETWORK_HOST, 0) -oscap.unselect_rules(util.get_datastream(), 'remediation-ds.xml', remediation.excludes()) -srv.add_file('remediation-ds.xml') -with srv: - host, port = srv.server.server_address +with util.BackgroundHTTPServer(virt.NETWORK_HOST, 0) as srv: + oscap.unselect_rules(util.get_datastream(), 'remediation-ds.xml', remediation.excludes()) + srv.add_file('remediation-ds.xml') + + host, port = srv.start() + oscap_conf = { 'content-type': 'datastream', 'content-url': f'http://{host}:{port}/remediation-ds.xml',