Drop common app settings and simplify config

This commit is contained in:
Disassembler 2020-03-29 22:50:42 +02:00
parent fb38e535e1
commit e468ec9051
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
12 changed files with 57 additions and 123 deletions

View File

@ -1,9 +1,5 @@
{ {
"apps": {}, "apps": {},
"common": {
"email": "admin@example.com",
"gmaps-api-key": ""
},
"host": { "host": {
"adminpwd": "${ADMINPWD}", "adminpwd": "${ADMINPWD}",
"domain": "spotter.vm", "domain": "spotter.vm",

View File

@ -3,11 +3,9 @@
import argparse import argparse
from vmmgr.config import Config from vmmgr import config, vmmgr
from vmmgr.vmmgr import VMMgr
def main(args): def main(args):
vmmgr = VMMgr(Config())
if args.action == 'register-app': if args.action == 'register-app':
# Used by package install.sh script # Used by package install.sh script
vmmgr.register_app(args.app, args.host, args.login, args.password) vmmgr.register_app(args.app, args.host, args.login, args.password)

View File

@ -17,8 +17,8 @@ class ActionItemType(Enum):
APP_UNINSTALL = 8 APP_UNINSTALL = 8
class ActionItem: class ActionItem:
def __init__(self, type, key, action, show_progress=True): def __init__(self, action_type, key, action, show_progress=True):
self.type = type self.type = action_type
self.key = key self.key = key
self.action = action self.action = action
self.show_progress = show_progress self.show_progress = show_progress

View File

@ -25,59 +25,38 @@ def save():
mtime = os.stat(CONF_FILE).st_mtime mtime = os.stat(CONF_FILE).st_mtime
@locked(CONF_LOCK) @locked(CONF_LOCK)
def get_entries(attr): def get_apps():
load() load()
return data[attr] return data['apps']
@locked(CONF_LOCK) @locked(CONF_LOCK)
def add_entry(entry_type, name, definition): def get_host():
load() load()
data[entry_type][name] = definition return data['host']
@locked(CONF_LOCK)
def register_app(name, definition):
load()
data['apps'][name] = definition
save() save()
@locked(CONF_LOCK) @locked(CONF_LOCK)
def delete_entry(entry_type, name): def unregister_app(name):
load() load()
try: try:
del data[entry_type][name] del data['apps'][name]
save() save()
except KeyError: except KeyError:
pass pass
def get_apps():
return get_entries('apps')
def get_common():
return get_entries('common')
def get_host():
host = get_entries('host')
return (host['domain'], host['port'])
def get_adminpwd():
return get_entries('host')['adminpwd']
def register_app(app_name, definition):
add_entry('apps', app_name, definition)
def unregister_app(app_name):
delete_entry('apps', app_name)
def set_common(key, value):
add_entry('common', key, value)
@locked(CONF_LOCK) @locked(CONF_LOCK)
def set_host(domain, port): def set_host_value(key, value):
load() load()
data['host']['domain'] = domain data['host'][key] = value
data['host']['port'] = port
save() save()
def set_adminpwd(hash):
add_entry('host', 'adminpwd', hash)
@locked(CONF_LOCK) @locked(CONF_LOCK)
def set_app(app_name, key, value): def set_app_value(name, key, value):
load() load()
data['apps'][app_name][key] = value data['apps'][name][key] = value
save() save()

View File

@ -14,7 +14,7 @@ from .paths import ACME_CRON, CERT_PUB_FILE, CERT_KEY_FILE
def create_selfsigned_cert(): def create_selfsigned_cert():
# Create selfsigned certificate with wildcard alternative subject name # Create selfsigned certificate with wildcard alternative subject name
domain = config.get_host()[0] domain = config.get_host()['domain']
private_key = ec.generate_private_key(ec.SECP384R1(), default_backend()) private_key = ec.generate_private_key(ec.SECP384R1(), default_backend())
public_key = private_key.public_key() public_key = private_key.public_key()
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, domain)]) subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, domain)])
@ -60,4 +60,4 @@ def adminpwd_hash(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def adminpwd_verify(password): def adminpwd_verify(password):
return bcrypt.checkpw(password.encode(), config.get_adminpwd().encode()) return bcrypt.checkpw(password.encode(), config.get_host()['adminpwd'].encode())

View File

@ -41,10 +41,10 @@ resolver.timeout = 3
resolver.lifetime = 3 resolver.lifetime = 3
resolver.nameservers = ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844'] resolver.nameservers = ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844']
def resolve_ip(domain, qtype): def resolve_ip(domain, query_type):
# Resolve domain name using Google Public DNS # Resolve domain name using Google Public DNS
try: try:
return resolver.query(domain, qtype)[0].address return resolver.query(domain, query_type)[0].address
except dns.exception.Timeout: except dns.exception.Timeout:
raise raise
except: except:

View File

@ -7,10 +7,11 @@ from .paths import AUTHORIZED_KEYS, INTERFACES_FILE, WG_CONF_FILE, WG_CONF_FILE_
def get_authorized_keys(): def get_authorized_keys():
# Fetches content of root's authorized_files # Fetches content of root's authorized_files
if not os.path.exists(AUTHORIZED_KEYS): try:
with open(AUTHORIZED_KEYS) as f:
return f.read()
except FileNotFoundError:
return '' return ''
with open(AUTHORIZED_KEYS) as f:
return f.read()
def set_authorized_keys(keys): def set_authorized_keys(keys):
# Saves content of root's authorized_files # Saves content of root's authorized_files
@ -33,7 +34,7 @@ def regenerate_wireguard_key():
was_running = is_wireguard_running() was_running = is_wireguard_running()
if was_running: if was_running:
stop_wireguard() stop_wireguard()
privkey = subprocess.run(['wg', 'genkey'], stdout=subprocess.PIPE).stdout.strip().decode() privkey = subprocess.run(['wg', 'genkey'], stdout=subprocess.PIPE).stdout.decode().strip()
with open(WG_CONF_FILE_DISABLED) as f: with open(WG_CONF_FILE_DISABLED) as f:
conf_lines = f.readlines() conf_lines = f.readlines()
conf_lines[2] = f'PrivateKey = {privkey}\n' conf_lines[2] = f'PrivateKey = {privkey}\n'
@ -62,7 +63,7 @@ def get_wireguard_conf():
if privkey == 'None': if privkey == 'None':
privkey = regenerate_wireguard_key() privkey = regenerate_wireguard_key()
p = subprocess.Popen(['wg', 'pubkey'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) p = subprocess.Popen(['wg', 'pubkey'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
result['pubkey'] = p.communicate(privkey.encode())[0].strip().decode() result['pubkey'] = p.communicate(privkey.encode())[0].decode().strip()
return result return result
def set_wireguard_conf(ip, port, peers): def set_wireguard_conf(ip, port, peers):
@ -91,14 +92,16 @@ def set_wireguard_conf(ip, port, peers):
def start_wireguard(): def start_wireguard():
# Sets up WireGuard interface # Sets up WireGuard interface
if not os.path.exists(WG_CONF_FILE): try:
os.rename(WG_CONF_FILE_DISABLED, WG_CONF_FILE) os.rename(WG_CONF_FILE_DISABLED, WG_CONF_FILE)
else: except FileNotFoundError:
subprocess.run(['ifdown', 'wg0']) subprocess.run(['ifdown', 'wg0'])
subprocess.run(['ifup', 'wg0']) subprocess.run(['ifup', 'wg0'])
def stop_wireguard(): def stop_wireguard():
# Tears down WireGuard interface # Tears down WireGuard interface
subprocess.run(['ifdown', 'wg0']) subprocess.run(['ifdown', 'wg0'])
if os.path.exists(WG_CONF_FILE): try:
os.rename(WG_CONF_FILE, WG_CONF_FILE_DISABLED) os.rename(WG_CONF_FILE, WG_CONF_FILE_DISABLED)
except FileNotFoundError:
pass

View File

@ -109,6 +109,6 @@ ISSUE = '''
Pro přístup k aplikacím otevřete jednu z těcho URL v internetovém prohlížeči. Pro přístup k aplikacím otevřete jednu z těcho URL v internetovém prohlížeči.
Open one the following URLs in web browser to access the applications. Open one the following URLs in web browser to access the applications.
- \x1b[1m{url}\x1b[0m - \x1b[1m{url_host}\x1b[0m
- \x1b[1m{ip}\x1b[0m\x1b[?1c - \x1b[1m{url_ip}\x1b[0m\x1b[?1c
''' '''

View File

@ -26,10 +26,10 @@ def unregister_app(app):
def register_proxy(app): def register_proxy(app):
# Setup proxy configuration and reload nginx # Setup proxy configuration and reload nginx
app_host = config.get_app(app)['host'] app_host = config.get_apps()[app]['host']
domain,port = config.get_host() host = config.get_host()
with open(os.path.join(NGINX_DIR, f'{app}.conf'), 'w') as f: with open(os.path.join(NGINX_DIR, f'{app}.conf'), 'w') as f:
f.write(templates.NGINX.format(app=app, host=app_host, domain=domain, port=port)) f.write(templates.NGINX.format(app=app, host=app_host, domain=host['domain'], port=host['port']))
reload_nginx() reload_nginx()
def unregister_proxy(app): def unregister_proxy(app):
@ -54,18 +54,15 @@ def restart_nginx():
def rebuild_issue(): def rebuild_issue():
# Compile the URLs displayed in terminal banner and rebuild the issue and motd files # Compile the URLs displayed in terminal banner and rebuild the issue and motd files
domain, port = config.get_host() host = config.get_host()
issue = templates.ISSUE.format(url=net.compile_url(domain, port), ip=net.compile_url(net.get_local_ip(), port)) url_host = net.compile_url(host['domain'], host['port'])
url_ip = net.compile_url(net.get_local_ip(), host['port'])
issue = templates.ISSUE.format(url_host=url_host, url_ip=url_ip)
with open(ISSUE_FILE, 'w') as f: with open(ISSUE_FILE, 'w') as f:
f.write(issue) f.write(issue)
with open(MOTD_FILE, 'w') as f: with open(MOTD_FILE, 'w') as f:
f.write(issue) f.write(issue)
def update_common_settings(email, gmaps_api_key):
# Update common configuration values
config.set_common('email', email)
config.set_common('gmaps-api-key', gmaps_api_key)
def update_password(oldpassword, newpassword): def update_password(oldpassword, newpassword):
# Update LUKS password and adminpwd for WSGI application # Update LUKS password and adminpwd for WSGI application
pwinput = f'{oldpassword}\n{newpassword}'.encode() pwinput = f'{oldpassword}\n{newpassword}'.encode()
@ -73,20 +70,21 @@ def update_password(oldpassword, newpassword):
partition_name = subprocess.run(['/sbin/blkid', '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip() partition_name = subprocess.run(['/sbin/blkid', '-U', partition_uuid], check=True, stdout=subprocess.PIPE).stdout.decode().strip()
subprocess.run(['cryptsetup', 'luksChangeKey', partition_name], input=pwinput, check=True) subprocess.run(['cryptsetup', 'luksChangeKey', partition_name], input=pwinput, check=True)
# Update bcrypt-hashed password in config # Update bcrypt-hashed password in config
config.set_adminpwd(crypto.adminpwd_hash(newpassword)) hash = crypto.adminpwd_hash(newpassword)
config.set_host('adminpwd', hash)
def create_selfsigned_cert(): def create_selfsigned_cert():
# Disable acme.sh cronjob # Disable acme.sh cronjob
os.chmod(ACME_CRON, 0o640) os.chmod(ACME_CRON, 0o640)
# Create selfsigned certificate with wildcard alternative subject name # Create selfsigned certificate with wildcard alternative subject name
domain = config.get_host()[0] domain = config.get_host()['domain']
crypto.create_selfsigned_cert(domain) crypto.create_selfsigned_cert(domain)
# Reload nginx # Reload nginx
reload_nginx() reload_nginx()
def request_acme_cert(): def request_acme_cert():
# Remove all possible conflicting certificates requested in the past # Remove all possible conflicting certificates requested in the past
domain = config.get_host()[0] domain = config.get_host()['domain']
certs = [i for i in os.listdir(ACME_DIR) if i not in ('account.conf', 'ca', 'http.header')] certs = [i for i in os.listdir(ACME_DIR) if i not in ('account.conf', 'ca', 'http.header')]
for cert in certs: for cert in certs:
if cert != domain: if cert != domain:

View File

@ -42,7 +42,6 @@ class WSGIApp:
Rule('/verify-https', endpoint='verify_http_action', defaults={'proto': 'https'}), Rule('/verify-https', endpoint='verify_http_action', defaults={'proto': 'https'}),
Rule('/verify-http', endpoint='verify_http_action', defaults={'proto': 'http'}), Rule('/verify-http', endpoint='verify_http_action', defaults={'proto': 'http'}),
Rule('/update-cert', endpoint='update_cert_action'), Rule('/update-cert', endpoint='update_cert_action'),
Rule('/update-common', endpoint='update_common_action'),
Rule('/update-repo', endpoint='update_repo_action'), Rule('/update-repo', endpoint='update_repo_action'),
Rule('/update-app-visibility', endpoint='update_app_visibility_action'), Rule('/update-app-visibility', endpoint='update_app_visibility_action'),
Rule('/update-app-autostart', endpoint='update_app_autostart_action'), Rule('/update-app-autostart', endpoint='update_app_autostart_action'),
@ -138,7 +137,8 @@ class WSGIApp:
def portal_view(self, request): def portal_view(self, request):
# Default portal view. # Default portal view.
host = net.compile_url(*config.get_host(), None) host = config.get_host()
host = net.compile_url(host['domain'], host['port'], None)
apps = config.get_apps() apps = config.get_apps()
visible_apps = [app for app,definition in apps.items() if definition['visible'] and vmmgr.is_app_started(app)] visible_apps = [app for app,definition in apps.items() if definition['visible'] and vmmgr.is_app_started(app)]
if request.session['admin']: if request.session['admin']:
@ -153,9 +153,8 @@ class WSGIApp:
in_ipv6 = net.get_local_ip(6) in_ipv6 = net.get_local_ip(6)
cert_info = crypto.get_cert_info() cert_info = crypto.get_cert_info()
apps = config.get_apps() apps = config.get_apps()
common = config.get_common() host = config.get_host()
domain,port = config.get_host() return self.render_html('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, cert_info=cert_info, apps=apps, domain=host['domain'], port=host['port'])
return self.render_html('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, cert_info=cert_info, apps=apps, common=common, domain=domain, port=port)
def setup_apps_view(self, request): def setup_apps_view(self, request):
# Application manager view. # Application manager view.
@ -172,8 +171,7 @@ class WSGIApp:
table = self.render_setup_apps_table(request) table = self.render_setup_apps_table(request)
message = self.get_session_message(request) message = self.get_session_message(request)
repo_url, repo_user, _ = vmmgr.get_repo_settings() repo_url, repo_user, _ = vmmgr.get_repo_settings()
common = config.get_common() return self.render_html('setup-apps.html', request, repo_url=repo_url, repo_user=repo_user, repo_error=repo_error, table=table, message=message)
return self.render_html('setup-apps.html', request, repo_url=repo_url, repo_user=repo_user, repo_error=repo_error, table=table, message=message, common=common)
def render_setup_apps_table(self, request): def render_setup_apps_table(self, request):
lang = request.session.lang lang = request.session.lang
@ -289,7 +287,7 @@ class WSGIApp:
def verify_dns_action(self, request): def verify_dns_action(self, request):
# Check if all FQDNs for all applications are resolvable and point to current external IP # Check if all FQDNs for all applications are resolvable and point to current external IP
domain = config.get_host()[0] domain = config.get_host()['domain']
domains = [domain]+[f'{definition["host"]}.{domain}' for app,definition in config.get_apps().items()] domains = [domain]+[f'{definition["host"]}.{domain}' for app,definition in config.get_apps().items()]
ipv4 = net.get_external_ip(4) ipv4 = net.get_external_ip(4)
ipv6 = net.get_external_ip(6) ipv6 = net.get_external_ip(6)
@ -310,9 +308,9 @@ class WSGIApp:
def verify_http_action(self, request, **kwargs): def verify_http_action(self, request, **kwargs):
# Check if all applications are accessible from the internet using 3rd party ping service # Check if all applications are accessible from the internet using 3rd party ping service
proto = kwargs['proto'] proto = kwargs['proto']
domain, port = config.get_host() host = config.get_host()
port = port if proto == 'https' else '80' port = host['port'] if proto == 'https' else '80'
domains = [domain]+[f'{definition["host"]}.{domain}' for app,definition in config.get_apps().items()] domains = [host['domain']]+[f'{definition["host"]}.{host["domain"]}' for app,definition in config.get_apps().items()]
for domain in domains: for domain in domains:
url = net.compile_url(domain, port, proto) url = net.compile_url(domain, port, proto)
try: try:
@ -340,19 +338,10 @@ class WSGIApp:
os.unlink('/tmp/private.pem') os.unlink('/tmp/private.pem')
else: else:
return self.render_json({'error': request.session.lang.cert_request_error()}) return self.render_json({'error': request.session.lang.cert_request_error()})
url = net.compile_url(*config.get_host()) host = config.get_host()
url = net.compile_url(host['domain'], host['port'])
return self.render_json({'ok': request.session.lang.cert_installed(url, url)}) return self.render_json({'ok': request.session.lang.cert_installed(url, url)})
def update_common_action(self, request):
# Update common settings shared between apps - admin e-mail address, Google Maps API key
email = request.form['email']
if not validator.is_valid_email(email):
request.session['msg'] = f'common:error:{request.session.lang.invalid_email(email)}'
else:
vmmgr.update_common_settings(email, request.form['gmaps-api-key'])
request.session['msg'] = f'common:info:{request.session.lang.common_updated()}'
return redirect('/setup-apps')
def update_repo_action(self, request): def update_repo_action(self, request):
# Update repository URL and credentials # Update repository URL and credentials
url = request.form['repourl'] url = request.form['repourl']

View File

@ -19,7 +19,6 @@ class WSGILang:
'cert_installed': 'Certifikát byl úspěšně nainstalován. Přejděte na URL <a href="{}">{}</a> nebo restartujte webový prohlížeč pro jeho načtení.', 'cert_installed': 'Certifikát byl úspěšně nainstalován. Přejděte na URL <a href="{}">{}</a> nebo restartujte webový prohlížeč pro jeho načtení.',
'invalid_email': 'Zadaný e-mail "{}" není platný.', 'invalid_email': 'Zadaný e-mail "{}" není platný.',
'invalid_url': 'Zadaná adresa "{}" není platná.', 'invalid_url': 'Zadaná adresa "{}" není platná.',
'common_updated': 'Nastavení aplikací bylo úspěšně změněno. Pokud je některá z aplikací spuštěna, změny se projeví po jejím restartu.',
'repo_updated': 'Nastavení distribučního serveru bylo úspěšně změněno.', 'repo_updated': 'Nastavení distribučního serveru bylo úspěšně změněno.',
'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.', 'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.',
'installation_in_progress': 'Probíhá instalace jiného balíku. Vyčkejte na její dokončení.', 'installation_in_progress': 'Probíhá instalace jiného balíku. Vyčkejte na její dokončení.',

View File

@ -49,34 +49,6 @@
</form> </form>
</div> </div>
<div class="setup-box">
<h2>Nastavení aplikací</h2>
<p>Společné nastavení sdílené některými aplikacemi.</p>
<form id="update-common" action="/update-common" method="post">
<table>
<tr>
<td>E-mail</td>
<td><input type="text" name="email" value="{{ common['email'] }}"></td>
<td class="remark">Administrativní e-mail na který budou doručovány zprávy a upozornění z aplikací. Stejná e-mailová adresa bude také využita některými aplikacemi pro odesílání zpráv uživatelům.</td>
</tr>
<tr>
<td>Google Maps API klíč</td>
<td><input type="text" name="gmaps-api-key" value="{{ common['gmaps-api-key'] }}"></td>
<td class="remark">API klíč pro službu Google Maps, která je využita některými aplikacemi.</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="common-submit" value="Nastavit hodnoty">
{% if message and message[0] == 'common' %}
<div class="{{ message[1] }}">{{ message[2] }}</div>
{% endif %}
</td>
</tr>
</table>
</form>
</div>
<div class="setup-box"> <div class="setup-box">
<h2>Správce virtuálního stroje</h2> <h2>Správce virtuálního stroje</h2>
<p>Změna hesla k šifrovanému diskovému oddílu a administračnímu rozhraní.</p> <p>Změna hesla k šifrovanému diskovému oddílu a administračnímu rozhraní.</p>