diff --git a/.gitignore b/.gitignore index 5afb0bad..4cecc0d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ .vscode/sftp.json src/.vscode/sftp.json .DS_Store -wg.db -*.json .idea -src/test.py -tmp +src/db __pycache__ +src/test.py +*.db src/wg-dashboard.ini src/static/pic.xd *.conf @@ -15,3 +14,5 @@ public_key.txt venv/** log/** release/* +src/db/wgdashboard.db +.jshintrc \ No newline at end of file diff --git a/README.md b/README.md index cad73930..a5c12a3f 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,9 @@ -
Monitoring WireGuard is not convinient, need to login into server and type wg show. That's why this platform is being created, to view all configurations and manage them in a easier way.
Note: This project is not affiliate to the official WireGuard Project ;)
-## 📣 What's New: v2.3 +## 📣 What's New: v3.0 - 🎉 **New Features** - - **Update directly from `wgd.sh`:** Now you can update WGDashboard directly from the bash script. - - **Displaying Peers:** You can switch the display mode between list and table in the configuration page. + - **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. + - **Added Some [🥘 Experimental Functions](#-experimental-functions)** + - 🪚 **Bug Fixed** - - [Peer DNS Validation Fails #67](issues/67): Added DNS format check. [❤️ @realfian] - - [configparser.NoSectionError: No section: 'Interface' #66](issues/66): Changed permission requirement for `etc/wireguard` from `744` to `755`. [❤️ @ramalmaty] - - [Feature request: Interface not loading when information missing #73](issues/73): Fixed when Configuration Address and Listen Port is missing will crash the dashboard. [❤️ @js32] - - [Remote Peer, MTU and PersistentKeepalives added #70](pull/70): Added MTU, remote peer and Persistent Keepalive. [❤️ @realfian] - - [Fixes DNS check to support search domain #65](pull/65): Added allow input domain into DNS. [❤️@davejlong] + - [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** - - Moved Add Peer Button into the right bottom corner. + - **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). + - **UI adjustment on running peers**: peers will have a new style indicating that it is running. + - **`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: + +**For users who is using `v2.x.x` please be sure to read [this](#please-note-for-user-who-is-using-v231-or-below) before updating WGDashboard ;)**
+
+
+
## 🔍 Screenshot

@@ -346,7 +383,7 @@ Endpoint = 0.0.0.0:51820

-
+

@@ -354,10 +391,26 @@ Endpoint = 0.0.0.0:51820

-
-
## ⏰ Changelog
+#### v2.3.1 - Sep 8, 2021
+
+- Updated dashboard's name to **WGDashboard**!!
+
+#### v2.3 - Sep 8, 2021
+
+- 🎉 **New Features**
+ - **Update directly from `wgd.sh`:** Now you can update WGDashboard directly from the bash script.
+ - **Displaying Peers:** You can switch the display mode between list and table in the configuration page.
+- 🪚 **Bug Fixed**
+ - [Peer DNS Validation Fails #67](issues/67): Added DNS format check. [❤️ @realfian]
+ - [configparser.NoSectionError: No section: 'Interface' #66](issues/66): Changed permission requirement for `etc/wireguard` from `744` to `755`. [❤️ @ramalmaty]
+ - [Feature request: Interface not loading when information missing #73](issues/73): Fixed when Configuration Address and Listen Port is missing will crash the dashboard. [❤️ @js32]
+ - [Remote Peer, MTU and PersistentKeepalives added #70](pull/70): Added MTU, remote peer and Persistent Keepalive. [❤️ @realfian]
+ - [Fixes DNS check to support search domain #65](pull/65): Added allow input domain into DNS. [❤️@davejlong]
+- **🧐 Other Changes**
+ - Moved Add Peer Button into the right bottom corner.
+
#### v2.2.1 - Aug 16, 2021
Bug Fixed:
@@ -425,7 +478,6 @@ Bug Fixed:
- [jQuery](https://jquery.com) `v3.5.1`
- Python
- [Flask](https://pypi.org/project/Flask/) `v2.0.1`
- - [TinyDB](https://pypi.org/project/tinydb/) `v4.3.0`
- [ifcfg](https://pypi.org/project/ifcfg/) `v0.21`
- [icmplib](https://pypi.org/project/icmplib/) `v2.1.1`
- [flask-qrcode](https://pypi.org/project/Flask-QRcode/) `v3.0.0`
diff --git a/img/AddPeer.png b/img/AddPeer.png
index e130b015..d4627ce1 100644
Binary files a/img/AddPeer.png and b/img/AddPeer.png differ
diff --git a/img/Configuration.png b/img/Configuration.png
index 393c4bfc..317a68b3 100644
Binary files a/img/Configuration.png and b/img/Configuration.png differ
diff --git a/img/DashboardSetting.png b/img/DashboardSetting.png
index b09ced3e..463f947f 100644
Binary files a/img/DashboardSetting.png and b/img/DashboardSetting.png differ
diff --git a/img/DeleteBulk.png b/img/DeleteBulk.png
new file mode 100644
index 00000000..afa2f538
Binary files /dev/null and b/img/DeleteBulk.png differ
diff --git a/img/EditPeer.png b/img/EditPeer.png
index 9bb9858d..fed3bb34 100644
Binary files a/img/EditPeer.png and b/img/EditPeer.png differ
diff --git a/img/HomePage.png b/img/HomePage.png
index 41c64dc5..2eab836c 100644
Binary files a/img/HomePage.png and b/img/HomePage.png differ
diff --git a/img/PWA.gif b/img/PWA.gif
new file mode 100644
index 00000000..bcc0f4b9
Binary files /dev/null and b/img/PWA.gif differ
diff --git a/img/QRCode.png b/img/QRCode.png
index 004e27b1..f2d8b6d5 100644
Binary files a/img/QRCode.png and b/img/QRCode.png differ
diff --git a/img/SearchIP.png b/img/SearchIP.png
new file mode 100644
index 00000000..0c78d659
Binary files /dev/null and b/img/SearchIP.png differ
diff --git a/src/certbot.ini b/src/certbot.ini
new file mode 100644
index 00000000..cf9b7a28
--- /dev/null
+++ b/src/certbot.ini
@@ -0,0 +1,4 @@
+authenticator = standalone
+noninteractive = true
+agree-tos = true
+rsa-key-size = 2048
\ No newline at end of file
diff --git a/src/dashboard.py b/src/dashboard.py
index 728db1af..1e4dbdb0 100644
--- a/src/dashboard.py
+++ b/src/dashboard.py
@@ -1,8 +1,10 @@
"""
-< WGDashboard > - by Donald Zou [https://github.com/donaldzou]
+< WGDashboard > - Copyright(C) 2021 Donald Zou [https://github.com/donaldzou]
Under Apache-2.0 License
"""
+import sqlite3
+from flask import g
import configparser
import hashlib
import ipaddress
@@ -11,20 +13,18 @@ import json
import os
import secrets
import subprocess
-import threading
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
import ifcfg
from flask import Flask, request, render_template, redirect, url_for, session, jsonify
from flask_qrcode import QRcode
from icmplib import ping, traceroute
-from tinydb import TinyDB, Query
# Import other python files
from util import regex_match, check_DNS, check_Allowed_IPs, check_remote_endpoint, \
@@ -32,7 +32,7 @@ from util import regex_match, check_DNS, check_Allowed_IPs, check_remote_endpoin
# Dashboard Version
DASHBOARD_VERSION = 'v3.0'
-# WireGuard configuration path
+# WireGuard's configuration path
WG_CONF_PATH = None
# Dashboard Config Name
configuration_path = os.getenv('CONFIGURATION_PATH', '.')
@@ -50,49 +50,49 @@ app.config['TEMPLATES_AUTO_RELOAD'] = True
# Enable QR Code Generator
QRcode(app)
-# TODO: Testing semaphore on reading/writing database
-sem = threading.RLock()
+# TODO: use class and object oriented programming
-# TODO use class and object oriented programming
-
-# Read / Write Dashboard Config File
-def get_dashboard_conf():
- """Dashboard Configuration Related
-
- :return: A config parser object
- :rtype: configparser.ConfigParser
+def connect_db():
"""
+ Connect to the database
+ @return: sqlite3.Connection
+ """
+ return sqlite3.connect(os.path.join(configuration_path, 'db', 'wgdashboard.db'))
+
+def get_dashboard_conf():
+ """
+ Get dashboard configuration
+ @return: configparser.ConfigParser
+ """
config = configparser.ConfigParser(strict=False)
config.read(DASHBOARD_CONF)
return config
def set_dashboard_conf(config):
- """Configuration writer
-
- :param config: A config parser object
- :type config: configparser.ConfigParser
"""
-
+ Write to configuration
+ @param config: Input configuration
+ """
with open(DASHBOARD_CONF, "w", encoding='utf-8') as conf_object:
config.write(conf_object)
# Get all keys from a configuration
def get_conf_peer_key(config_name):
- """Get the peers keys of wireguard interface.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return list of peers keys or text if configuration not running
- :rtype: list, str
+ """
+ Get the peers keys of wireguard interface.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return list of peers keys or text if configuration not running
+ @rtype: list, str
"""
try:
- peers_keys = subprocess.run(f"wg show {config_name} peers",
- check=True, shell=True, capture_output=True).stdout
+ peers_keys = subprocess.check_output(f"wg show {config_name} peers",
+ shell=True, stderr=subprocess.STDOUT)
peers_keys = peers_keys.decode("UTF-8").split()
return peers_keys
except subprocess.CalledProcessError:
@@ -101,19 +101,19 @@ def get_conf_peer_key(config_name):
# Get numbers of connected peer of a configuration
def get_conf_running_peer_number(config_name):
- """Get number of running peers on wireguard interface.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Number of running peers, or test if configuration not running
- :rtype: int, str
+ """
+ Get number of running peers on wireguard interface.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Number of running peers, or test if configuration not running
+ @rtype: int, str
"""
running = 0
# Get latest handshakes
try:
- data_usage = subprocess.run(f"wg show {config_name} latest-handshakes",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} latest-handshakes",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split()
@@ -128,15 +128,14 @@ 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):
- """Get interface settings.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Dictionary with interface settings
- :rtype: dict
+ """
+ Get interface settings.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Dictionary with interface settings
+ @rtype: dict
"""
conf_location = WG_CONF_PATH + "/" + config_name + ".conf"
@@ -153,15 +152,13 @@ def read_conf_file_interface(config_name):
return data
-# TODO use modules for working with ini(configparser or wireguard)
-# Read the whole configuration file
def read_conf_file(config_name):
- """Get configurations from file of wireguard interface.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Dictionary with interface and peers settings
- :rtype: dict
+ """
+ Get configurations from file of wireguard interface.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Dictionary with interface and peers settings
+ @rtype: dict
"""
# Read Configuration File Start
@@ -174,7 +171,7 @@ def read_conf_file(config_name):
}
peers_start = 0
for i in range(len(file)):
- if not regex_match("#(.*)", file[i]):
+ if not regex_match("#(.*)", file[i]) and regex_match(";(.*)", file[i]):
if file[i] == "[Peer]":
peers_start = i
break
@@ -187,7 +184,7 @@ def read_conf_file(config_name):
conf_peers = file[peers_start:]
peer = -1
for i in conf_peers:
- if not regex_match("#(.*)", i):
+ if not regex_match("#(.*)", i) and not regex_match(";(.*)", i):
if i == "[Peer]":
peer += 1
conf_peer_data["Peers"].append({})
@@ -202,23 +199,17 @@ def read_conf_file(config_name):
return conf_peer_data
-# Get latest handshake from all peers of a configuration
-def get_latest_handshake(config_name, db, peers):
- """Update latest handshake of peers.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :param db: Connector for database
- :type db: tinydb.TinyDB
- :param peers: TODO
- :return: Return string if stopped or None if OK.
- :rtype: string, None
+def get_latest_handshake(config_name):
+ """
+ Get the latest handshake from all peers of a configuration
+ @param config_name: Configuration name
+ @return: str
"""
# Get latest handshakes
try:
- data_usage = subprocess.run(f"wg show {config_name} latest-handshakes",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} latest-handshakes",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split()
@@ -232,32 +223,24 @@ def get_latest_handshake(config_name, db, peers):
else:
status = "stopped"
if int(data_usage[count + 1]) > 0:
- db.update({"latest_handshake": str(minus).split(".", maxsplit=1)[0], "status": status},
- peers.id == data_usage[count])
+ g.cur.execute("UPDATE %s SET latest_handshake = '%s', status = '%s' WHERE id='%s'"
+ % (config_name, str(minus).split(".", maxsplit=1)[0], status, data_usage[count]))
else:
- db.update({"latest_handshake": "(None)", "status": status}, peers.id == data_usage[count])
+ g.cur.execute("UPDATE %s SET latest_handshake = '(None)', status = '%s' WHERE id='%s'"
+ % (config_name, status, data_usage[count]))
count += 2
- return None
-
-# Get transfer from all peers of a configuration
-def get_transfer(config_name, db, peers):
- """Update transfer data values of peers.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :param db: Connector for database
- :type db: tinydb.TinyDB
- :param peers: TODO
- :return: Return string if stopped or None if OK.
- :rtype: string, None
+def get_transfer(config_name):
+ """
+ Get transfer from all peers of a configuration
+ @param config_name: Configuration name
+ @return: str
"""
-
# Get transfer
try:
- data_usage = subprocess.run(f"wg show {config_name} transfer",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} transfer",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split("\n")
@@ -266,222 +249,185 @@ def get_transfer(config_name, db, peers):
final.append(i.split("\t"))
data_usage = final
for i in range(len(data_usage)):
- cur_i = db.search(peers.id == data_usage[i][0])
+ cur_i = g.cur.execute(
+ "SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM %s WHERE id='%s'"
+ % (config_name, data_usage[i][0])).fetchall()
if len(cur_i) > 0:
- total_sent = cur_i[0]['total_sent']
- total_receive = cur_i[0]['total_receive']
- traffic = cur_i[0]['traffic']
+ total_sent = cur_i[0][1]
+ total_receive = cur_i[0][0]
cur_total_sent = round(int(data_usage[i][2]) / (1024 ** 3), 4)
cur_total_receive = round(int(data_usage[i][1]) / (1024 ** 3), 4)
- if cur_i[0]["status"] == "running":
+ if cur_i[0][4] == "running":
if total_sent <= cur_total_sent and total_receive <= cur_total_receive:
total_sent = cur_total_sent
total_receive = cur_total_receive
else:
- now = datetime.now()
- ctime = now.strftime("%d/%m/%Y %H:%M:%S")
- traffic.append(
- {
- "time": ctime, "total_receive": round(total_receive, 4),
- "total_sent": round(total_sent, 4),
- "total_data": round(total_receive + total_sent, 4)
- }
- )
+ cumulative_receive = cur_i[0][2] + total_receive
+ cumulative_sent = cur_i[0][3] + total_sent
+ g.cur.execute("UPDATE %s SET cumu_receive = %f, cumu_sent = %f, cumu_data = %f WHERE id = '%s'" %
+ (config_name, round(cumulative_receive, 4), round(cumulative_sent, 4),
+ round(cumulative_sent + cumulative_receive, 4), data_usage[i][0]))
total_sent = 0
total_receive = 0
- db.update({"traffic": traffic}, peers.id == data_usage[i][0])
- db.update({"total_receive": round(total_receive, 4), "total_sent": round(total_sent, 4),
- "total_data": round(total_receive + total_sent, 4)}, peers.id == data_usage[i][0])
- return None
+ g.cur.execute("UPDATE %s SET total_receive = %f, total_sent = %f, total_data = %f WHERE id = '%s'" %
+ (config_name, round(total_receive, 4), round(total_sent, 4),
+ round(total_receive + total_sent, 4), data_usage[i][0]))
-# Get endpoint from all peers of a configuration
-def get_endpoint(config_name, db, peers):
- """Get endpoint address for peers.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :param db: Connector for database
- :type db: tinydb.TinyDB
- :param peers: TODO
- :return: Return string if stopped or None if OK.
- :rtype: string, None
+def get_endpoint(config_name):
+ """
+ Get endpoint from all peers of a configuration
+ @param config_name: Configuration name
+ @return: str
"""
-
# Get endpoint
try:
- data_usage = subprocess.run(f"wg show {config_name} endpoints",
- check=True, shell=True, capture_output=True).stdout
+ data_usage = subprocess.check_output(f"wg show {config_name} endpoints",
+ shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
return "stopped"
data_usage = data_usage.decode("UTF-8").split()
count = 0
for _ in range(int(len(data_usage) / 2)):
- db.update({"endpoint": data_usage[count + 1]}, peers.id == data_usage[count])
+ g.cur.execute("UPDATE " + config_name + " SET endpoint = '%s' WHERE id = '%s'"
+ % (data_usage[count + 1], data_usage[count]))
count += 2
- return None
-
-# Get allowed ips from all peers of a configuration
-def get_allowed_ip(db, peers, conf_peer_data):
- """Get allowed subnets or ips for peers.
-
- :param peers: TODO
- :param db: Connector for database
- :type db: tinydb.TinyDB
- :param conf_peer_data: Dictionary with configurations fot peers and interfaces
- :type conf_peer_data" dict
+def get_allowed_ip(conf_peer_data, config_name):
+ """
+ Get allowed ips from all peers of a configuration
+ @param conf_peer_data: Configuration peer data
+ @param config_name: Configuration name
+ @return: None
"""
-
# Get allowed ip
for i in conf_peer_data["Peers"]:
- db.update({"allowed_ip": i.get('AllowedIPs', '(None)')}, peers.id == i["PublicKey"])
+ g.cur.execute("UPDATE " + config_name + " SET allowed_ip = '%s' WHERE id = '%s'"
+ % (i.get('AllowedIPs', '(None)'), i["PublicKey"]))
-# Look for new peers from WireGuard
def get_all_peers_data(config_name):
- """Get all settings fot peer.
-
- :param config_name: Name of WG interface
- :type config_name: str
"""
-
- sem.acquire(timeout=1)
- db = TinyDB(os.path.join(DB_PATH, config_name + '.json'))
- peers = Query()
+ Look for new peers from WireGuard
+ @param config_name: Configuration name
+ @return: None
+ """
conf_peer_data = read_conf_file(config_name)
config = get_dashboard_conf()
- for i in conf_peer_data['Peers']:
- search = db.search(peers.id == i['PublicKey'])
- if not search:
- new_data = {
- "id": i['PublicKey'],
- "private_key": "",
- "DNS": config.get("Peers", "peer_global_DNS"),
- "endpoint_allowed_ip": config.get("Peers", "peer_endpoint_allowed_ip"),
- "name": "",
- "total_receive": 0,
- "total_sent": 0,
- "total_data": 0,
- "endpoint": "N/A",
- "status": "stopped",
- "latest_handshake": "N/A",
- "allowed_ip": "N/A",
- "traffic": [],
- "mtu": config.get("Peers", "peer_mtu"),
- "keepalive": config.get("Peers", "peer_keep_alive"),
- "remote_endpoint": config.get("Peers", "remote_endpoint"),
- "preshared_key": ""
- }
- if "PresharedKey" in i.keys():
- new_data["preshared_key"] = i["PresharedKey"]
- db.insert(new_data)
+ failed_index = []
+ for i in range(len(conf_peer_data['Peers'])):
+ if "PublicKey" in conf_peer_data['Peers'][i].keys():
+ result = g.cur.execute(
+ "SELECT * FROM %s WHERE id='%s'" % (config_name, conf_peer_data['Peers'][i]["PublicKey"])).fetchall()
+ if len(result) == 0:
+ new_data = {
+ "id": conf_peer_data['Peers'][i]['PublicKey'],
+ "private_key": "",
+ "DNS": config.get("Peers", "peer_global_DNS"),
+ "endpoint_allowed_ip": config.get("Peers", "peer_endpoint_allowed_ip"),
+ "name": "",
+ "total_receive": 0,
+ "total_sent": 0,
+ "total_data": 0,
+ "endpoint": "N/A",
+ "status": "stopped",
+ "latest_handshake": "N/A",
+ "allowed_ip": "N/A",
+ "cumu_receive": 0,
+ "cumu_sent": 0,
+ "cumu_data": 0,
+ "traffic": [],
+ "mtu": config.get("Peers", "peer_mtu"),
+ "keepalive": config.get("Peers", "peer_keep_alive"),
+ "remote_endpoint": config.get("Peers", "remote_endpoint"),
+ "preshared_key": ""
+ }
+ if "PresharedKey" in conf_peer_data['Peers'][i].keys():
+ new_data["preshared_key"] = conf_peer_data['Peers'][i]["PresharedKey"]
+ sql = f"""
+ INSERT INTO {config_name}
+ VALUES (:id, :private_key, :DNS, :endpoint_allowed_ip, :name, :total_receive, :total_sent,
+ :total_data, :endpoint, :status, :latest_handshake, :allowed_ip, :cumu_receive, :cumu_sent,
+ :cumu_data, :mtu, :keepalive, :remote_endpoint, :preshared_key);
+ """
+ g.cur.execute(sql, new_data)
else:
- # Update database since V2.2
- update_db = {}
- # Required peer settings
- if "DNS" not in search[0]:
- update_db['DNS'] = config.get("Peers", "peer_global_DNS")
- if "endpoint_allowed_ip" not in search[0]:
- update_db['endpoint_allowed_ip'] = config.get("Peers", "peer_endpoint_allowed_ip")
- # Not required peers settings (Only for QR code)
- if "private_key" not in search[0]:
- update_db['private_key'] = ''
- if "mtu" not in search[0]:
- update_db['mtu'] = config.get("Peers", "peer_mtu")
- if "keepalive" not in search[0]:
- update_db['keepalive'] = config.get("Peers", "peer_keep_alive")
- if "remote_endpoint" not in search[0]:
- update_db['remote_endpoint'] = config.get("Peers", "remote_endpoint")
- if "preshared_key" not in search[0]:
- if "PresharedKey" in i.keys():
- update_db['preshared_key'] = i["PresharedKey"]
- else:
- update_db['preshared_key'] = ""
- db.update(update_db, peers.id == i['PublicKey'])
+ print("Trying to parse a peer doesn't have public key...")
+ failed_index.append(i)
+ for i in failed_index:
+ conf_peer_data['Peers'].pop(i)
# Remove peers no longer exist in WireGuard configuration file
- db_key = list(map(lambda a: a['id'], db.all()))
+ db_key = list(map(lambda a: a[0], g.cur.execute("SELECT id FROM %s" % config_name)))
wg_key = list(map(lambda a: a['PublicKey'], conf_peer_data['Peers']))
for i in db_key:
if i not in wg_key:
- db.remove(peers.id == i)
- tic = time.perf_counter()
- get_latest_handshake(config_name, db, peers)
- get_transfer(config_name, db, peers)
- get_endpoint(config_name, db, peers)
- get_allowed_ip(db, peers, conf_peer_data)
- toc = time.perf_counter()
- print(f"Finish fetching data in {toc - tic:0.4f} seconds")
- db.close()
- try:
- sem.release()
- except RuntimeError as e:
- print("RuntimeError: cannot release un-acquired lock")
+ g.cur.execute("DELETE FROM %s WHERE id = '%s'" % (config_name, i))
+ get_latest_handshake(config_name)
+ get_transfer(config_name)
+ get_endpoint(config_name)
+ get_allowed_ip(conf_peer_data, config_name)
-# Search for peers
def get_peers(config_name, search, sort_t):
- """Get all peers.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :param search: Search string
- :type search: str
- :param sort_t: TODO
- :type sort_t: str
- :return: TODO
"""
-
+ Get all peers.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @param search: Search string
+ @type search: str
+ @param sort_t: Sorting tag
+ @type sort_t: str
+ @return: list
+ """
+ tic = time.perf_counter()
+ col = g.cur.execute("PRAGMA table_info(" + config_name + ")").fetchall()
+ col = [a[1] for a in col]
get_all_peers_data(config_name)
- sem.acquire(timeout=1)
- db = TinyDB(os.path.join(DB_PATH, config_name + ".json"))
- peer = Query()
if len(search) == 0:
- result = db.all()
+ data = g.cur.execute("SELECT * FROM " + config_name).fetchall()
+ result = [{col[i]: data[k][i] for i in range(len(col))} for k in range(len(data))]
else:
- result = db.search(peer.name.matches('(.*)(' + re.escape(search) + ')(.*)'))
+ sql = "SELECT * FROM " + config_name + " WHERE name LIKE '%" + search + "%'"
+ data = g.cur.execute(sql).fetchall()
+ result = [{col[i]: data[k][i] for i in range(len(col))} for k in range(len(data))]
if sort_t == "allowed_ip":
- result = sorted(result, key=lambda d: ipaddress.ip_network(d[sort_t].split(",")[0]))
+ result = sorted(result, key=lambda d: ipaddress.ip_network(
+ "0.0.0.0/0" if d[sort_t].split(",")[0] == "(None)" else d[sort_t].split(",")[0]))
else:
result = sorted(result, key=lambda d: d[sort_t])
- db.close()
- try:
- sem.release()
- except RuntimeError as e:
- print("RuntimeError: cannot release un-acquired lock")
+ toc = time.perf_counter()
+ print(f"Finish fetching peers in {toc - tic:0.4f} seconds")
return result
-# Get configuration public key
def get_conf_pub_key(config_name):
- """Get public key for configuration.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return public key or empty string
- :rtype: str
+ """
+ Get public key for configuration.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return public key or empty string
+ @rtype: str
"""
try:
conf = configparser.ConfigParser(strict=False)
conf.read(WG_CONF_PATH + "/" + config_name + ".conf")
pri = conf.get("Interface", "PrivateKey")
- pub = subprocess.run(f"echo '{pri}' | wg pubkey", check=True, shell=True, capture_output=True).stdout
+ pub = subprocess.check_output(f"echo '{pri}' | wg pubkey", shell=True, stderr=subprocess.STDOUT)
conf.clear()
return pub.decode().strip("\n")
- except configparser.NoSectionError as e:
+ except configparser.NoSectionError:
return ""
-# Get configuration listen port
def get_conf_listen_port(config_name):
- """Get listen port number.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return number of port or empty string
- :rtype: str
+ """
+ Get listen port number.
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return number of port or empty string
+ @rtype: str
"""
conf = configparser.ConfigParser(strict=False)
@@ -491,63 +437,66 @@ def get_conf_listen_port(config_name):
port = conf.get("Interface", "ListenPort")
except (configparser.NoSectionError, configparser.NoOptionError):
if get_conf_status(config_name) == "running":
- port = subprocess.run(f"wg show {config_name} listen-port",
- check=True, shell=True, capture_output=True).stdout
+ port = subprocess.check_output(f"wg show {config_name} listen-port",
+ shell=True, stderr=subprocess.STDOUT)
port = port.decode("UTF-8")
conf.clear()
return port
-# Get configuration total data
def get_conf_total_data(config_name):
- """Get total transferred data via wireguard interface.
-
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Dictionary with total_sent, total_receive, total io
- :rtype: dict
"""
-
- sem.acquire(timeout=1)
- db = TinyDB(os.path.join(DB_PATH, config_name + ".json"))
+ 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
- for i in db.all():
- upload_total += i['total_sent']
- download_total += i['total_receive']
- for k in i['traffic']:
- upload_total += k['total_sent']
- download_total += k['total_receive']
+ for i in data.fetchall():
+ upload_total += i[0]
+ download_total += i[1]
+ upload_total += i[2]
+ download_total += i[3]
total = round(upload_total + download_total, 4)
upload_total = round(upload_total, 4)
download_total = round(download_total, 4)
- db.close()
- try:
- sem.release()
- except RuntimeError as e:
- print("RuntimeError: cannot release un-acquired lock")
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 = []
for i in os.listdir(WG_CONF_PATH):
if regex_match("^(.{1,}).(conf)$", i):
i = i.replace('.conf', '')
+ create_table = f"""
+ CREATE TABLE IF NOT EXISTS {i} (
+ id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL,
+ endpoint_allowed_ip VARCHAR NULL, name VARCHAR NULL, total_receive FLOAT NULL,
+ total_sent FLOAT NULL, total_data FLOAT NULL, endpoint VARCHAR NULL,
+ status VARCHAR NULL, latest_handshake VARCHAR NULL, allowed_ip VARCHAR NULL,
+ cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, mtu INT NULL,
+ keepalive INT NULL, remote_endpoint VARCHAR NULL, preshared_key VARCHAR NULL,
+ PRIMARY KEY (id)
+ )
+ """
+ g.cur.execute(create_table)
temp = {"conf": i, "status": get_conf_status(i), "public_key": get_conf_pub_key(i)}
if temp['status'] == "running":
temp['checked'] = 'checked'
@@ -559,40 +508,19 @@ def get_conf_list():
return conf
-# Generate private key
-def gen_private_key():
- """Generate the private key.
-
- :return: Return dict with private, public and preshared keys
- :rtype: dict
- """
-
- subprocess.run('wg genkey > private_key.txt && wg pubkey < private_key.txt > public_key.txt',
- check=True, shell=True)
- gen_psk = subprocess.run('wg genpsk', shell=True, check=True, capture_output=True).stdout
- preshare_key = gen_psk.decode("UTF-8").strip()
- 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, "preshared_key": preshare_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:
file_object.write(private_key)
try:
- subprocess.run("wg pubkey < private_key.txt > public_key.txt", check=True, shell=True)
+ subprocess.check_output("wg pubkey < private_key.txt > public_key.txt", shell=True)
with open('public_key.txt', encoding='utf-8') as file_object:
public_key = file_object.readline().strip()
os.remove('private_key.txt')
@@ -603,96 +531,111 @@ def gen_public_key(private_key):
return {"status": 'failed', "msg": "Key is not the correct length or format", "data": ""}
-# Check if private key and public key match
def f_check_key_match(private_key, public_key, config_name):
- """TODO
-
- :param private_key: Private key
- :type private_key: str
- :param public_key: Public key
- :type public_key: str
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return dictionary with status
- :rtype: dict
+ """
+ Check if private key and public key match
+ @param private_key: Private key
+ @type private_key: str
+ @param public_key: Public key
+ @type public_key: str
+ @param config_name: Name of WG interface
+ @type config_name: str
+ @return: Return dictionary with status
+ @rtype: dict
"""
result = gen_public_key(private_key)
if result['status'] == 'failed':
return result
else:
- sem.acquire(timeout=1)
- db = TinyDB(os.path.join(DB_PATH, config_name + ".json"))
- peers = Query()
- match = db.search(peers.id == result['data'])
+ sql = "SELECT * FROM " + config_name + " WHERE id = ?"
+ match = g.cur.execute(sql, (result['data'],)).fetchall()
if len(match) != 1 or result['data'] != public_key:
- db.close()
- try:
- sem.release()
- except RuntimeError as e:
- print("RuntimeError: cannot release un-acquired lock")
return {'status': 'failed', 'msg': 'Please check your private key, it does not match with the public key.'}
else:
- db.close()
- try:
- sem.release()
- except RuntimeError as e:
- print("RuntimeError: cannot release un-acquired lock")
return {'status': 'success'}
-# Check if there is repeated allowed IP
def check_repeat_allowed_ip(public_key, ip, config_name):
- """Check for the existence of an allowed ip.
-
- :param public_key: Public key
- :type public_key: str
- :param ip: allowed ip
- :type ip: str
- :param config_name: Name of WG interface
- :type config_name: str
- :return: Return dictionary with status
- :rtype: dict
"""
-
- sem.acquire(timeout=1)
- db = TinyDB(os.path.join(DB_PATH, config_name + ".json"))
- peers = Query()
- peer = db.search(peers.id == public_key)
- if len(peer) != 1:
+ 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'}
else:
- existed_ip = db.search((peers.id != public_key) & (peers.allowed_ip == ip))
- if len(existed_ip) != 0:
- db.close()
- try:
- sem.release()
- except RuntimeError as e:
- print("RuntimeError: cannot release un-acquired lock")
+ existed_ip = g.cur.execute("SELECT COUNT(*) FROM " +
+ config_name + " WHERE id != ? AND allowed_ip LIKE '" + ip + "/%'", (public_key,)) \
+ .fetchone()
+ if existed_ip[0] != 0:
return {'status': 'failed', 'msg': "Allowed IP already taken by another peer."}
else:
- db.close()
- try:
- sem.release()
- except RuntimeError as e:
- print("RuntimeError: cannot release un-acquired lock")
return {'status': 'success'}
+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 = []
+ conf_address = config_interface['Address']
+ address = conf_address.split(',')
+ for i in address:
+ add, sub = i.split("/")
+ existed.append(ipaddress.ip_address(add))
+ peers = g.cur.execute("SELECT allowed_ip FROM " + config_name).fetchall()
+ for i in peers:
+ add = i[0].split(",")
+ for k in add:
+ a, s = k.split("/")
+ existed.append(ipaddress.ip_address(a.strip()))
+ available = list(ipaddress.ip_network(address[0], False).hosts())
+ for i in existed:
+ try:
+ available.remove(i)
+ except ValueError:
+ pass
+ available = [str(i) for i in available]
+ return available
+ else:
+ return []
+
+
"""
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()
+
+
# Before request
@app.before_request
def auth_req():
- """ TODO
-
- :return: TODO
- :rtype: str, None
"""
-
+ Action before every request
+ @return: Redirect
+ """
+ if getattr(g, 'db', None) is None:
+ g.db = connect_db()
+ g.cur = g.db.cursor()
conf = get_dashboard_conf()
req = conf.get("Server", "auth_req")
session['update'] = UPDATE
@@ -703,13 +646,13 @@ def auth_req():
request.endpoint != "signout" and \
request.endpoint != "auth" and \
"username" not in session:
- print("User not loggedin - Attemped access: " + str(request.endpoint))
+ print("User not signed in - Attempted access: " + str(request.endpoint))
if request.endpoint != "index":
session['message'] = "You need to sign in first!"
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']:
@@ -724,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 = ""
@@ -743,58 +684,58 @@ 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:
+ msg = session["switch_msg"]
+ session.pop("switch_msg")
- return render_template('index.html', conf=get_conf_list())
+ return render_template('index.html', conf=get_conf_list(), msg=msg)
# Setting Page
@app.route('/settings', methods=['GET'])
def settings():
- """Setting Page Related.
-
- :return: TODO
- :rtype: TODO
+ """
+ Settings page related
+ @return: Template
"""
message = ""
status = ""
@@ -815,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:
@@ -844,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()
@@ -918,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()
@@ -952,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()
@@ -963,13 +901,16 @@ 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')
+ subprocess.Popen('bash wgd.sh restart', shell=True)
+ return ""
# 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()
@@ -978,13 +919,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')
+ subprocess.Popen('bash wgd.sh restart', shell=True)
-# 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()
@@ -1002,10 +944,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"]
@@ -1022,12 +964,11 @@ def update_dashboard_refresh_interval():
# Configuration Page
@app.route('/configuration/