mirror of
https://github.com/towalink/wgfrontend.git
synced 2025-08-27 23:31:15 +00:00
Initial public commit
This commit is contained in:
108
src/config.py
Normal file
108
src/config.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
import pwdtools
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
config_filename = '/etc/wgfrontend/wgfrontend.conf'
|
||||
|
||||
|
||||
class Configuration():
|
||||
"""Class for reading/writing the configuration file"""
|
||||
_config = None
|
||||
|
||||
def exists(self):
|
||||
"""Checks whether the config file exists"""
|
||||
return os.path.isfile(self.filename)
|
||||
|
||||
def read_config(self):
|
||||
"""Reads the config file"""
|
||||
try:
|
||||
logger.debug('Attempting to read config file [{0}]'.format(self.filename))
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(self.filename)
|
||||
self._config = dict(cfg['general'])
|
||||
self._users = dict(cfg['users'])
|
||||
except Exception as e:
|
||||
logger.warning('Config file [{0}] could not be read [{1}], using defaults'.format(self.filename, str(e)))
|
||||
self._config = dict()
|
||||
|
||||
def write_config(self, wg_configfile='', user='', users={}):
|
||||
"""Writes a new config file with the given attributes"""
|
||||
# Set default values
|
||||
if not wg_configfile.strip():
|
||||
wg_configfile = '/etc/wireguard/wg_rw.conf'
|
||||
if not user.strip():
|
||||
user = 'wgfrontend'
|
||||
users = { username if username.strip() else 'admin': password for username, password in users.items() }
|
||||
username = next(iter(users.keys()))
|
||||
password = pwdtools.hash_password(users[username])
|
||||
# Config file content
|
||||
config_content = textwrap.dedent(f'''\
|
||||
### Config file of the Towalink WireGuard Frontend ###
|
||||
[general]
|
||||
# The WireGuard config file to read and write
|
||||
wg_configfile = {wg_configfile}
|
||||
|
||||
# The system user to be used for the frontend
|
||||
user = {user}
|
||||
|
||||
[users]
|
||||
{username} = {password}
|
||||
''')
|
||||
# Write to file system
|
||||
try:
|
||||
with open(self.filename, 'w') as config_file:
|
||||
config_file.write(config_content)
|
||||
except OSError as e:
|
||||
logger.error('Could not write config file [{0}], [{1}]'.format(self.filename, str(e)))
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
"""Return the name of the config file (incl. path)"""
|
||||
return config_filename
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Return the config dictionary"""
|
||||
if self._config is None:
|
||||
self.read_config()
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""Return the users dictionary"""
|
||||
if self._users is None:
|
||||
self.read_config()
|
||||
return self._users
|
||||
|
||||
@property
|
||||
def wg_configfile(self):
|
||||
"""The filename incl. path of the config file for the WireGuard interface"""
|
||||
return self.config.get('wg_configfile', '/etc/wireguard/wg_rw.conf')
|
||||
|
||||
@property
|
||||
def sslcertfile(self):
|
||||
"""The filename incl. path for the server certificate"""
|
||||
return os.path.join(os.path.dirname(self.filename), 'server.pem')
|
||||
|
||||
@property
|
||||
def sslkeyfile(self):
|
||||
"""The filename incl. path for the server private key"""
|
||||
return os.path.join(os.path.dirname(self.filename), 'key.pem')
|
||||
|
||||
@property
|
||||
def libdir(self):
|
||||
"""The directory for the generated config files"""
|
||||
return '/var/lib/wgfrontend'
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
"""The configured name for the wgfrontend system user"""
|
||||
return self.config.get('user', 'wgfrontend')
|
32
src/pwdtools.py
Normal file
32
src/pwdtools.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Thx to https://www.vitoshacademy.com/hashing-passwords-in-python/
|
||||
|
||||
import hashlib, binascii, os
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
"""Hash a password for storing."""
|
||||
salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii')
|
||||
pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt, 100000)
|
||||
pwdhash = binascii.hexlify(pwdhash)
|
||||
return (salt + pwdhash).decode('ascii')
|
||||
|
||||
def verify_password(stored_password, provided_password):
|
||||
"""Verify a stored password against one provided by user"""
|
||||
salt = stored_password[:64]
|
||||
stored_password = stored_password[64:]
|
||||
pwdhash = hashlib.pbkdf2_hmac('sha512', provided_password.encode('utf-8'), salt.encode('ascii'), 100000)
|
||||
pwdhash = binascii.hexlify(pwdhash).decode('ascii')
|
||||
return pwdhash == stored_password
|
||||
|
||||
def hash_password_interactively():
|
||||
"""Ask for a password and print it in hashed form"""
|
||||
pwd = input('Please enter password to hash: ')
|
||||
print('Hashed password:')
|
||||
print(hash_password(pwd))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
hash_password_interactively()
|
178
src/setupenv.py
Normal file
178
src/setupenv.py
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import getpass
|
||||
import grp
|
||||
import os
|
||||
import pwd
|
||||
import string
|
||||
import wgconfig
|
||||
import wgconfig.wgexec as wgexec
|
||||
|
||||
import config
|
||||
|
||||
|
||||
def is_root():
|
||||
"""Returns whether this script is run with user is 0 (root)"""
|
||||
return os.getuid() == 0
|
||||
|
||||
def get_user():
|
||||
"""Returns the effective user that executes this script"""
|
||||
return getpass.getuser()
|
||||
|
||||
def check_user(username):
|
||||
"""Returns a boolean that indicates if the given user exists on the system"""
|
||||
try:
|
||||
pwd.getpwnam(username)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def create_user(username):
|
||||
"""Create the given user on the system"""
|
||||
if os.path.exists('/usr/sbin/useradd'):
|
||||
os.system(f'useradd {username}')
|
||||
else:
|
||||
os.system(f'adduser {username} -D')
|
||||
|
||||
def ensure_user(username):
|
||||
"""Ensures that the given user exists on the system"""
|
||||
if not check_user(username):
|
||||
create_user(username)
|
||||
|
||||
def check_wg():
|
||||
"""Check whether the wg tool is present"""
|
||||
return os.path.isfile('/usr/bin/wg')
|
||||
|
||||
def check_wgquick():
|
||||
"""Check whether the wg-quick tool is present"""
|
||||
return os.path.isfile('/usr/bin/wg-quick')
|
||||
|
||||
def touch_file(filename, perm=0o640):
|
||||
"""Touch the given file with the provided permissions"""
|
||||
if not os.path.exists(os.path.dirname(filename)):
|
||||
os.makedirs(os.path.dirname(filename))
|
||||
with os.fdopen(os.open(filename, os.O_WRONLY | os.O_CREAT, perm), 'w') as handle:
|
||||
pass
|
||||
|
||||
def check_validcharacters(value, valid_chars=string.ascii_letters):
|
||||
"""Checks whether the provided string only contains the listed characters"""
|
||||
invalid = [ k for k in value if k not in valid_chars ]
|
||||
if invalid:
|
||||
return False
|
||||
return True
|
||||
|
||||
def chown(username, path):
|
||||
"""Change file/path ownership to given user"""
|
||||
uid = pwd.getpwnam(username).pw_uid
|
||||
os.chown(path, uid, -1)
|
||||
|
||||
def drop_privileges(uid_name='nobody', gid_name='nogroup'):
|
||||
""""""
|
||||
if not is_root():
|
||||
raise ValueError('No privileges present to drop')
|
||||
uid = pwd.getpwnam(uid_name).pw_uid
|
||||
gid = grp.getgrnam(gid_name).gr_gid
|
||||
# os.setgroups([]) # remove group privileges
|
||||
os.setgid(gid)
|
||||
os.setuid(uid)
|
||||
|
||||
def setup_environment():
|
||||
"""Environment setup assistant"""
|
||||
cfg = config.Configuration()
|
||||
if is_root():
|
||||
print('Welcome to Towalink WireGuard Frontend')
|
||||
print('======================================')
|
||||
print('You are executing "wgfrontend" as root user. We\'ll now make sure that everything is properly installed.')
|
||||
if check_wg():
|
||||
print(f'1a) Wireguard (wg) is available. Ok.')
|
||||
else:
|
||||
print(f'1a) Wireguard (wg) is not available. FAIL.')
|
||||
if check_wg():
|
||||
print(f'1b) Wireguard (wg-quick) is available. Ok.')
|
||||
else:
|
||||
print(f'1b) Wireguard (wg-quick) is not available. FAIL.')
|
||||
if cfg.exists():
|
||||
print(f'2) Config file {cfg.filename} already exists. Ok.')
|
||||
else:
|
||||
print(f'2) Config file {cfg.filename} does not yet exist. Let\'s create one...')
|
||||
print(f' Press enter to select defaults.')
|
||||
wg_configfile = input(f'2a) Please specify the WireGuard config file to be used [/etc/wireguard/wg_rw.conf]: ')
|
||||
user = input(f'2b) Please specify the system user for the web frontend [wgfrontend]: ')
|
||||
ok = False
|
||||
while not ok:
|
||||
username = input(f'2c) Please specify the username for your web frontend user [admin]: ')
|
||||
if check_validcharacters(username, string.ascii_letters + '_'):
|
||||
ok = True
|
||||
else:
|
||||
print(' Username must only contain letters and underscores. Please enter anew.')
|
||||
ok = False
|
||||
while not ok:
|
||||
password = input(f'2d) Please specify the password for your web frontend user: ')
|
||||
if len(password) >= 8:
|
||||
ok = True
|
||||
else:
|
||||
print(' Password must have at least eight characters. Please enter anew.')
|
||||
touch_file(cfg.filename, perm=0o640) # create without world read permissions
|
||||
cfg.write_config(wg_configfile=wg_configfile, user=user, users={username: password})
|
||||
print(' Config file written. Ok.')
|
||||
print(f'3) Ensuring that system user "{cfg.user}" exists.')
|
||||
ensure_user(cfg.user)
|
||||
print(f'4) Ensuring ownership of config file {cfg.filename}.')
|
||||
chown(cfg.user, cfg.filename)
|
||||
if os.path.exists(cfg.libdir):
|
||||
print(f'5) Directory {cfg.libdir} already exists. Ok.')
|
||||
else:
|
||||
print(f'5) Directory {cfg.libdir} does not yet exist. Let\'s create it...')
|
||||
os.makedirs(cfg.libdir, mode=0o640, exist_ok=True)
|
||||
print(' Directory created. Ok.')
|
||||
print(f'6) Ensuring ownership of directory {cfg.libdir}.')
|
||||
chown(cfg.user, cfg.libdir)
|
||||
if os.path.exists(cfg.wg_configfile):
|
||||
print(f'7) WireGuard config file {cfg.wg_configfile} already exists. Ok.')
|
||||
else:
|
||||
print(f'7) WireGuard config file {cfg.wg_configfile} does not yet exist. Let\'s create one...')
|
||||
wg_listenport = input(f'7a) Please specify the listen port of the WireGuard interface [51820]: ')
|
||||
if not wg_listenport.strip():
|
||||
wg_listenport = 51820
|
||||
ok = False
|
||||
while not ok:
|
||||
endpoint = input(f'7b) Please specify the endpoint hostname (and optionally port) to reach your WireGuard server: ')
|
||||
if len(endpoint) > 0:
|
||||
ok = True
|
||||
else:
|
||||
print(' You need to enter an endpoint hostname.')
|
||||
wg_address = input(f'7c) Please specify the IP address of the WireGuard interface incl. prefix length [192.168.0.1/24]: ')
|
||||
if not wg_address.strip():
|
||||
wg_address = '192.168.0.1/24'
|
||||
wg_networks = input(f'7d) Please specify the network ranges that the clients shall route to the WireGuard server [192.168.0.0/16]: ')
|
||||
if not wg_networks.strip():
|
||||
wg_networks = '192.168.0.0/16'
|
||||
wc = wgconfig.WGConfig(cfg.wg_configfile)
|
||||
wc.initialize_file('# This file has been created and is managed by wgfrontend. Only change manually if you know what you\'re doing.')
|
||||
if not ':' in endpoint:
|
||||
endpoint += ':51820'
|
||||
wc.add_attr(None, 'ListenPort', wg_listenport, '# Endpoint = ' + endpoint, append_as_line=True)
|
||||
wc.add_attr(None, 'PrivateKey', wgexec.generate_privatekey())
|
||||
wc.add_attr(None, 'Address', wg_address, '# Networks = ' + wg_networks, append_as_line=True)
|
||||
wc.write_file()
|
||||
print(' Config file written. Ok.')
|
||||
print(f'8a) Ensuring list permission of WireGuard config directory {os.path.dirname(cfg.wg_configfile)}.')
|
||||
os.chmod(os.path.dirname(cfg.wg_configfile), 0o711)
|
||||
print(f'8b) Ensuring ownership of WireGuard config file {cfg.wg_configfile}.')
|
||||
chown(cfg.user, cfg.wg_configfile)
|
||||
print(f'8c) Ensuring ownership of server certificate file {cfg.sslcertfile} in case it exists.')
|
||||
if os.path.exists(cfg.sslcertfile):
|
||||
chown(cfg.user, cfg.sslcertfile)
|
||||
print(f'8d) Ensuring ownership of server private key file {cfg.sslkeyfile} in case it exists.')
|
||||
if os.path.exists(cfg.sslkeyfile):
|
||||
chown(cfg.user, cfg.sslkeyfile)
|
||||
print(f'9) Dropping root privileges to user/group "{cfg.user}".')
|
||||
drop_privileges(cfg.user, cfg.user)
|
||||
print(f'Attempting to start web frontend...')
|
||||
return cfg
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
setup_environment()
|
15
src/templates/base.html
Normal file
15
src/templates/base.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv='content-type' content='text/html; charset=utf-8' />
|
||||
<title>Towalink WireGuard Frontend</title>
|
||||
<link rel='stylesheet' media='screen' href='/static/layout.css' />
|
||||
</head>
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='header'>
|
||||
{% include 'part_header.html' %}
|
||||
</div>
|
||||
<div class='content'>{% block content %}{% endblock %}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
24
src/templates/config.html
Normal file
24
src/templates/config.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<h3>Client Config</h3>
|
||||
<div class='instructions'>
|
||||
<a href='https://www.wireguard.com/install/' target="_blank">WireGuard client installation instructions</a>
|
||||
</div>
|
||||
<div class='span-18 last'>
|
||||
<form method="get" action="download">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
{{ peerdata['Description'] }}<br>
|
||||
{{ peerdata['Address'] }}
|
||||
</td>
|
||||
<td>
|
||||
<img src="/configs/{{ peerdata['Id'] }}.png" alt="QR Code">
|
||||
<button type="submit" name="id" value="{{ peerdata['Id'] }}">Download Config</button>
|
||||
<button type="submit" formaction=".." }}">Return</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
22
src/templates/edit.html
Normal file
22
src/templates/edit.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<h3>Configured Clients</h3>
|
||||
<div class='span-18 last'>
|
||||
<form method="get" action="edit">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="id" value="{{ peerdata['Id'] }}" />
|
||||
<input type="text" name="description" value="{{ peerdata['Description'] }}" size="40" /><br>
|
||||
{{ peerdata['Address'] }}
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit" name="action" value="save">Save Data</button>
|
||||
<button type="submit" name="action" value="delete" formaction="/" onclick="return confirm('Do you really want to delete this client?')">Delete Client</button>
|
||||
<button type="submit" formaction="/">Return to List</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
23
src/templates/index.html
Normal file
23
src/templates/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<h3>Configured Clients</h3>
|
||||
<div class='span-18 last'>
|
||||
<form method="get" action="edit">
|
||||
<button type="submit" name="action" value="new">Add Client</button>
|
||||
<table>
|
||||
{%- for peer, peerdata in peers.items()|sort(attribute='1.Description') %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ peerdata['Description'] }}<br>
|
||||
{{ peerdata['Address'] }}
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit" name="id" value="{{ peerdata['Id'] }}">Edit Client</button>
|
||||
<button type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="config">Get Config</button>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
3
src/templates/part_header.html
Normal file
3
src/templates/part_header.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class='heading'>
|
||||
<h2><a href='/'>Towalink WireGuard Frontend</a></h2>
|
||||
</div>
|
151
src/webapp.py
Normal file
151
src/webapp.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import cherrypy
|
||||
import jinja2
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
import pwdtools
|
||||
import wgcfg
|
||||
|
||||
|
||||
def login_screen(from_page='..', username='', error_msg='', **kwargs):
|
||||
"""Based on https://docs.cherrypy.org/en/latest/_modules/cherrypy/lib/cptools.html"""
|
||||
content="""
|
||||
<form method="post" action="do_login">
|
||||
<div align=center>
|
||||
<span class=errormsg>%s</span>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
Login:
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="username" value="%s" size="40" />
|
||||
</td>
|
||||
<tr>
|
||||
<td>
|
||||
Password:
|
||||
</td>
|
||||
<td>
|
||||
<input type="password" name="password" size="40" />
|
||||
<input type="hidden" name="from_page" value="%s" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan=2 align=right>
|
||||
<input type="submit" value="Login" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
""" % (error_msg, username, from_page)
|
||||
title='Login'
|
||||
return ('<html><body>' + content + '</body></html>').encode('utf-8')
|
||||
# return cherrypy.tools.encode('<html><body>' + content + '</body></html>')
|
||||
|
||||
|
||||
class WebApp():
|
||||
|
||||
def __init__(self, cfg):
|
||||
"""Instance initialization"""
|
||||
self.cfg = cfg
|
||||
self.jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
|
||||
self.wg = wgcfg.WGCfg(self.cfg.wg_configfile, self.cfg.libdir)
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self, action=None, id=None, description=None):
|
||||
if (action == 'delete') and id:
|
||||
peer, peerdata = self.wg.get_peer_byid(id)
|
||||
self.wg.delete_peer(peer)
|
||||
peers = self.wg.get_peers()
|
||||
tmpl = self.jinja_env.get_template('index.html')
|
||||
return tmpl.render(peers=peers)
|
||||
|
||||
@cherrypy.expose
|
||||
def config(self, id):
|
||||
peer, peerdata = self.wg.get_peer_byid(id)
|
||||
tmpl = self.jinja_env.get_template('config.html')
|
||||
return tmpl.render(peerdata=peerdata)
|
||||
|
||||
@cherrypy.expose
|
||||
def edit(self, action='edit', id=None, description=None):
|
||||
if id: # existing client
|
||||
peer, peerdata = self.wg.get_peer_byid(id)
|
||||
if description:
|
||||
peerdata = self.wg.update_peer(peer, description)
|
||||
else:
|
||||
if not description:
|
||||
description = 'My new client'
|
||||
if action == 'new': # default values for new client
|
||||
peerdata = { 'Description': description, 'Id': '' }
|
||||
else: # save changes
|
||||
peer = self.wg.create_peer(description)
|
||||
peerdata = self.wg.get_peer(peer)
|
||||
tmpl = self.jinja_env.get_template('edit.html')
|
||||
return tmpl.render(peerdata=peerdata)
|
||||
|
||||
@cherrypy.expose
|
||||
def download(self, id):
|
||||
peer, peerdata = self.wg.get_peer_byid(id)
|
||||
config, peerdata = self.wg.get_peerconfig(peer)
|
||||
cherrypy.response.headers['Content-Disposition'] = f'attachment; filename=wg_{id}.conf'
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain' # 'application/x-download' 'application/octet-stream'
|
||||
return config.encode('utf-8')
|
||||
|
||||
@cherrypy.expose
|
||||
def logout(self):
|
||||
username = cherrypy.session['username']
|
||||
cherrypy.session.clear()
|
||||
return '"{0}" has been logged out'.format(username)
|
||||
|
||||
|
||||
|
||||
def run_webapp(cfg):
|
||||
|
||||
def check_username_and_password(username, password):
|
||||
"""Check whether provided username and password are valid when authenticating"""
|
||||
if (username in cfg.users) and (pwdtools.verify_password(cfg.users[username], password)):
|
||||
return
|
||||
return 'invalid username/password'
|
||||
|
||||
script_path = os.path.dirname(os.path.abspath(__file__))
|
||||
conf = {
|
||||
'/': {
|
||||
'tools.sessions.on': True,
|
||||
'tools.staticdir.root': os.path.join(script_path, 'webroot'),
|
||||
# 'tools.session_auth.on': True,
|
||||
'tools.session_auth.login_screen': login_screen,
|
||||
'tools.session_auth.check_username_and_password': check_username_and_password,
|
||||
},
|
||||
'/configs': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.root': None,
|
||||
'tools.staticdir.dir': cfg.libdir
|
||||
},
|
||||
'/static': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': 'static'
|
||||
},
|
||||
'/favicon.ico':
|
||||
{
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': os.path.join(script_path, 'webroot', 'static', 'favicon.ico')
|
||||
}
|
||||
}
|
||||
if os.path.exists(cfg.sslcertfile) and os.path.exists(cfg.sslkeyfile):
|
||||
cherrypy.server.ssl_module = 'builtin'
|
||||
cherrypy.server.ssl_certificate = cfg.sslcertfile
|
||||
cherrypy.server.ssl_private_key = cfg.sslkeyfile
|
||||
cherrypy.config.update({'server.socket_host': '0.0.0.0',
|
||||
'server.socket_port': 8080,
|
||||
})
|
||||
cherrypy.quickstart(WebApp(cfg), '/', conf)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
BIN
src/webroot/static/favicon.ico
Normal file
BIN
src/webroot/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
1
src/webroot/static/test.html
Normal file
1
src/webroot/static/test.html
Normal file
@@ -0,0 +1 @@
|
||||
TEST
|
160
src/wgcfg.py
Normal file
160
src/wgcfg.py
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
import qrcode
|
||||
import textwrap
|
||||
import wgconfig
|
||||
import wgconfig.wgexec as wgexec
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WGCfg():
|
||||
"""Class for reading/writing the WireGuard configuration file"""
|
||||
|
||||
def __init__(self, filename, libdir):
|
||||
"""Initialize instance for the given config file"""
|
||||
self.filename = filename
|
||||
self.libdir = libdir
|
||||
self.wc = wgconfig.WGConfig(self.filename)
|
||||
self.wc.read_file()
|
||||
|
||||
def get_interface(self):
|
||||
"""Get WireGuard interface data"""
|
||||
return self.wc.interface
|
||||
|
||||
def transform_to_clientdata(self, peer, peerdata):
|
||||
"""Transform data of a single peer from server into a dictionary of client config data"""
|
||||
result = dict()
|
||||
description = peerdata['_rawdata'][0]
|
||||
if description[0] == '#':
|
||||
description = description[2:]
|
||||
else:
|
||||
description = 'Peer: ' + peer
|
||||
result['Description'] = description
|
||||
for item in peerdata['_rawdata']:
|
||||
if item.startswith('# PrivateKey = '):
|
||||
result['PrivateKey'] = item[15:]
|
||||
result['PublicKey'] = peer
|
||||
result['PresharedKey'] = peerdata['PresharedKey']
|
||||
address = peerdata['AllowedIPs'].partition(',')[0] # get first allowed ip range
|
||||
address = address.partition('/')[0] + '/' + self.get_interface()['Address'].partition('/')[2] # take prefix length from interface address
|
||||
result['Address'] = address
|
||||
result['Id'] = address.partition('/')[0].replace('.', '-')
|
||||
result['QRCode'] = os.path.join(self.libdir, result['Id'] + '.png')
|
||||
return result
|
||||
|
||||
def get_peer(self, peer):
|
||||
"""Get data of the given WireGuard peer"""
|
||||
return self.transform_to_clientdata(peer, self.wc.peers[peer])
|
||||
|
||||
def get_peers(self):
|
||||
"""Get data of all WireGuard peers"""
|
||||
return { peer: self.get_peer(peer) for peer in self.wc.peers.keys() }
|
||||
|
||||
def get_peer_byid(self, id):
|
||||
"""Get data WireGuard peer with the given id"""
|
||||
peer = next(peer for peer, peerdata in self.get_peers().items() if peerdata['Id'] == id)
|
||||
return peer, self.get_peer(peer)
|
||||
|
||||
def get_peerconfig(self, peer):
|
||||
"""Get config for the given WireGuard peer"""
|
||||
peerdata = self.get_peer(peer)
|
||||
for item in self.get_interface()['_rawdata']:
|
||||
if item.startswith('# Endpoint = '):
|
||||
endpoint = item[13:]
|
||||
if item.startswith('# Networks = '):
|
||||
allowed_ips = item[13:]
|
||||
public_key = wgexec.get_publickey(peerdata['PrivateKey'])
|
||||
public_key_server = wgexec.get_publickey(self.get_interface()['PrivateKey'])
|
||||
config = textwrap.dedent(f'''\
|
||||
# {peerdata['Description']}
|
||||
[Interface]
|
||||
ListenPort = 51820
|
||||
PrivateKey = {peerdata['PrivateKey']}
|
||||
# PublicKey = {public_key}
|
||||
Address = {peerdata['Address']}
|
||||
|
||||
[Peer]
|
||||
Endpoint = {endpoint}
|
||||
PublicKey = {public_key_server}
|
||||
PresharedKey = {peerdata['PresharedKey']}
|
||||
AllowedIPs = {allowed_ips}
|
||||
PersistentKeepalive = 25
|
||||
''')
|
||||
return config, peerdata
|
||||
|
||||
def create_peer(self, description, ip=None):
|
||||
"""Create peer with the given description"""
|
||||
if ip is None:
|
||||
ip = self.find_free_ip()
|
||||
private_key = wgexec.generate_privatekey()
|
||||
peer = wgexec.get_publickey(private_key)
|
||||
self.wc.add_peer(peer, '# ' + description)
|
||||
comment = '# PrivateKey = ' + private_key
|
||||
self.wc.add_attr(peer, 'PresharedKey', wgexec.generate_presharedkey(), comment, append_as_line=True)
|
||||
self.wc.add_attr(peer, 'AllowedIPs', ip + '/32')
|
||||
self.wc.add_attr(peer, 'PersistentKeepalive', 25)
|
||||
self.wc.write_file()
|
||||
self.write_qrcode(peer)
|
||||
return peer
|
||||
|
||||
def update_peer(self, peer, description):
|
||||
"""Update the given peer"""
|
||||
peerdata = self.wc.peers[peer]
|
||||
first_line = peerdata['_index_firstline']
|
||||
if self.wc.lines[first_line][0] != '#':
|
||||
raise ValueError(f'Comment expected in first line of config for peer [{peerdata}]')
|
||||
self.wc.lines[first_line] = '# ' + description
|
||||
self.wc.invalidate_data()
|
||||
self.wc.write_file()
|
||||
return self.get_peer(peer)
|
||||
|
||||
def delete_peer(self, peer):
|
||||
"""Delete the given peer"""
|
||||
self.wc.del_peer(peer)
|
||||
self.wc.write_file()
|
||||
|
||||
def find_free_ip(self):
|
||||
"""Find the first free address in the given network"""
|
||||
interface_address = ipaddress.ip_interface(self.get_interface()['Address'])
|
||||
network = interface_address.network
|
||||
interface_address = interface_address.ip
|
||||
addresses = [ ipaddress.ip_interface(peerdata['Address']).ip for peer, peerdata in self.get_peers().items() ]
|
||||
print(addresses) # ***
|
||||
ip = None
|
||||
for addr in network.hosts():
|
||||
if addr == interface_address:
|
||||
continue
|
||||
if addr in addresses:
|
||||
continue
|
||||
ip = addr
|
||||
break
|
||||
if ip is None:
|
||||
raise ValueError('No free IP address available any more')
|
||||
return str(ip)
|
||||
|
||||
def write_qrcode(self, peer):
|
||||
"""Generate a QRCode for the given peers configuration file and store in lib directory"""
|
||||
config, peerdata = self.get_peerconfig(peer)
|
||||
#img = qrcode.make(config)
|
||||
qr = qrcode.QRCode(version=15, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=2, border=5)
|
||||
qr.add_data(config)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color='black', back_color='white')
|
||||
img.save(peerdata['QRCode'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import pprint
|
||||
wg = WGCfg('/etc/wireguard/wg_rw.conf', '/var/lib/wgfrontend')
|
||||
peer = wg.create_peer('This is a first test peer')
|
||||
peer2 = wg.create_peer('This is a second test peer')
|
||||
wg.update_peer(peer2, 'CHANGED')
|
||||
print(wg.get_peerconfig(peer2)[0])
|
||||
wg.delete_peer(peer)
|
||||
wg.delete_peer(peer2)
|
15
src/wgfrontend.py
Normal file
15
src/wgfrontend.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import setupenv
|
||||
import webapp
|
||||
|
||||
|
||||
def main():
|
||||
cfg = setupenv.setup_environment()
|
||||
webapp.run_webapp(cfg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Reference in New Issue
Block a user