Don't import separate path constants, import whole module in case the constants are not so constant
This commit is contained in:
parent
43f55e5917
commit
0ca993a9ed
@ -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()
|
||||
data['apps'][name][key] = value
|
||||
save()
|
||||
try:
|
||||
data['apps'][name][key] = value
|
||||
save()
|
||||
except KeyError:
|
||||
pass
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -1,26 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Config
|
||||
CONF_FILE = '/etc/vmmgr/config.json'
|
||||
CONF_LOCK = '/var/lock/vmmgr-config.lock'
|
||||
# Module config
|
||||
CONF_FILE = '/etc/vmmgr/config.json'
|
||||
CONF_LOCK = '/var/lock/vmmgr-config.lock'
|
||||
|
||||
# Crypto
|
||||
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'
|
||||
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'
|
||||
|
||||
# OS
|
||||
ISSUE_FILE = '/etc/issue'
|
||||
MOTD_FILE = '/etc/motd'
|
||||
NGINX_DIR = '/etc/nginx/conf.d'
|
||||
ISSUE_FILE = '/etc/issue'
|
||||
MOTD_FILE = '/etc/motd'
|
||||
NGINX_DIR = '/etc/nginx/conf.d'
|
||||
|
||||
# Remote access
|
||||
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'
|
||||
AUTHORIZED_KEYS = '/root/.ssh/authorized_keys'
|
||||
INTERFACES_FILE = '/etc/network/interfaces'
|
||||
WG_CONF_FILE = '/etc/wireguard/wg0.conf'
|
||||
WG_CONF_DISABLED = '/etc/wireguard/wg0.conf.disabled'
|
||||
|
||||
# URLs
|
||||
MYIP_URL = 'https://repo.spotter.cz/tools/myip.php'
|
||||
PING_URL = 'https://repo.spotter.cz/tools/vm-ping.php'
|
||||
MYIP_URL = 'https://repo.spotter.cz/tools/myip.php'
|
||||
PING_URL = 'https://repo.spotter.cz/tools/vm-ping.php'
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user