From 258564208bd886936f8dfbc7e5b5423272e7168f Mon Sep 17 00:00:00 2001 From: Donald Cheng Hong Zou Date: Tue, 18 Jan 2022 10:42:23 -0500 Subject: [PATCH] Finalizing everything. --- README.md | 19 +- src/dashboard.py | 388 +++++++++++++++++++------------------- src/requirements.txt | 1 - src/templates/signin.html | 38 +++- src/wgd.sh | 45 +---- 5 files changed, 245 insertions(+), 246 deletions(-) diff --git a/README.md b/README.md index 0d07ab4..2b3f037 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,19 @@ - 🎉 **New Features** - **Moved from TinyDB to SQLite**: SQLite provide a better performance and loading speed when getting peers! Also avoided crashing the database due to **race condition**. + - **Added Gunicorn WSGI Server**: This could provide more stable on handling HTTP request, and more flexibility in the future (such as HTTPS support). **BIG THANKS to @pgalonza :heart: ** - **Add Peers by Bulk: ** User can add peers by bulk, just simply set the amount and click add. - **Delete Peers by Bulk**: User can delete peers by bulk, without deleting peers one by one. - **Download Peers in Zip**: User can download all *downloadable* peers in a zip. - **Added Pre-shared Key to peers:** Now each peer can add with a pre-shared key to enhance security. Previously added peers can add the pre-shared key through the peer setting button. - + - **Redirect Back to Previous Page:** The dashboard will now redirect you back to your previous page if the current session got timed out and you need to sign in again. + - 🪚 **Bug Fixed** - [IP Sorting range issues #99](https://github.com/donaldzou/WGDashboard/issues/99) [❤️ @barryboom] - [INvalid character written to tunnel json file #108](https://github.com/donaldzou/WGDashboard/issues/108) [❤️ @ ikidd] - [Add IPv6 #91](https://github.com/donaldzou/WGDashboard/pull/91) [❤️ @ pgalonza] - [Added MTU and PersistentKeepalive to QR code and download files #112](https://github.com/donaldzou/WGDashboard/pull/112) [:heart: @reafian] + - **And many other bugs provided by our beloved users** :heart: - **🧐 Other Changes** - **Key generating moved to front-end**: No longer need to use the server's WireGuard to generate keys, thanks to the `wireguard.js` from the [official repository](https://git.zx2c4.com/wireguard-tools/tree/contrib/keygen-html/wireguard.js)! - **Peer transfer calculation**: each peer will now show all transfer amount (previously was only showing transfer amount from the last configuration start-up). @@ -35,12 +38,15 @@ - **`wgd.sh` finally can update itself**: So now user could update the whole dashboard from `wgd.sh`, with the `update` command. - **Minified JS and CSS files**: Although only a small changes on the file size, but I think is still a good practice to save a bit of bandwidth ;) - *And many other small changes for performance and bug fixes! :laughing:* +> If you have any other brilliant ideas for this project, please shout it in here [#129](https://github.com/donaldzou/WGDashboard/issues/129) :heart: +
+ ## Table of Content + - [💡 Features](#-features) - [📝 Requirement](#-requirement) - [🛠 Install](#-install) @@ -332,11 +338,18 @@ Endpoint = 0.0.0.0:51820 ## ❓ How to update the dashboard? +#### **Please note for user who is using `v2.3.1` or below** + +- For user who is using `v2.3.1` or below, please notice that all data that stored in the current database will **not** transfer to the new database. This is hard decision to move from TinyDB to SQLite. But SQLite does provide a thread-safe access and TinyDB doesn't. I couldn't find a safe way to transfer the data, so you need to do them manually... Sorry about that :pensive: . But I guess this would be a great start for future development :sunglasses:. + +
+ 1. Change your directory to `wgdashboard` + ```shell cd wgdashboard ``` - + 2. Update the dashboard ```shell git pull https://github.com/donaldzou/WGDashboard.git v3.0 --force diff --git a/src/dashboard.py b/src/dashboard.py index bea2fe4..e8aca9d 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -17,6 +17,7 @@ import time import re import urllib.parse import urllib.request +import urllib.error from datetime import datetime, timedelta from operator import itemgetter # PIP installed library @@ -65,7 +66,6 @@ def get_dashboard_conf(): Get dashboard configuration @return: configparser.ConfigParser """ - config = configparser.ConfigParser(strict=False) config.read(DASHBOARD_CONF) return config @@ -76,7 +76,6 @@ def set_dashboard_conf(config): Write to configuration @param config: Input configuration """ - with open(DASHBOARD_CONF, "w", encoding='utf-8') as conf_object: config.write(conf_object) @@ -129,7 +128,6 @@ def get_conf_running_peer_number(config_name): return running -# TODO use modules for working with ini(configparser or wireguard) # Read [Interface] section from configuration file def read_conf_file_interface(config_name): """ @@ -154,10 +152,6 @@ def read_conf_file_interface(config_name): return data -# TODO use modules for working with ini(configparser or wireguard) -# Tried to use configparser but it does not support sections with the same name - - def read_conf_file(config_name): """ Get configurations from file of wireguard interface. @@ -375,7 +369,6 @@ def get_all_peers_data(config_name): get_allowed_ip(conf_peer_data, config_name) -# Search for peers def get_peers(config_name, search, sort_t): """ Get all peers. @@ -428,7 +421,6 @@ def get_conf_pub_key(config_name): return "" -# Get configuration listen port def get_conf_listen_port(config_name): """ Get listen port number. @@ -452,8 +444,12 @@ def get_conf_listen_port(config_name): return port -# Get configuration total data def get_conf_total_data(config_name): + """ + Get configuration's total amount of data + @param config_name: Configuration name + @return: list + """ data = g.cur.execute("SELECT total_sent, total_receive, cumu_sent, cumu_receive FROM " + config_name) upload_total = 0 download_total = 0 @@ -468,19 +464,21 @@ def get_conf_total_data(config_name): return [total, upload_total, download_total] -# Get configuration status def get_conf_status(config_name): + """ + Check if the configuration is running or not + @param config_name: + @return: Return a string indicate the running status + """ ifconfig = dict(ifcfg.interfaces().items()) - return "running" if config_name in ifconfig.keys() else "stopped" -# Get all configuration as a list def get_conf_list(): """Get all wireguard interfaces with status. - :return: Return a list of dicts with interfaces and its statuses - :rtype: list + @return: Return a list of dicts with interfaces and its statuses + @rtype: list """ conf = [] @@ -510,25 +508,13 @@ def get_conf_list(): return conf -# Generate private key -def gen_private_key(): - subprocess.run('wg genkey > private_key.txt && wg pubkey < private_key.txt > public_key.txt', shell=True) - with open('private_key.txt', encoding='utf-8') as file_object: - private_key = file_object.readline().strip() - with open('public_key.txt', encoding='utf-8') as file_object: - public_key = file_object.readline().strip() - data = {"private_key": private_key, "public_key": public_key} - return data - - -# Generate public key def gen_public_key(private_key): """Generate the public key. - :param private_key: Pricate key - :type private_key: str - :return: Return dict with public key or error message - :rtype: dict + @param private_key: Private key + @type private_key: str + @return: Return dict with public key or error message + @rtype: dict """ with open('private_key.txt', 'w', encoding='utf-8') as file_object: @@ -570,8 +556,14 @@ def f_check_key_match(private_key, public_key, config_name): return {'status': 'success'} -# Check if there is repeated allowed IP def check_repeat_allowed_ip(public_key, ip, config_name): + """ + Check if there are repeated IPs + @param public_key: Public key of the peer + @param ip: IP of the peer + @param config_name: configuration name + @return: a JSON object + """ peer = g.cur.execute("SELECT COUNT(*) FROM " + config_name + " WHERE id = ?", (public_key,)).fetchone() if peer[0] != 1: return {'status': 'failed', 'msg': 'Peer does not exist'} @@ -586,6 +578,11 @@ def check_repeat_allowed_ip(public_key, ip, config_name): def f_available_ips(config_name): + """ + Get a list of available IPs + @param config_name: Configuration Name + @return: list + """ config_interface = read_conf_file_interface(config_name) if "Address" in config_interface: existed = [] @@ -596,7 +593,6 @@ def f_available_ips(config_name): existed.append(ipaddress.ip_address(add)) peers = g.cur.execute("SELECT allowed_ip FROM " + config_name).fetchall() for i in peers: - print(i[0]) add = i[0].split(",") for k in add: a, s = k.split("/") @@ -620,6 +616,11 @@ Flask Functions @app.teardown_request def close_DB(exception): + """ + Commit to the database for every request + @param exception: Exception + @return: None + """ if hasattr(g, 'db'): g.db.commit() g.db.close() @@ -628,6 +629,10 @@ def close_DB(exception): # Before request @app.before_request def auth_req(): + """ + Action before every request + @return: Redirect + """ if getattr(g, 'db', None) is None: g.db = connect_db() g.cur = g.db.cursor() @@ -647,7 +652,7 @@ def auth_req(): else: session['message'] = "" conf.clear() - return redirect(url_for("signin")) + return redirect("/signin?redirect=" + str(request.url)) else: if request.endpoint in ['signin', 'signout', 'auth', 'settings', 'update_acct', 'update_pwd', 'update_app_ip_port', 'update_wg_conf_path']: @@ -662,13 +667,11 @@ Sign In / Sign Out """ -# Sign In @app.route('/signin', methods=['GET']) def signin(): - """Sign in request. - - :return: TODO - :rtype: TODO + """ + Sign in request + @return: template """ message = "" @@ -681,46 +684,43 @@ def signin(): # Sign Out @app.route('/signout', methods=['GET']) def signout(): - """Sign out request. - - :return: TODO - :rtype: TODO """ - + Sign out request + @return: redirect back to sign in + """ if "username" in session: session.pop("username") - message = "Sign out successfully!" - return render_template('signin.html', message=message) + return redirect(url_for('signin')) -# Authentication @app.route('/auth', methods=['POST']) def auth(): - """Authentication request. - - :return: TODO - :rtype: TODO """ - + Authentication request + @return: json object indicating verifying + """ + data = request.get_json() config = get_dashboard_conf() - password = hashlib.sha256(request.form['password'].encode()) + password = hashlib.sha256(data['password'].encode()) if password.hexdigest() == config["Account"]["password"] \ - and request.form['username'] == config["Account"]["username"]: - session['username'] = request.form['username'] + and data['username'] == config["Account"]["username"]: + session['username'] = data['username'] config.clear() - return redirect(url_for("index")) - - session['message'] = "Username or Password is incorrect." + return jsonify({"status": True, "msg": ""}) config.clear() - return redirect(url_for("signin")) + return jsonify({"status": False, "msg": "Username or Password is incorrect."}) + + +""" +Index Page +""" @app.route('/', methods=['GET']) def index(): - """Index Page Related. - - :return: TODO - :rtype: TODO + """ + Index page related + @return: Template """ msg = "" if "switch_msg" in session: @@ -733,10 +733,9 @@ def index(): # Setting Page @app.route('/settings', methods=['GET']) def settings(): - """Setting Page Related. - - :return: TODO - :rtype: TODO + """ + Settings page related + @return: Template """ message = "" status = "" @@ -757,13 +756,11 @@ def settings(): peer_remote_endpoint=config.get("Peers", "remote_endpoint")) -# Update account username @app.route('/update_acct', methods=['POST']) def update_acct(): - """Change account user name. - - :return: TODO - :rtype: TODO + """ + Change dashboard username + @return: Redirect """ if len(request.form['username']) == 0: @@ -786,13 +783,12 @@ def update_acct(): return redirect(url_for("settings")) -# Update peer default settting +# Update peer default setting @app.route('/update_peer_default_config', methods=['POST']) def update_peer_default_config(): - """Change default configurations for peers. - - :return: TODO - :rtype: TODO + """ + Update new peers default setting + @return: None """ config = get_dashboard_conf() @@ -860,10 +856,9 @@ def update_peer_default_config(): # Update dashboard password @app.route('/update_pwd', methods=['POST']) def update_pwd(): - """Change account password. - - :return: TODO - :rtype: TODO + """ + Update dashboard password + @return: Redirect """ config = get_dashboard_conf() @@ -894,10 +889,11 @@ def update_pwd(): return redirect(url_for("settings")) -# Update dashboard IP and port @app.route('/update_app_ip_port', methods=['POST']) def update_app_ip_port(): - """Change port number of dashboard. + """ + Update dashboard ip and port + @return: None """ config = get_dashboard_conf() @@ -905,13 +901,15 @@ def update_app_ip_port(): config.set("Server", "app_port", request.form['app_port']) set_dashboard_conf(config) config.clear() - os.system('bash wgd.sh restart') + os.system('./wgd.sh restart') # Update WireGuard configuration file path @app.route('/update_wg_conf_path', methods=['POST']) def update_wg_conf_path(): - """Change path to dashboard configuration. + """ + Update configuration path + @return: None """ config = get_dashboard_conf() @@ -920,13 +918,14 @@ def update_wg_conf_path(): config.clear() session['message'] = "WireGuard Configuration Path Update Successfully!" session['message_status'] = "success" - os.system('bash wgd.sh restart') + os.system('./wgd.sh restart') -# Update configuration sorting @app.route('/update_dashboard_sort', methods=['POST']) def update_dashbaord_sort(): - """Configuration Page Related + """ + Update configuration sorting + @return: Boolean """ config = get_dashboard_conf() @@ -944,10 +943,10 @@ def update_dashbaord_sort(): # Update configuration refresh interval @app.route('/update_dashboard_refresh_interval', methods=['POST']) def update_dashboard_refresh_interval(): - """Change the refresh time. - - :return: Return text with result - :rtype: str + """ + Change the refresh time. + @return: Return text with result + @rtype: str """ preset_interval = ["5000", "10000", "30000", "60000"] @@ -964,12 +963,11 @@ def update_dashboard_refresh_interval(): # Configuration Page @app.route('/configuration/', methods=['GET']) def configuration(config_name): - """Show wireguard interface view. - - :param config_name: Name of WG interface - :type config_name: str - :return: TODO - :rtype: TODO + """ + Show wireguard interface view. + @param config_name: Name of WG interface + @type config_name: str + @return: Template """ config = get_dashboard_conf() @@ -1004,12 +1002,11 @@ def configuration(config_name): # Get configuration details @app.route('/get_config/', methods=['GET']) def get_conf(config_name): - """Get configuration setting of wireguard interface. - - :param config_name: Name of WG interface - :type config_name: str - :return: TODO - :rtype: TODO + """ + Get configuration setting of wireguard interface. + @param config_name: Name of WG interface + @type config_name: str + @return: TODO """ config_interface = read_conf_file_interface(config_name) @@ -1050,12 +1047,11 @@ def get_conf(config_name): # Turn on / off a configuration @app.route('/switch/', methods=['GET']) def switch(config_name): - """On/off the wireguard interface. - - :param config_name: Name of WG interface - :type config_name: str - :return: TODO - :rtype: TODO + """ + On/off the wireguard interface. + @param config_name: Name of WG interface + @type config_name: str + @return: redirects """ status = get_conf_status(config_name) @@ -1078,6 +1074,11 @@ def switch(config_name): @app.route('/add_peer_bulk/', methods=['POST']) def add_peer_bulk(config_name): + """ + Add peers by bulk + @param config_name: Configuration Name + @return: String + """ data = request.get_json() keys = data['keys'] endpoint_allowed_ip = data['endpoint_allowed_ip'] @@ -1138,9 +1139,13 @@ def add_peer_bulk(config_name): return exc.output.strip() -# Add peer @app.route('/add_peer/', methods=['POST']) def add_peer(config_name): + """ + Add Peers + @param config_name: configuration name + @return: string + """ data = request.get_json() public_key = data['public_key'] allowed_ips = data['allowed_ips'] @@ -1172,7 +1177,6 @@ def add_peer(config_name): if enable_preshared_key: now = str(datetime.now().strftime("%m%d%Y%H%M%S")) f_name = now + "_tmp_psk.txt" - print(f_name) f = open(f_name, "w+") f.write(preshared_key) f.close() @@ -1192,15 +1196,14 @@ def add_peer(config_name): return exc.output.strip() -# Remove peer @app.route('/remove_peer/', methods=['POST']) def remove_peer(config_name): - """Remove peer. - - :param config_name: Name of WG interface - :type config_name: str - :return: Return result of action or recommendations - :rtype: str + """ + Remove peer. + @param config_name: Name of WG interface + @type config_name: str + @return: Return result of action or recommendations + @rtype: str """ if get_conf_status(config_name) == "stopped": @@ -1231,15 +1234,14 @@ def remove_peer(config_name): return "true" -# Save peer settings @app.route('/save_peer_setting/', methods=['POST']) def save_peer_setting(config_name): - """Save peer configuration. + """ + Save peer configuration. - :param config_name: Name of WG interface - :type config_name: str - :return: Return status of action and text with recommendations - :rtype: TODO + @param config_name: Name of WG interface + @type config_name: str + @return: Return status of action and text with recommendations """ data = request.get_json() @@ -1296,12 +1298,12 @@ def save_peer_setting(config_name): # Get peer settings @app.route('/get_peer_data/', methods=['POST']) def get_peer_name(config_name): - """Get peer settings. + """ + Get peer settings. - :param config_name: Name of WG interface - :type config_name: str - :return: Return settings of peer - :rtype: TODO + @param config_name: Name of WG interface + @type config_name: str + @return: Return settings of peer """ data = request.get_json() @@ -1321,41 +1323,14 @@ def available_ips(config_name): return jsonify(f_available_ips(config_name)) -# Generate a private key -@app.route('/generate_peer', methods=['GET']) -def generate_peer(): - """Generate the private key for peer. - - :return: Return dict with private, public and preshared keys - :rtype: TODO - """ - - return jsonify(gen_private_key()) - - -# Generate a public key from a private key -@app.route('/generate_public_key', methods=['POST']) -def generate_public_key(): - """Generate the public key. - - :return: Return dict with public key or error message - :rtype: TODO - """ - - data = request.get_json() - private_key = data['private_key'] - return jsonify(gen_public_key(private_key)) - - # Check if both key match @app.route('/check_key_match/', methods=['POST']) def check_key_match(config_name): - """TODO - - :param config_name: Name of WG interface - :type config_name: str - :return: Return dictionary with status - :rtype: TODO + """ + Check key matches + @param config_name: Name of WG interface + @type config_name: str + @return: Return dictionary with status """ data = request.get_json() @@ -1366,6 +1341,11 @@ def check_key_match(config_name): @app.route("/qrcode/", methods=['GET']) def generate_qrcode(config_name): + """ + Generate QRCode + @param config_name: Configuration Name + @return: Template containing QRcode img + """ peer_id = request.args.get('id') get_peer = g.cur.execute( "SELECT private_key, allowed_ip, DNS, mtu, endpoint_allowed_ip, keepalive, preshared_key FROM " @@ -1396,9 +1376,13 @@ def generate_qrcode(config_name): return redirect("/configuration/" + config_name) -# Download all configuration file @app.route('/download_all/', methods=['GET']) def download_all(config_name): + """ + Download all configuration + @param config_name: Configuration Name + @return: JSON Object + """ get_peer = g.cur.execute( "SELECT private_key, allowed_ip, DNS, mtu, endpoint_allowed_ip, keepalive, preshared_key, name FROM " + config_name + " WHERE private_key != ''").fetchall() @@ -1445,6 +1429,11 @@ def download_all(config_name): # Download configuration file @app.route('/download/', methods=['GET']) def download(config_name): + """ + Download one configuration + @param config_name: Configuration name + @return: JSON object + """ peer_id = request.args.get('id') get_peer = g.cur.execute( "SELECT private_key, allowed_ip, DNS, mtu, endpoint_allowed_ip, keepalive, preshared_key, name FROM " @@ -1491,15 +1480,15 @@ def download(config_name): return jsonify({"status": False, "filename": "", "content": ""}) -# Switch peer display mode @app.route('/switch_display_mode/', methods=['GET']) def switch_display_mode(mode): - """Change display view style. + """ + Change display view style. - :param mode: Mode name - :type mode: str - :return: Return text with result - :rtype: str + @param mode: Mode name + @type mode: str + @return: Return text with result + @rtype: str """ if mode in ['list', 'grid']: @@ -1519,10 +1508,11 @@ Dashboard Tools Related # Get all IP for ping @app.route('/get_ping_ip', methods=['POST']) def get_ping_ip(): - """Get ips for network testing. + # TODO: convert return to json object - :return: TODO - :rtype: TODO + """ + Get ips for network testing. + @return: HTML containing a list of IPs """ config = request.form['config'] @@ -1545,10 +1535,10 @@ def get_ping_ip(): # Ping IP @app.route('/ping_ip', methods=['POST']) def ping_ip(): - """Execute ping command. - - :return: Return text with result - :rtype: str + """ + Execute ping command. + @return: Return text with result + @rtype: str """ try: @@ -1573,10 +1563,11 @@ def ping_ip(): # Traceroute IP @app.route('/traceroute_ip', methods=['POST']) def traceroute_ip(): - """Execute ping traceroute command. + """ + Execute ping traceroute command. - :return: Return text with result - :rtype: str + @return: Return text with result + @rtype: str """ try: @@ -1600,21 +1591,22 @@ Dashboard Initialization def init_dashboard(): - """Create dashboard default configuration. + """ + Create dashboard default configuration. """ # Set Default INI File if not os.path.isfile(DASHBOARD_CONF): open(DASHBOARD_CONF, "w+").close() config = get_dashboard_conf() - # Defualt dashboard account setting + # Default dashboard account setting if "Account" not in config: config['Account'] = {} if "username" not in config['Account']: config['Account']['username'] = 'admin' if "password" not in config['Account']: config['Account']['password'] = '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918' - # Defualt dashboard server setting + # Default dashboard server setting if "Server" not in config: config['Server'] = {} if 'wg_conf_path' not in config['Server']: @@ -1651,24 +1643,28 @@ def init_dashboard(): def check_update(): - """Dashboard check update + """ + Dashboard check update - :return: Retunt text with result - :rtype: str + @return: Retunt text with result + @rtype: str """ config = get_dashboard_conf() - data = urllib.request.urlopen("https://api.github.com/repos/donaldzou/WGDashboard/releases").read() - output = json.loads(data) - release = [] - for i in output: - if not i["prerelease"]: - release.append(i) - if config.get("Server", "version") == release[0]["tag_name"]: - result = "false" - else: - result = "true" + try: + data = urllib.request.urlopen("https://api.github.com/repos/donaldzou/WGDashboard/releases").read() + output = json.loads(data) + release = [] + for i in output: + if not i["prerelease"]: + release.append(i) + if config.get("Server", "version") == release[0]["tag_name"]: + result = "false" + else: + result = "true" - return result + return result + except urllib.error.HTTPError: + return "false" """ diff --git a/src/requirements.txt b/src/requirements.txt index 6bf8c79..5d3b347 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,5 +1,4 @@ Flask -tinydb==4.5.2 ifcfg icmplib flask-qrcode diff --git a/src/templates/signin.html b/src/templates/signin.html index 4d6835b..da86297 100644 --- a/src/templates/signin.html +++ b/src/templates/signin.html @@ -16,11 +16,10 @@