diff --git a/usr/lib/python3.8/vmmgr/actionqueue.py b/usr/lib/python3.8/vmmgr/actionqueue.py index 158a30b..35d87a4 100644 --- a/usr/lib/python3.8/vmmgr/actionqueue.py +++ b/usr/lib/python3.8/vmmgr/actionqueue.py @@ -119,6 +119,8 @@ class ActionQueue: app_queue.process() # If the actions finished without errors, restore nominal state by deleting the item from action list self.clear_action(app_name) + except (SystemExit, KeyboardInterrupt): + raise except BaseException as e: # If the action failed, store the exception and leave it in the list for manual clearance with self.lock: diff --git a/usr/lib/python3.8/vmmgr/net.py b/usr/lib/python3.8/vmmgr/net.py index ea80f9b..f7f7379 100644 --- a/usr/lib/python3.8/vmmgr/net.py +++ b/usr/lib/python3.8/vmmgr/net.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -import dns.exception import dns.resolver import requests import socket @@ -21,7 +20,7 @@ def get_local_ip(version=None): output = subprocess.run(['/sbin/ip', 'route', 'get', '1' if version == 4 else '2003::'], check=True, stdout=subprocess.PIPE).stdout.decode().split() # Get field right after 'src' return output[output.index('src')+1] - except: + except (subprocess.CalledProcessError, ValueError, IndexError): return None def get_external_ip(version): @@ -31,7 +30,7 @@ def get_external_ip(version): try: requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family return requests.get(paths.MYIP_URL, timeout=5).text - except: + except requests.RequestException: return None finally: requests.packages.urllib3.util.connection.allowed_gai_family = allowed_gai_family @@ -45,15 +44,8 @@ def resolve_ip(domain, query_type): # Resolve domain name using Google Public DNS try: return resolver.query(domain, query_type)[0].address - except dns.exception.Timeout: - raise - except: + except dns.resolver.NXDOMAIN: return None def ping_url(url): - try: - return requests.get(paths.PING_URL, params={'url': url}, timeout=5).text == 'vm-pong' - except requests.exceptions.Timeout: - raise - except: - return False + return requests.get(paths.PING_URL, params={'url': url}, timeout=5).text == 'vm-pong' diff --git a/usr/lib/python3.8/vmmgr/paths.py b/usr/lib/python3.8/vmmgr/paths.py index 318f26d..cc617c5 100644 --- a/usr/lib/python3.8/vmmgr/paths.py +++ b/usr/lib/python3.8/vmmgr/paths.py @@ -9,6 +9,8 @@ ACME_CRON = '/etc/periodic/daily/acme.sh' ACME_DIR = '/etc/acme.sh.d' CERT_KEY_FILE = '/etc/ssl/services.key' CERT_PUB_FILE = '/etc/ssl/services.pem' +TMP_KEY_FILE = '/tmp/private.pem' +TMP_PUB_FILE = '/tmp/public.pem' # OS ISSUE_FILE = '/etc/issue' diff --git a/usr/lib/python3.8/vmmgr/validator.py b/usr/lib/python3.8/vmmgr/validator.py index 7d199f7..af0e883 100644 --- a/usr/lib/python3.8/vmmgr/validator.py +++ b/usr/lib/python3.8/vmmgr/validator.py @@ -13,9 +13,8 @@ def is_valid_port(port): try: port = int(port) return 0 < port < 65536 and port not in (22, 25, 80, 8080) - except: - pass - return False + except TypeError: + return False def is_valid_email(email): parts = email.split('@') @@ -26,6 +25,5 @@ def is_valid_repo_url(url): try: parsed = urlparse(url) return parsed.scheme in ('http', 'https') and not parsed.params and not parsed.query and not parsed.fragment - except: - pass - return False + except ValueError: + return False diff --git a/usr/lib/python3.8/vmmgr/vmmgr.py b/usr/lib/python3.8/vmmgr/vmmgr.py index f0076c4..f5d7180 100644 --- a/usr/lib/python3.8/vmmgr/vmmgr.py +++ b/usr/lib/python3.8/vmmgr/vmmgr.py @@ -14,6 +14,15 @@ from spoc.image import Image from . import config, crypto, net, paths, templates +BIN_ACME_SH = '/usr/bin/acme.sh' +BIN_BLKID = '/sbin/blkid' +BIN_CRYPTSETUP = '/sbin/cryptsetup' +BIN_NGINX = '/usr/sbin/nginx' +BIN_POWEROFF = '/sbin/poweroff' +BIN_REBOOT = '/sbin/reboot' +BIN_SERVICE = '/sbin/service' +CRYPTTAB_FILE = '/etc/crypttab' + def register_app(app, host, login, password): # Register newly installed application, its subdomain and credentials (called at the end of package install.sh) config.register_app(app, { @@ -58,10 +67,10 @@ def update_host(domain, port): update_app_config() def reload_nginx(): - subprocess.run(['/usr/sbin/nginx', '-s', 'reload']) + subprocess.run([BIN_NGINX, '-s', 'reload']) def restart_nginx(): - subprocess.run(['/sbin/service', 'nginx', 'restart']) + subprocess.run([BIN_SERVICE, 'nginx', 'restart']) def rebuild_issue(): # Compile the URLs displayed in terminal banner and rebuild the issue and motd files @@ -83,9 +92,9 @@ def update_common_settings(email, gmaps_api_key): def update_password(oldpassword, newpassword): # Update LUKS password and adminpwd for WSGI application pwinput = f'{oldpassword}\n{newpassword}'.encode() - partition_uuid = open('/etc/crypttab').read().split()[1][5:] - partition_name = subprocess.run(['/sbin/blkid', '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip() - subprocess.run(['/sbin/cryptsetup', 'luksChangeKey', partition_name], input=pwinput, check=True) + partition_uuid = open(CRYPTTAB_FILE).read().split()[1][5:] + partition_name = subprocess.run([BIN_BLKID, '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip() + subprocess.run([BIN_CRYPTSETUP, 'luksChangeKey', partition_name], input=pwinput, check=True) # Update bcrypt-hashed password in config hash = crypto.adminpwd_hash(newpassword) config.set_host('adminpwd', hash) @@ -106,12 +115,12 @@ def request_acme_cert(): certs = [i for i in os.listdir(paths.ACME_DIR) if i not in ('account.conf', 'ca', 'http.header')] for cert in certs: if cert != domain: - subprocess.run(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--remove', '-d', cert]) + subprocess.run([BIN_ACME_SH, '--home', paths.ACME_DIR, '--remove', '-d', cert]) except FileNotFoundError: pass # Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before if not os.path.exists(os.path.join(paths.ACME_DIR, domain)): - cmd = ['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--issue', '-d', domain] + cmd = [BIN_ACME_SH, '--home', paths.ACME_DIR, '--issue', '-d', domain] for app,definition in config.get_apps().items(): cmd += ['-d', f'{definition["host"]}.{domain}'] cmd += ['-w', paths.ACME_DIR] @@ -120,13 +129,13 @@ def request_acme_cert(): # Otherwise just try to renew else: try: - subprocess.run(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--renew', '-d', domain], check=True) + subprocess.run([BIN_ACME_SH, '--home', paths.ACME_DIR, '--renew', '-d', domain], check=True) except subprocess.CalledProcessError as e: # return code 2 means skipped renew, which is OK if e.returncode != 2: raise # Install the issued certificate - subprocess.run(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--install-cert', '-d', domain, '--key-file', paths.CERT_KEY_FILE, '--fullchain-file', paths.CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True) + subprocess.run([BIN_ACME_SH, '--home', paths.ACME_DIR, '--install-cert', '-d', domain, '--key-file', paths.CERT_KEY_FILE, '--fullchain-file', paths.CERT_PUB_FILE, '--reloadcmd', f'{BIN_SERVICE} nginx reload'], check=True) # Enable acme.sh cronjob os.chmod(paths.ACME_CRON, 0o750) @@ -141,10 +150,10 @@ def install_manual_cert(public_file, private_file): reload_nginx() def shutdown_vm(): - subprocess.run(['/sbin/poweroff']) + subprocess.run([BIN_POWEROFF]) def reboot_vm(): - subprocess.run(['/sbin/reboot']) + subprocess.run([BIN_REBOOT]) def start_app(app_name, queue): # Enqueue application start @@ -182,7 +191,7 @@ def update_app_config(app_name=None): env['HOST'] = f'{apps[app_name]["host"]}.{host["domain"]}' try: subprocess.run(script_path, cwd=install_dir, env=env) - except: + except FileNotFoundError: pass def is_app_started(app_name): diff --git a/usr/lib/python3.8/vmmgr/wsgiapp.py b/usr/lib/python3.8/vmmgr/wsgiapp.py index 1cb6038..c717fc4 100644 --- a/usr/lib/python3.8/vmmgr/wsgiapp.py +++ b/usr/lib/python3.8/vmmgr/wsgiapp.py @@ -1,19 +1,22 @@ # -*- coding: utf-8 -*- +import cryptography.exceptions +import dns.exception import json import logging import os -from cryptography.exceptions import InvalidSignature +import requests.exceptions +import subprocess from jinja2 import Environment, FileSystemLoader from math import floor from pkg_resources import parse_version from spoc import repo_online, repo_local -from werkzeug.exceptions import HTTPException, NotFound, Unauthorized +from werkzeug.exceptions import HTTPException, NotFound from werkzeug.routing import Map, Rule from werkzeug.utils import redirect from werkzeug.wrappers import Request, Response -from . import config, crypto, net, remote, validator, vmmgr +from . import config, crypto, net, paths, remote, validator, vmmgr from .actionqueue import ActionQueue, ActionItemType from .wsgilang import WSGILang from .wsgisession import WSGISession @@ -166,12 +169,13 @@ class WSGIApp: try: # Repopulate online_repo cache or fail early when the repo can't be reached repo_online.load(True) - except InvalidSignature: + except cryptography.exceptions.InvalidSignature: repo_error = request.session.lang.invalid_packages_signature() - except Unauthorized: - repo_error = request.session.lang.repo_invalid_credentials() - except: - repo_error = request.session.lang.repo_unavailable() + except requests.exceptions.RequestException as e: + if isinstance(e, requests.exceptions.HTTPError) and e.response.status_code == 401: + repo_error = request.session.lang.repo_invalid_credentials() + else: + repo_error = request.session.lang.repo_unavailable() table = self.render_setup_apps_table(request) message = self.get_session_message(request) repo_conf = vmmgr.get_repo_conf() @@ -183,7 +187,7 @@ class WSGIApp: local_apps = repo_local.get_apps() try: online_apps = repo_online.get_apps() - except: + except (cryptography.exceptions.InvalidSignature, requests.exceptions.RequestException): online_apps = {} apps_config = config.get_apps() actionable_apps = sorted(set(online_apps) | set(local_apps)) @@ -205,7 +209,7 @@ class WSGIApp: app_queue = pending_actions[app] if app_queue.exception: # Display failed task - if isinstance(app_queue.exception, InvalidSignature): + if isinstance(app_queue.exception, cryptography.exceptions.InvalidSignature): status = lang.repo_package_invalid_signature() elif isinstance(app_queue.exception, NotFound): status = lang.repo_package_missing() @@ -314,8 +318,8 @@ class WSGIApp: return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, a, ipv4)}) if aaaa and aaaa != ipv6: return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, aaaa, ipv6)}) - except: - return self.render_json({'error': request.session.lang.dns_timeout()}) + except dns.exception.DNSException: + return self.render_json({'error': request.session.lang.dns_error()}) return self.render_json({'ok': request.session.lang.dns_records_ok()}) def verify_http_action(self, request, **kwargs): @@ -327,10 +331,11 @@ class WSGIApp: for domain in domains: url = net.compile_url(domain, port, proto) try: - if not net.ping_url(url): - return self.render_json({'error': request.session.lang.http_host_not_reachable(url)}) - except: + net.ping_url(url) + except requests.exceptions.Timeout: return self.render_json({'error': request.session.lang.http_timeout()}) + except requests.exceptions.RequestException: + return self.render_json({'error': request.session.lang.http_host_not_reachable(url)}) return self.render_json({'ok': request.session.lang.http_hosts_ok(port)}) def update_cert_action(self, request): @@ -344,11 +349,11 @@ class WSGIApp: return self.render_json({'error': request.session.lang.cert_file_missing()}) if not request.files['private']: return self.render_json({'error': request.session.lang.key_file_missing()}) - request.files['public'].save('/tmp/public.pem') - request.files['private'].save('/tmp/private.pem') - vmmgr.install_manual_cert('/tmp/public.pem', '/tmp/private.pem') - os.unlink('/tmp/public.pem') - os.unlink('/tmp/private.pem') + request.files['public'].save(paths.TMP_PUB_FILE) + request.files['private'].save(paths.TMP_KEY_FILE) + vmmgr.install_manual_cert(paths.TMP_PUB_FILE, paths.TMP_KEY_FILE) + os.unlink(paths.TMP_PUB_FILE) + os.unlink(paths.TMP_KEY_FILE) else: return self.render_json({'error': request.session.lang.cert_request_error()}) host = config.get_host() @@ -424,14 +429,14 @@ class WSGIApp: def update_password_action(self, request): # Updates password for both HDD encryption (LUKS-on-LVM) and web interface admin account + if request.form['newpassword'] != request.form['newpassword2']: + return self.render_json({'error': request.session.lang.password_mismatch()}) + if request.form['newpassword'] == '': + return self.render_json({'error': request.session.lang.password_empty()}) try: - if request.form['newpassword'] != request.form['newpassword2']: - return self.render_json({'error': request.session.lang.password_mismatch()}) - if request.form['newpassword'] == '': - return self.render_json({'error': request.session.lang.password_empty()}) # No need to explicitly validate old password, vmmgr.update_password will raise exception if it's wrong vmmgr.update_password(request.form['oldpassword'], request.form['newpassword']) - except: + except subprocess.CalledProcessError: return self.render_json({'error': request.session.lang.bad_password()}) return self.render_json({'ok': request.session.lang.password_changed()}) diff --git a/usr/lib/python3.8/vmmgr/wsgilang.py b/usr/lib/python3.8/vmmgr/wsgilang.py index 292ef00..a7a26ee 100644 --- a/usr/lib/python3.8/vmmgr/wsgilang.py +++ b/usr/lib/python3.8/vmmgr/wsgilang.py @@ -8,7 +8,7 @@ class WSGILang: 'host_updated': 'Nastavení hostitele bylo úspěšně změněno. Přejděte na URL {} a pokračujte následujícími kroky.', 'dns_record_does_not_exist': 'DNS záznam pro název "{}" neexistuje.', 'dns_record_mismatch': 'DNS záznam pro název "{}" směřuje na IP {} místo očekávané {}.', - 'dns_timeout': 'Nepodařilo se kontaktovat DNS server. Zkontrolujte, zda má virtuální stroj přístup k internetu.', + 'dns_error': 'Nepodařilo se získat platnou odpověď z DNS serveru. Zkontrolujte, zda má virtuální stroj přístup k internetu.', 'dns_records_ok': 'DNS záznamy jsou nastaveny správně.', 'http_host_not_reachable': 'Adresa {} není dostupná z internetu. Zkontrolujte nastavení síťových komponent.', 'http_timeout': 'Nepodařilo se kontaktovat ping server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',