Don't import separate path constants, import whole module in case the constants are not so constant

This commit is contained in:
Disassembler 2020-04-04 20:14:29 +02:00
parent 43f55e5917
commit 0ca993a9ed
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
6 changed files with 78 additions and 77 deletions

View File

@ -4,7 +4,7 @@ import json
import os
from spoc.flock import locked
from .paths import CONF_FILE, CONF_LOCK
from . import paths
data = {}
mtime = None
@ -12,35 +12,35 @@ mtime = None
def load():
global data
global mtime
file_mtime = os.stat(CONF_FILE).st_mtime
file_mtime = os.stat(paths.CONF_FILE).st_mtime
if mtime != file_mtime:
with open(CONF_FILE, 'r') as f:
with open(paths.CONF_FILE, 'r') as f:
data = json.load(f)
mtime = file_mtime
def save():
global mtime
with open(CONF_FILE, 'w') as f:
with open(paths.CONF_FILE, 'w') as f:
json.dump(data, f, sort_keys=True, indent=4)
mtime = os.stat(CONF_FILE).st_mtime
mtime = os.stat(paths.CONF_FILE).st_mtime
@locked(CONF_LOCK)
@locked(paths.CONF_LOCK)
def get_apps():
load()
return data['apps']
@locked(CONF_LOCK)
@locked(paths.CONF_LOCK)
def get_host():
load()
return data['host']
@locked(CONF_LOCK)
@locked(paths.CONF_LOCK)
def register_app(name, definition):
load()
data['apps'][name] = definition
save()
@locked(CONF_LOCK)
@locked(paths.CONF_LOCK)
def unregister_app(name):
load()
try:
@ -49,14 +49,17 @@ def unregister_app(name):
except KeyError:
pass
@locked(CONF_LOCK)
@locked(paths.CONF_LOCK)
def set_host(key, value):
load()
data['host'][key] = value
save()
@locked(CONF_LOCK)
@locked(paths.CONF_LOCK)
def set_app(name, key, value):
load()
try:
data['apps'][name][key] = value
save()
except KeyError:
pass

View File

@ -9,7 +9,7 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
from .paths import ACME_CRON, CERT_PUB_FILE, CERT_KEY_FILE
from . import paths
def create_selfsigned_cert(domain):
# Create selfsigned certificate with wildcard alternative subject name
@ -31,25 +31,24 @@ def create_selfsigned_cert(domain):
.add_extension(x509.KeyUsage(digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False, key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False), critical=True) \
.add_extension(x509.ExtendedKeyUsage((ExtendedKeyUsageOID.SERVER_AUTH, ExtendedKeyUsageOID.CLIENT_AUTH)), critical=False) \
.sign(private_key, hashes.SHA256(), default_backend())
with open(CERT_PUB_FILE, 'wb') as f:
with open(paths.CERT_PUB_FILE, 'wb') as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
with open(CERT_KEY_FILE, 'wb') as f:
with open(paths.CERT_KEY_FILE, 'wb') as f:
f.write(private_key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
os.chmod(CERT_KEY_FILE, 0o600)
os.chmod(paths.CERT_KEY_FILE, 0o600)
def get_cert_info():
# Gather certificate data important for setup-host
with open(CERT_PUB_FILE, 'rb') as f:
with open(paths.CERT_PUB_FILE, 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read(), default_backend())
data = {'subject': cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
'issuer': cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value,
'expires': f'{cert.not_valid_after} UTC',
'method': 'manual'}
if os.access(ACME_CRON, os.X_OK):
if os.access(paths.ACME_CRON, os.X_OK):
data['method'] = 'automatic'
# Naive method of inferring if the cert is selfsigned
# Good enough as reputable CAs will never have the same subject and issuer CN
# and the 'method' field is used just to populate a GUI element and not for any real cryptography
# Good enough as reputable CAs will never have the same subject and issuer CN and the 'method' field is used just to populate a GUI element and not for any real cryptography
elif data['subject'] == data['issuer']:
data['method'] = 'selfsigned'
return data

View File

@ -6,7 +6,7 @@ import requests
import socket
import subprocess
from .paths import MYIP_URL, PING_URL
from . import paths
def compile_url(domain, port, proto='https'):
port = '' if (proto in (None, 'https') and port == '443') or (proto == 'http' and port == '80') else f':{port}'
@ -30,7 +30,7 @@ def get_external_ip(version):
allowed_gai_family = requests.packages.urllib3.util.connection.allowed_gai_family
try:
requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family
return requests.get(MYIP_URL, timeout=5).text
return requests.get(paths.MYIP_URL, timeout=5).text
except:
return None
finally:
@ -52,7 +52,7 @@ def resolve_ip(domain, query_type):
def ping_url(url):
try:
return requests.get(PING_URL, params={'url': url}, timeout=5).text == 'vm-pong'
return requests.get(paths.PING_URL, params={'url': url}, timeout=5).text == 'vm-pong'
except requests.exceptions.Timeout:
raise
except:

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# Config
# Module config
CONF_FILE = '/etc/vmmgr/config.json'
CONF_LOCK = '/var/lock/vmmgr-config.lock'
# Crypto
ACME_CRON = '/etc/periodic/daily/acme-sh'
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'
@ -19,7 +19,7 @@ NGINX_DIR = '/etc/nginx/conf.d'
AUTHORIZED_KEYS = '/root/.ssh/authorized_keys'
INTERFACES_FILE = '/etc/network/interfaces'
WG_CONF_FILE = '/etc/wireguard/wg0.conf'
WG_CONF_FILE_DISABLED = '/etc/wireguard/wg0.conf.disabled'
WG_CONF_DISABLED = '/etc/wireguard/wg0.conf.disabled'
# URLs
MYIP_URL = 'https://repo.spotter.cz/tools/myip.php'

View File

@ -3,19 +3,19 @@
import os
import subprocess
from .paths import AUTHORIZED_KEYS, INTERFACES_FILE, WG_CONF_FILE, WG_CONF_FILE_DISABLED
from . import paths
def get_authorized_keys():
# Fetches content of root's authorized_files
try:
with open(AUTHORIZED_KEYS) as f:
with open(paths.AUTHORIZED_KEYS) as f:
return f.read()
except FileNotFoundError:
return ''
def set_authorized_keys(keys):
# Saves content of root's authorized_files
with open(AUTHORIZED_KEYS, 'w') as f:
with open(paths.AUTHORIZED_KEYS, 'w') as f:
f.write(keys)
# Enable or disable SSH service
if keys.strip():
@ -27,18 +27,18 @@ def set_authorized_keys(keys):
def is_wireguard_running():
# Returns status of wg0 interface (inferred from existence of its config file)
return os.path.exists(WG_CONF_FILE)
return os.path.exists(paths.WG_CONF_FILE)
def regenerate_wireguard_key():
# Regenerates WireGuard key pair for wg0 interface
was_running = is_wireguard_running()
if was_running:
stop_wireguard()
privkey = subprocess.run(['wg', 'genkey'], stdout=subprocess.PIPE).stdout.decode().strip()
with open(WG_CONF_FILE_DISABLED) as f:
privkey = subprocess.run(['/usr/bin/wg', 'genkey'], stdout=subprocess.PIPE).stdout.decode().strip()
with open(paths.WG_CONF_DISABLED) as f:
conf_lines = f.readlines()
conf_lines[2] = f'PrivateKey = {privkey}\n'
with open(WG_CONF_FILE_DISABLED, 'w') as f:
with open(paths.WG_CONF_DISABLED, 'w') as f:
f.writelines(conf_lines)
if was_running:
start_wireguard()
@ -48,13 +48,13 @@ def get_wireguard_conf():
# Returns dictionary with WireGuard wg0 interface VPN IP, public key, listen port and peer info
result = {'running': is_wireguard_running()}
# IP
with open(INTERFACES_FILE) as f:
with open(paths.INTERFACES_FILE) as f:
for line in f.read().splitlines():
if '172.17.255' in line:
result['ip'] = line.split('.')[-1]
break
# Listen port and peers
with open(WG_CONF_FILE if result['running'] else WG_CONF_FILE_DISABLED) as f:
with open(paths.WG_CONF_FILE if result['running'] else paths.WG_CONF_DISABLED) as f:
conf_lines = f.read().splitlines()
result['port'] = conf_lines[1].split()[2]
result['peers'] = '\n'.join(conf_lines[4:])
@ -62,8 +62,7 @@ def get_wireguard_conf():
privkey = conf_lines[2].split()[2]
if privkey == 'None':
privkey = regenerate_wireguard_key()
p = subprocess.Popen(['wg', 'pubkey'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
result['pubkey'] = p.communicate(privkey.encode())[0].decode().strip()
result['pubkey'] = subprocess.run(['/usr/bin/wg', 'pubkey'], input=privkey.encode(), stdout=subprocess.PIPE).stdout.decode().strip()
return result
def set_wireguard_conf(ip, port, peers):
@ -73,18 +72,18 @@ def set_wireguard_conf(ip, port, peers):
stop_wireguard()
# IP
interface_lines = []
with open(INTERFACES_FILE) as f:
with open(paths.INTERFACES_FILE) as f:
for line in f.readlines():
if '172.17.255' in line:
line = f' address 172.17.255.{ip}\n'
interface_lines.append(line)
with open(INTERFACES_FILE, 'w') as f:
with open(paths.INTERFACES_FILE, 'w') as f:
f.writelines(interface_lines)
# Recreate config (listen port and peers)
with open(WG_CONF_FILE_DISABLED) as f:
with open(paths.WG_CONF_DISABLED) as f:
conf_lines = f.readlines()[:4]
conf_lines[1] = f'ListenPort = {port}\n'
with open(WG_CONF_FILE_DISABLED, 'w') as f:
with open(paths.WG_CONF_DISABLED, 'w') as f:
f.writelines(conf_lines)
f.write(peers)
if was_running:
@ -93,15 +92,15 @@ def set_wireguard_conf(ip, port, peers):
def start_wireguard():
# Sets up WireGuard interface
try:
os.rename(WG_CONF_FILE_DISABLED, WG_CONF_FILE)
os.rename(paths.WG_CONF_DISABLED, paths.WG_CONF_FILE)
except FileNotFoundError:
subprocess.run(['ifdown', 'wg0'])
subprocess.run(['ifup', 'wg0'])
subprocess.run(['/sbin/ifdown', 'wg0'])
subprocess.run(['/sbin/ifup', 'wg0'])
def stop_wireguard():
# Tears down WireGuard interface
subprocess.run(['ifdown', 'wg0'])
subprocess.run(['/sbin/ifdown', 'wg0'])
try:
os.rename(WG_CONF_FILE, WG_CONF_FILE_DISABLED)
os.rename(paths.WG_CONF_FILE, paths.WG_CONF_DISABLED)
except FileNotFoundError:
pass

View File

@ -12,8 +12,7 @@ from spoc.container import Container, ContainerState
from spoc.depsolver import DepSolver
from spoc.image import Image
from . import config, crypto, net, templates
from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, MOTD_FILE, NGINX_DIR
from . import config, crypto, net, paths, templates
def register_app(app, host, login, password):
# Register newly installed application, its subdomain and credentials (called at the end of package install.sh)
@ -35,22 +34,23 @@ def register_proxy(app):
host = config.get_host()
# Assume that the main container has always the same name as the app
app_ip = spoc_net.get_ip(app)
with open(os.path.join(NGINX_DIR, f'{app}.conf'), 'w') as f:
with open(os.path.join(paths.NGINX_DIR, f'{app}.conf'), 'w') as f:
f.write(templates.NGINX.format(app=app, host=app_host, ip=app_ip, domain=host['domain'], port=host['port']))
reload_nginx()
def unregister_proxy(app):
# Remove proxy configuration and reload nginx
try:
os.unlink(os.path.join(NGINX_DIR, f'{app}.conf'))
os.unlink(os.path.join(paths.NGINX_DIR, f'{app}.conf'))
reload_nginx()
except FileNotFoundError:
pass
def update_host(domain, port):
config.set_host(domain, port)
config.set_host('domain', domain)
config.set_host('port', port)
# Rebuild nginx config for the portal app. Web interface calls restart_nginx() in WSGI close handler
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
with open(os.path.join(paths.NGINX_DIR, 'default.conf'), 'w') as f:
f.write(templates.NGINX_DEFAULT.format(port=port, domain_esc=domain.replace('.', '\\.')))
def reload_nginx():
@ -65,9 +65,9 @@ def rebuild_issue():
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(paths.ISSUE_FILE, 'w') as f:
f.write(issue)
with open(MOTD_FILE, 'w') as f:
with open(paths.MOTD_FILE, 'w') as f:
f.write(issue)
def update_password(oldpassword, newpassword):
@ -82,7 +82,7 @@ def update_password(oldpassword, newpassword):
def create_selfsigned_cert():
# Disable acme.sh cronjob
os.chmod(ACME_CRON, 0o640)
os.chmod(paths.ACME_CRON, 0o640)
# Create selfsigned certificate with wildcard alternative subject name
domain = config.get_host()['domain']
crypto.create_selfsigned_cert(domain)
@ -92,34 +92,34 @@ def create_selfsigned_cert():
def request_acme_cert():
# Remove all possible conflicting certificates requested in the past
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(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', '/etc/acme.sh.d', '--remove', '-d', cert])
subprocess.run(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--remove', '-d', cert])
# 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(ACME_DIR, domain)):
if not os.path.exists(os.path.join(paths.ACME_DIR, domain)):
cmd = ['/usr/bin/acme.sh', '--issue', '-d', domain]
for app,definition in config.get_apps():
cmd += ['-d', f'{definition["host"]}.{domain}']
cmd += ['-w', ACME_DIR]
cmd += ['-w', paths.ACME_DIR]
# Request the certificate
subprocess.run(cmd, check=True)
# Otherwise just try to renew
else:
# Acme.sh returns code 2 on skipped renew
try:
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--renew', '-d', domain], check=True)
subprocess.run(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--renew', '-d', domain], check=True)
except subprocess.CalledProcessError as e:
if e.returncode != 2:
raise
# Install the issued certificate
subprocess.run(['/usr/bin/acme.sh', '--home', '/etc/acme.sh.d', '--install-cert', '-d', domain, '--key-file', crypto.CERT_KEY_FILE, '--fullchain-file', crypto.CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True)
subprocess.run(['/usr/bin/acme.sh', '--home', paths.ACME_DIR, '--install-cert', '-d', domain, '--key-file', crypto.CERT_KEY_FILE, '--fullchain-file', crypto.CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True)
# Enable acme.sh cronjob
os.chmod(ACME_CRON, 0o750)
os.chmod(paths.ACME_CRON, 0o750)
def install_manual_cert(public_file, private_file):
# Disable acme.sh cronjob
os.chmod(ACME_CRON, 0o640)
os.chmod(paths.ACME_CRON, 0o640)
# Copy certificate files
shutil.copyfile(public_file, crypto.CERT_PUB_FILE)
shutil.copyfile(private_file, crypto.CERT_KEY_FILE)