diff --git a/.gitignore b/.gitignore index 5afb0ba..4cecc0d 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 cad7393..a5c12a3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,9 @@ -
- -> Hi! I'm planning the next major update for this project, please let me know if you have any suggestions or feature requests ;) You can create an issue with the "Feature request" template. Cheers! - -### Help Wanted - -> If anyone know a better way to distribute releases of python application other than GitHub, please let me know in #103! - -> Please provide your OS name and version if you can run the dashboard on it perfectly in #31, since I only tested on Ubuntu. Thank you! - -

WGDashboard

WGDashboard

+

@@ -24,27 +14,42 @@

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 ;)**
- ## Table of Content + - [💡 Features](#-features) - [📝 Requirement](#-requirement) - [🛠 Install](#-install) @@ -55,6 +60,7 @@ - [Dashboard Configuration file](#dashboard-configuration-file) - [Generating QR code and peer configuration file (.conf)](#generating-qr-code-and-peer-configuration-file-conf) - [❓ How to update the dashboard?](#-how-to-update-the-dashboard) +- [🥘 Experimental Functions](#-experimental-functions) - [🔍 Screenshot](#-screenshot) - [⏰ Changelog](#--changelog) - [🛒 Dependencies](#-dependencies) @@ -103,11 +109,13 @@ - Python 3.7+ & Pip3 +- Browser support CSS3 and ES6 + ## 🛠 Install 1. Download WGDashboard ```shell - git clone -b v2.3.1 https://github.com/donaldzou/WGDashboard.git wgdashboard + git clone -b v3.0 https://github.com/donaldzou/WGDashboard.git wgdashboard 2. Open the WGDashboard folder @@ -280,22 +288,30 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u #### Dashboard Configuration file -Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following config: +Since version 2.0, WGDashboard will be using a configuration file called `wg-dashboard.ini`, (It will generate automatically after first time running the dashboard). More options will include in future versions, and for now it included the following configurations: -| | Description | Default | Available in Setting | -| --------------- | ------------------------------------------------------------ | ------------------------ | -------------------- | -| **`[Account]`** | | | | -| `username` | Dashboard login username | `admin` | Yes | -| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes | -| **`[Server]`** | | | | -| `wg_conf_path` | The path of all the Wireguard configurations | `/etc/wireguard` | Yes | -| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes | -| `app_port` | Port the the dashboard will run with | `10086` | Yes | -| `auth_req` | Does the dashboard need authentication to access | `true` | No | -| | If `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | | | -| `version` | Dashboard Version | `v2.2` | No | - -**Except `auth_req` due to security consideration.** +| | Description | Default | Edit Available | +| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -------------- | +| **`[Account]`** | *Configuration on account* | | | +| `username` | Dashboard login username | `admin` | Yes | +| `password` | Password, will be hash with SHA256 | `admin` hashed in SHA256 | Yes | +| | | | | +| **`[Server]`** | *Configuration on dashboard* | | | +| `wg_conf_path` | The path of all the Wireguard configurations | `/etc/wireguard` | Yes | +| `app_ip` | IP address the dashboard will run with | `0.0.0.0` | Yes | +| `app_port` | Port the the dashboard will run with | `10086` | Yes | +| `auth_req` | Does the dashboard need authentication to access, if `auth_req = false` , user will not be access the **Setting** tab due to security consideration. **User can only edit the file directly in system**. | `true` | **No** | +| `version` | Dashboard Version | `v3.0` | **No** | +| `dashboard_refresh_interval` | How frequent the dashboard will refresh on the configuration page | `60000ms` | Yes | +| `dashboard_sort` | How configuration is sorting | `status` | Yes | +| | | | | +| **`[Peers]`** | *Default Settings on a new peer* | | | +| `peer_global_dns` | DNS Server | `1.1.1.1` | Yes | +| `peer_endpoint_allowed_ip` | Endpoint Allowed IP | `0.0.0.0/0` | Yes | +| `peer_display_mode` | How peer will display | `grid` | Yes | +| `remote_endpoint` | Remote Endpoint (i.e where your peers will connect to) | *depends on your server's default network interface* | Yes | +| `peer_mtu` | Maximum Transmit Unit | `1420` | | +| `peer_keep_alive` | Keep Alive | `21` | Yes | #### Generating QR code and peer configuration file (.conf) @@ -326,14 +342,35 @@ Endpoint = 0.0.0.0:51820 ## ❓ How to update the dashboard? -1. Change your directory to `wireguard-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 wireguard-dashboard/src + cd wgdashboard ``` + 2. Update the dashboard ```shell - sudo ./wgd.sh update + git pull https://github.com/donaldzou/WGDashboard.git v3.0 --force ``` + +Starting with `v3.0`, you can simply do `./wgd.sh update` !! (I hope, lol) + +## 🥘 Experimental Functions + +#### Progressive Web App (PWA) for WGDashboard + +- With `v3.0`, I've added a `manifest.json` into the dashboard, so user could add their dashboard as a PWA to their browser or mobile device. + + + + + ## 🔍 Screenshot ![Sign In Page](img/SignIn.png) @@ -346,7 +383,7 @@ Endpoint = 0.0.0.0:51820 ![Edit Peer](img/EditPeer.png) -![Delete Peer](img/DeletePeer.png) +![Delete Peer](img/DeleteBulk.png) ![Dashboard Setting](img/DashboardSetting.png) @@ -354,10 +391,26 @@ Endpoint = 0.0.0.0:51820 ![Traceroute](img/Traceroute.png) - - ## ⏰ 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 e130b01..d4627ce 100644 Binary files a/img/AddPeer.png and b/img/AddPeer.png differ diff --git a/img/Configuration.png b/img/Configuration.png index 393c4bf..317a68b 100644 Binary files a/img/Configuration.png and b/img/Configuration.png differ diff --git a/img/DashboardSetting.png b/img/DashboardSetting.png index b09ced3..463f947 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 0000000..afa2f53 Binary files /dev/null and b/img/DeleteBulk.png differ diff --git a/img/EditPeer.png b/img/EditPeer.png index 9bb9858..fed3bb3 100644 Binary files a/img/EditPeer.png and b/img/EditPeer.png differ diff --git a/img/HomePage.png b/img/HomePage.png index 41c64dc..2eab836 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 0000000..bcc0f4b Binary files /dev/null and b/img/PWA.gif differ diff --git a/img/QRCode.png b/img/QRCode.png index 004e27b..f2d8b6d 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 0000000..0c78d65 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 0000000..cf9b7a2 --- /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 728db1a..1e4dbdb 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/', 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() @@ -1062,12 +1003,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) @@ -1101,206 +1041,208 @@ def get_conf(config_name): conf_data['checked'] = "nope" else: conf_data['checked'] = "checked" - print(wg_ip) config.clear() return jsonify(conf_data) - # return render_template('get_conf.html', conf_data=conf_data, wg_ip=config.get("Peers","remote_endpoint"), sort_tag=sort, - # dashboard_refresh_interval=int(config.get("Server", "dashboard_refresh_interval")), peer_display_mode=peer_display_mode) # 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 """ - if "username" not in session: - print("not loggedin") - return redirect(url_for("signin")) status = get_conf_status(config_name) if status == "running": try: - subprocess.run("wg-quick down " + config_name, - check=True, shell=True, capture_output=True).stdout - except subprocess.CalledProcessError: + check = subprocess.check_output("wg-quick down " + config_name, + shell=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as exc: + session["switch_msg"] = exc.output.strip().decode("utf-8") return redirect('/') elif status == "stopped": try: - subprocess.run("wg-quick up " + config_name, - check=True, shell=True, capture_output=True).stdout - except subprocess.CalledProcessError: + subprocess.check_output("wg-quick up " + config_name, + shell=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as exc: + session["switch_msg"] = exc.output.strip().decode("utf-8") return redirect('/') return redirect(request.referrer) -# Add peer +@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'] + dns_addresses = data['DNS'] + enable_preshared_key = data["enable_preshared_key"] + amount = data['amount'] + config_interface = read_conf_file_interface(config_name) + if "Address" not in config_interface: + return "Configuration must have an IP address." + if not amount.isdigit() or int(amount) < 1: + return "Amount must be integer larger than 0" + amount = int(amount) + if not check_DNS(dns_addresses): + return "DNS formate is incorrect. Example: 1.1.1.1" + if not check_Allowed_IPs(endpoint_allowed_ip): + return "Endpoint Allowed IPs format is incorrect." + if len(data['MTU']) == 0 or not data['MTU'].isdigit(): + return "MTU format is not correct." + if len(data['keep_alive']) == 0 or not data['keep_alive'].isdigit(): + return "Persistent Keepalive format is not correct." + ips = f_available_ips(config_name) + if amount > len(ips): + return f"Cannot create more than {len(ips)} peers." + wg_command = ["wg", "set", config_name] + sql_command = [] + for i in range(amount): + keys[i]['name'] = f"{config_name}_{datetime.now().strftime('%m%d%Y%H%M%S')}_Peer_#_{(i + 1)}" + wg_command.append("peer") + wg_command.append(keys[i]['publicKey']) + keys[i]['allowed_ips'] = ips.pop(0) + if enable_preshared_key: + keys[i]['psk_file'] = f"{keys[i]['name']}.txt" + f = open(keys[i]['psk_file'], "w+") + f.write(keys[i]['presharedKey']) + f.close() + wg_command.append("preshared-key") + wg_command.append(keys[i]['psk_file']) + else: + keys[i]['psk_file'] = "" + wg_command.append("allowed-ips") + wg_command.append(keys[i]['allowed_ips']) + update = ["UPDATE ", config_name, " SET name = '", keys[i]['name'], + "', private_key = '", keys[i]['privateKey'], "', DNS = '", dns_addresses, + "', endpoint_allowed_ip = '", endpoint_allowed_ip, "' WHERE id = '", keys[i]['publicKey'], "'"] + sql_command.append(update) + try: + status = subprocess.check_output(" ".join(wg_command), shell=True, stderr=subprocess.STDOUT) + status = subprocess.check_output("wg-quick save " + config_name, shell=True, stderr=subprocess.STDOUT) + get_all_peers_data(config_name) + if enable_preshared_key: + for i in keys: + os.remove(i['psk_file']) + for i in range(len(sql_command)): + sql_command[i] = "".join(sql_command[i]) + g.cur.executescript("; ".join(sql_command)) + return "true" + except subprocess.CalledProcessError as exc: + return exc.output.strip() + + @app.route('/add_peer/', methods=['POST']) def add_peer(config_name): - """Add new peer. - - :param config_name: Name of WG interface - :type config_name: str - :return: Return result of action or recommendations - :rtype: str """ - - sem.acquire(timeout=1) - db = TinyDB(os.path.join(DB_PATH, config_name + ".json")) - peers = Query() + Add Peers + @param config_name: configuration name + @return: string + """ data = request.get_json() public_key = data['public_key'] allowed_ips = data['allowed_ips'] endpoint_allowed_ip = data['endpoint_allowed_ip'] dns_addresses = data['DNS'] enable_preshared_key = data["enable_preshared_key"] + preshared_key = data['preshared_key'] keys = get_conf_peer_key(config_name) if len(public_key) == 0 or len(dns_addresses) == 0 or len(allowed_ips) == 0 or len(endpoint_allowed_ip) == 0: - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return "Please fill in all required box." if not isinstance(keys, list): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return config_name + " is not running." if public_key in keys: - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return "Public key already exist." - if len(db.search(peers.allowed_ip.matches(allowed_ips))) != 0: - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") + check_dup_ip = g.cur.execute( + "SELECT COUNT(*) FROM " + config_name + " WHERE allowed_ip LIKE '" + allowed_ips + "/%'", ) \ + .fetchone() + if check_dup_ip[0] != 0: return "Allowed IP already taken by another peer." if not check_DNS(dns_addresses): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return "DNS formate is incorrect. Example: 1.1.1.1" if not check_Allowed_IPs(endpoint_allowed_ip): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return "Endpoint Allowed IPs format is incorrect." if len(data['MTU']) == 0 or not data['MTU'].isdigit(): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return "MTU format is not correct." if len(data['keep_alive']) == 0 or not data['keep_alive'].isdigit(): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return "Persistent Keepalive format is not correct." try: - if enable_preshared_key == True: - key = subprocess.check_output("wg genpsk > tmp_psk.txt", shell=True) + if enable_preshared_key: + now = str(datetime.now().strftime("%m%d%Y%H%M%S")) + f_name = now + "_tmp_psk.txt" + f = open(f_name, "w+") + f.write(preshared_key) + f.close() status = subprocess.check_output( - f"wg set {config_name} peer {public_key} allowed-ips {allowed_ips} preshared-key tmp_psk.txt", + f"wg set {config_name} peer {public_key} allowed-ips {allowed_ips} preshared-key {f_name}", shell=True, stderr=subprocess.STDOUT) - os.remove("tmp_psk.txt") - elif enable_preshared_key == False: + os.remove(f_name) + elif not enable_preshared_key: status = subprocess.check_output(f"wg set {config_name} peer {public_key} allowed-ips {allowed_ips}", shell=True, stderr=subprocess.STDOUT) status = subprocess.check_output("wg-quick save " + config_name, shell=True, stderr=subprocess.STDOUT) - get_all_peers_data(config_name) - db.update({"name": data['name'], "private_key": data['private_key'], "DNS": data['DNS'], - "endpoint_allowed_ip": endpoint_allowed_ip}, - peers.id == public_key) - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") + sql = "UPDATE " + config_name + " SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ? WHERE id = ?" + g.cur.execute(sql, (data['name'], data['private_key'], data['DNS'], endpoint_allowed_ip, public_key)) return "true" except subprocess.CalledProcessError as exc: - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") 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": return "Your need to turn on " + config_name + " first." - sem.acquire(timeout=1) - db = TinyDB(os.path.join(DB_PATH, config_name + ".json")) - peers = Query() data = request.get_json() - delete_key = data['peer_id'] + delete_keys = data['peer_ids'] keys = get_conf_peer_key(config_name) if not isinstance(keys, list): return config_name + " is not running." - if delete_key not in keys: - db.close() - return "This key does not exist" - - try: - subprocess.run(f"wg set {config_name} peer {delete_key} remove", - shell=True, check=True, stderr=subprocess.STDOUT) - subprocess.run(f"wg-quick save {config_name}", - shell=True, check=True, stderr=subprocess.STDOUT) - db.remove(peers.id == delete_key) - db.close() + else: + sql_command = [] + wg_command = ["wg", "set", config_name] + for delete_key in delete_keys: + if delete_key not in keys: + return "This key does not exist" + sql_command.append("DELETE FROM " + config_name + " WHERE id = '" + delete_key + "';") + wg_command.append("peer") + wg_command.append(delete_key) + wg_command.append("remove") try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") + remove_wg = subprocess.check_output(" ".join(wg_command), + shell=True, stderr=subprocess.STDOUT) + save_wg = subprocess.check_output(f"wg-quick save {config_name}", shell=True, stderr=subprocess.STDOUT) + g.cur.executescript(' '.join(sql_command)) + g.db.commit() + except subprocess.CalledProcessError as exc: + return exc.output.strip() return "true" - except subprocess.CalledProcessError as exc: - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") - return exc.output.strip() -# 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() @@ -1311,176 +1253,85 @@ def save_peer_setting(config_name): allowed_ip = data['allowed_ip'] endpoint_allowed_ip = data['endpoint_allowed_ip'] preshared_key = data['preshared_key'] - sem.acquire(timeout=1) - db = TinyDB(os.path.join(DB_PATH, config_name + ".json")) - peers = Query() - if len(db.search(peers.id == id)) == 1: + check_peer_exist = g.cur.execute("SELECT COUNT(*) FROM " + config_name + " WHERE id = ?", (id,)).fetchone() + if check_peer_exist[0] == 1: check_ip = check_repeat_allowed_ip(id, allowed_ip, config_name) if not check_IP_with_range(endpoint_allowed_ip): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify({"status": "failed", "msg": "Endpoint Allowed IPs format is incorrect."}) if not check_DNS(dns_addresses): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify({"status": "failed", "msg": "DNS format is incorrect."}) if len(data['MTU']) == 0 or not data['MTU'].isdigit(): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify({"status": "failed", "msg": "MTU format is not correct."}) if len(data['keep_alive']) == 0 or not data['keep_alive'].isdigit(): - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify({"status": "failed", "msg": "Persistent Keepalive format is not correct."}) if private_key != "": check_key = f_check_key_match(private_key, id, config_name) if check_key['status'] == "failed": - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify(check_key) if check_ip['status'] == "failed": - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify(check_ip) try: tmp_psk = open("tmp_edit_psk.txt", "w+") tmp_psk.write(preshared_key) tmp_psk.close() - change_psk = subprocess.run(f"wg set {config_name} peer {id} preshared-key tmp_edit_psk.txt", - shell=True, check=True, stderr=subprocess.STDOUT) + change_psk = subprocess.check_output(f"wg set {config_name} peer {id} preshared-key tmp_edit_psk.txt", + shell=True, stderr=subprocess.STDOUT) if change_psk.decode("UTF-8") != "": - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify({"status": "failed", "msg": change_psk.decode("UTF-8")}) if allowed_ip == "": allowed_ip = '""' allowed_ip = allowed_ip.replace(" ", "") - change_ip = subprocess.run(f"wg set {config_name} peer {id} allowed-ips {allowed_ip}", - shell=True, check=True, stderr=subprocess.STDOUT) - subprocess.run(f'wg-quick save {config_name}', shell=True, check=True, stderr=subprocess.STDOUT) + change_ip = subprocess.check_output(f"wg set {config_name} peer {id} allowed-ips {allowed_ip}", + shell=True, stderr=subprocess.STDOUT) + subprocess.check_output(f'wg-quick save {config_name}', shell=True, stderr=subprocess.STDOUT) if change_ip.decode("UTF-8") != "": - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify({"status": "failed", "msg": change_ip.decode("UTF-8")}) - db.update( - { - "name": name, - "private_key": private_key, - "DNS": dns_addresses, - "endpoint_allowed_ip": endpoint_allowed_ip, - "mtu": data['MTU'], - "keepalive": data['keep_alive'], "preshared_key": preshared_key - }, peers.id == id) - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") + sql = "UPDATE " + config_name + " SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?, keepalive = ?, preshared_key = ? WHERE id = ?" + g.cur.execute(sql, (name, private_key, dns_addresses, endpoint_allowed_ip, data["MTU"], + data["keep_alive"], preshared_key, id)) return jsonify({"status": "success", "msg": ""}) except subprocess.CalledProcessError as exc: - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return jsonify({"status": "failed", "msg": str(exc.output.decode("UTF-8").strip())}) - - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") - return jsonify({"status": "failed", "msg": "This peer does not exist."}) + else: + return jsonify({"status": "failed", "msg": "This peer does not exist."}) # 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() - id = data['id'] - sem.acquire(timeout=1) - db = TinyDB(os.path.join(DB_PATH, config_name + ".json")) - peers = Query() - result = db.search(peers.id == id) - db.close() - data = {"name": result[0]['name'], "allowed_ip": result[0]['allowed_ip'], "DNS": result[0]['DNS'], - "private_key": result[0]['private_key'], "endpoint_allowed_ip": result[0]['endpoint_allowed_ip'], - "mtu": result[0]['mtu'], "keep_alive": result[0]['keepalive'], "preshared_key": result[0]["preshared_key"]} - - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") + peer_id = data['id'] + result = g.cur.execute( + "SELECT name, allowed_ip, DNS, private_key, endpoint_allowed_ip, mtu, keepalive, preshared_key FROM " + + config_name + " WHERE id = ?", (peer_id,)).fetchall() + data = {"name": result[0][0], "allowed_ip": result[0][1], "DNS": result[0][2], + "private_key": result[0][3], "endpoint_allowed_ip": result[0][4], + "mtu": result[0][5], "keep_alive": result[0][6], "preshared_key": result[0][7]} return jsonify(data) -# 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)) +# Return available IPs +@app.route('/available_ips/', methods=['GET']) +def available_ips(config_name): + return jsonify(f_available_ips(config_name)) # 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() @@ -1491,105 +1342,122 @@ def check_key_match(config_name): @app.route("/qrcode/", methods=['GET']) def generate_qrcode(config_name): - """generate the QR with peer configuration. - - :param config_name: Name of WG interface - :type config_name: str - :return: TODO - :rtype: TODO """ - - id = request.args.get('id') - sem.acquire(timeout=1) - db = TinyDB(os.path.join(DB_PATH, config_name + ".json")) - peers = Query() - get_peer = db.search(peers.id == id) + 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 " + + config_name + " WHERE id = ?", (peer_id,)).fetchall() config = get_dashboard_conf() if len(get_peer) == 1: peer = get_peer[0] - if peer['private_key'] != "": + if peer[0] != "": public_key = get_conf_pub_key(config_name) listen_port = get_conf_listen_port(config_name) endpoint = config.get("Peers", "remote_endpoint") + ":" + listen_port - private_key = peer['private_key'] - allowed_ip = peer['allowed_ip'] - dns_addresses = peer['DNS'] - mtu_value = peer['mtu'] - endpoint_allowed_ip = peer['endpoint_allowed_ip'] - keepalive = peer['keepalive'] - preshared_key = peer["preshared_key"] - conf = { - "public_key": public_key, - "listen_port": listen_port, - "endpoint": endpoint, - "private_key": private_key, - "allowed_ip": allowed_ip, - "DNS": dns_addresses, - "mtu": mtu_value, - "endpoint_allowed_ip": endpoint_allowed_ip, - "keepalive": keepalive, - "preshared_key": preshared_key - } - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") + private_key = peer[0] + allowed_ip = peer[1] + dns_addresses = peer[2] + mtu_value = peer[3] + endpoint_allowed_ip = peer[4] + keepalive = peer[5] + preshared_key = peer[6] - result = "[Interface]\nPrivateKey = " + conf['private_key'] + "\nAddress = " + conf[ - 'allowed_ip'] + "\nMTU = " + conf['mtu'] + "\nDNS = " + conf['DNS'] \ - + "\n\n[Peer]\nPublicKey = " + conf['public_key'] + "\nAllowedIPs = " + conf[ - 'endpoint_allowed_ip'] + "\nPersistentKeepalive = " + conf['keepalive'] + "\nEndpoint = " + \ - conf['endpoint'] + result = "[Interface]\nPrivateKey = " + private_key + "\nAddress = " + allowed_ip + "\nMTU = " \ + + str(mtu_value) + "\nDNS = " + dns_addresses + "\n\n[Peer]\nPublicKey = " + public_key \ + + "\nAllowedIPs = " + endpoint_allowed_ip + "\nPersistentKeepalive = " \ + + str(keepalive) + "\nEndpoint = " + endpoint if preshared_key != "": result += "\nPresharedKey = " + preshared_key - return render_template("qrcode.html", i=result) + else: + return redirect("/configuration/" + config_name) - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") - return redirect("/configuration/" + config_name) + +@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() + config = get_dashboard_conf() + data = [] + public_key = get_conf_pub_key(config_name) + listen_port = get_conf_listen_port(config_name) + endpoint = config.get("Peers", "remote_endpoint") + ":" + listen_port + for peer in get_peer: + private_key = peer[0] + allowed_ip = peer[1] + dns_addresses = peer[2] + mtu_value = peer[3] + endpoint_allowed_ip = peer[4] + keepalive = peer[5] + preshared_key = peer[6] + filename = peer[7] + if len(filename) == 0: + filename = "Untitled_Peer" + else: + filename = peer[7] + # Clean filename + illegal_filename = [".", ",", "/", "?", "<", ">", "\\", ":", "*", '|' '\"', "com1", "com2", "com3", + "com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", + "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "con", "nul", "prn"] + for i in illegal_filename: + filename = filename.replace(i, "") + if len(filename) == 0: + filename = "Untitled_Peer" + filename = "".join(filename.split(' ')) + filename = filename + "_" + config_name + psk = "" + if preshared_key != "": + psk = "\nPresharedKey = " + preshared_key + + return_data = "[Interface]\nPrivateKey = " + private_key + "\nAddress = " + allowed_ip + "\nDNS = " + \ + dns_addresses + "\nMTU = " + str(mtu_value) + "\n\n[Peer]\nPublicKey = " + \ + public_key + "\nAllowedIPs = " + endpoint_allowed_ip + "\nEndpoint = " + \ + endpoint + "\nPersistentKeepalive = " + str(keepalive) + psk + data.append({"filename": f"{filename}.conf", "content": return_data}) + return jsonify({"status": True, "peers": data, "filename": f"{config_name}.zip"}) # Download configuration file @app.route('/download/', methods=['GET']) def download(config_name): - """Download client configuration file. - - :param config_name: Name of WG interface - :type config_name: str - :return: TODO - :rtype: TODO """ - - print(request.headers.get('User-Agent')) - id = request.args.get('id') - sem.acquire(timeout=1) - db = TinyDB(os.path.join(DB_PATH, config_name + ".json")) - peers = Query() - get_peer = db.search(peers.id == id) + 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 " + + config_name + " WHERE id = ?", (peer_id,)).fetchall() config = get_dashboard_conf() if len(get_peer) == 1: peer = get_peer[0] - if peer['private_key'] != "": + if peer[0] != "": public_key = get_conf_pub_key(config_name) listen_port = get_conf_listen_port(config_name) endpoint = config.get("Peers", "remote_endpoint") + ":" + listen_port - private_key = peer['private_key'] - allowed_ip = peer['allowed_ip'] - dns_addresses = peer['DNS'] - mtu_value = peer['mtu'] - endpoint_allowed_ip = peer['endpoint_allowed_ip'] - keepalive = peer['keepalive'] - filename = peer['name'] - preshared_key = peer["preshared_key"] + private_key = peer[0] + allowed_ip = peer[1] + dns_addresses = peer[2] + mtu_value = peer[3] + endpoint_allowed_ip = peer[4] + keepalive = peer[5] + preshared_key = peer[6] + filename = peer[7] if len(filename) == 0: - filename = "Untitled_Peers" + filename = "Untitled_Peer" else: - filename = peer['name'] + filename = peer[7] # Clean filename illegal_filename = [".", ",", "/", "?", "<", ">", "\\", ":", "*", '|' '\"', "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", @@ -1603,30 +1471,25 @@ def download(config_name): psk = "" if preshared_key != "": psk = "\nPresharedKey = " + preshared_key - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") - result = "[Interface]\nPrivateKey = " + private_key + "\nAddress = " + allowed_ip + "\nDNS = " + \ - dns_addresses + "\nMTU = " + mtu_value + "\n\n[Peer]\nPublicKey = " + \ - public_key + "\nAllowedIPs = " + endpoint_allowed_ip + "\nEndpoint = " + \ - endpoint + "\nPersistentKeepalive = " + keepalive + psk - return app.response_class((yield result), mimetype='text/conf', - headers={"Content-Disposition": "attachment;filename=" + filename + ".conf"}) - db.close() - return redirect("/configuration/" + config_name) + + return_data = "[Interface]\nPrivateKey = " + private_key + "\nAddress = " + allowed_ip + "\nDNS = " + \ + dns_addresses + "\nMTU = " + str(mtu_value) + "\n\n[Peer]\nPublicKey = " + \ + public_key + "\nAllowedIPs = " + endpoint_allowed_ip + "\nEndpoint = " + \ + endpoint + "\nPersistentKeepalive = " + str(keepalive) + psk + + return jsonify({"status": True, "filename": f"{filename}.conf", "content": return_data}) + 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']: @@ -1646,42 +1509,37 @@ 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'] - sem.acquire(timeout=1) - db = TinyDB(os.path.join(DB_PATH, config + ".json")) + peers = g.cur.execute("SELECT id, name, allowed_ip, endpoint FROM " + config).fetchall() html = "" - for i in db.all(): - html += '' - allowed_ip = str(i['allowed_ip']).split(",") + for i in peers: + html += '' + allowed_ip = str(i[2]).split(",") for k in allowed_ip: k = k.split("/") if len(k) == 2: html += "" - endpoint = str(i['endpoint']).split(":") + endpoint = str(i[3]).split(":") if len(endpoint) == 2: html += "" html += "" - db.close() - try: - sem.release() - except RuntimeError as e: - print("RuntimeError: cannot release un-acquired lock") return html # 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: @@ -1706,10 +1564,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: @@ -1733,26 +1592,26 @@ Dashboard Initialization def init_dashboard(): - """Create dashboard default configuration. + """ + Create dashboard default configuration. """ # Set Default INI File if not os.path.isfile(DASHBOARD_CONF): - conf_file = open(DASHBOARD_CONF, "w+") + 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']: config['Server']['wg_conf_path'] = '/etc/wireguard' - # TODO: IPv6 for the app IP might need to configure with Gunicorn... if 'app_ip' not in config['Server']: config['Server']['app_ip'] = '0.0.0.0' if 'app_port' not in config['Server']: @@ -1785,32 +1644,74 @@ 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" + + +""" +Configure DashBoard before start web-server +""" + + +def run_dashboard(): + init_dashboard() + global UPDATE + UPDATE = check_update() + config = configparser.ConfigParser(strict=False) + config.read('wg-dashboard.ini') + # global app_ip + app_ip = config.get("Server", "app_ip") + # global app_port + app_port = config.get("Server", "app_port") + global WG_CONF_PATH + WG_CONF_PATH = config.get("Server", "wg_conf_path") + config.clear() + return app + + +""" +Get host and port for web-server +""" + + +def get_host_bind(): + init_dashboard() + config = configparser.ConfigParser(strict=False) + config.read('wg-dashboard.ini') + app_ip = config.get("Server", "app_ip") + app_port = config.get("Server", "app_port") + + return app_ip, app_port if __name__ == "__main__": - init_dashboard() UPDATE = check_update() - configuration_settings = get_dashboard_conf() - app_ip = configuration_settings.get("Server", "app_ip") - app_port = int(configuration_settings.get("Server", "app_port")) - WG_CONF_PATH = configuration_settings.get("Server", "wg_conf_path") - configuration_settings.clear() + config = configparser.ConfigParser(strict=False) + config.read('wg-dashboard.ini') + # global app_ip + app_ip = config.get("Server", "app_ip") + # global app_port + app_port = config.get("Server", "app_port") + WG_CONF_PATH = config.get("Server", "wg_conf_path") + config.clear() app.run(host=app_ip, debug=False, port=app_port) diff --git a/src/gunicorn.conf.py b/src/gunicorn.conf.py new file mode 100644 index 0000000..56b82ce --- /dev/null +++ b/src/gunicorn.conf.py @@ -0,0 +1,11 @@ +import multiprocessing +import dashboard + +app_host, app_port = dashboard.get_host_bind() + +worker_class = 'gthread' +workers = multiprocessing.cpu_count() * 2 + 1 +threads = 4 +bind = f"{app_host}:{app_port}" +daemon = True +pidfile = './gunicorn.pid' diff --git a/src/requirements.txt b/src/requirements.txt index b590667..5d3b347 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,5 +1,6 @@ Flask -tinydb==4.5.2 ifcfg icmplib -flask-qrcode \ No newline at end of file +flask-qrcode +gunicorn +certbot \ No newline at end of file diff --git a/src/static/.DS_Store b/src/static/.DS_Store index 5008ddf..cf820c4 100644 Binary files a/src/static/.DS_Store and b/src/static/.DS_Store differ diff --git a/src/static/css/dashboard.css b/src/static/css/dashboard.css index e89cf5e..cc7da53 100644 --- a/src/static/css/dashboard.css +++ b/src/static/css/dashboard.css @@ -52,6 +52,7 @@ body { .nav-link:hover { padding-left: 30px; + background-color: #dfdfdf; } .sidebar .nav-link .feather { @@ -90,6 +91,14 @@ body { right: 1rem; } +.form-control{ + transition: all 0.2s ease-in-out; +} + +.form-control:disabled{ + cursor: not-allowed; +} + .navbar .form-control { padding: .75rem 1rem; border-width: 0; @@ -162,8 +171,24 @@ body { box-shadow: none; } +.btn-qrcode-peer{ + padding: 0 !important; +} + +.btn-qrcode-peer:active, .btn-qrcode-peer:hover{ + transform: scale(0.9) rotate(180deg); + border: 0 !important; +} + +.btn-download-peer:active, .btn-download-peer:hover{ + color: #17a2b8 !important; + transform: translateY(5px); +} + .share_peer_btn_group .btn-control{ - padding: 0 0 0 1rem; + margin: 0 0 0 1rem; + padding: 0 !important; + transition: all 0.4s cubic-bezier(1, -0.43, 0, 1.37); } .btn-control:hover{ @@ -233,18 +258,77 @@ main{ } .peer_list{ - margin-bottom: 4rem !important; + margin-bottom: 7rem !important; } } -.add_btn{ +.btn-manage-group{ + z-index: 99; position: fixed; bottom: 3rem; right: 2rem; + display: flex; +} + +.btn-manage-group .setting_btn_menu{ + position: absolute; + top: -124px; + background-color: white; + padding: 1rem 0; + right: 0; + box-shadow: 0 10px 20px rgb(0 0 0 / 19%), 0 6px 6px rgb(0 0 0 / 23%); + border-radius: 10px; + min-width: 200px; + display: none; + transform: translateY(-30px); + opacity: 0; + transition: all 0.3s cubic-bezier(0.58, 0.03, 0.05, 1.28); +} + +.btn-manage-group .setting_btn_menu.show{ + display: block; +} + +.setting_btn_menu.showing{ + transform: translateY(0px); + opacity: 1; +} + +.setting_btn_menu a{ + display: flex; + padding: 0.5rem 1rem; + transition: all 0.1s ease-in-out; + font-size: 1rem; + align-items: center; + cursor: pointer; +} + +.setting_btn_menu a:hover{ + background-color: #efefef; + text-decoration: none; +} + +.setting_btn_menu a i{ + margin-right: auto !important; +} + +.add_btn{ + height: 54px; z-index: 99; border-radius: 100px !important; - padding: 10px 20px; + padding: 0 14px; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); + margin-right: 1rem; + font-size: 1.5rem; +} + +.setting_btn{ + height: 54px; + z-index: 99; + border-radius: 100px !important; + padding: 0 14px; + box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); + font-size: 1.5rem; } @@ -382,3 +466,67 @@ main{ animation: loading 3s infinite ease-in-out; } +#qrcode_img img{ + width: 100%; +} + +#selected_ip_list .badge, #selected_peer_list .badge{ + margin: 0.1rem +} + +#add_modal.ip_modal_open{ + transition: filter 0.2s ease-in-out; + filter: brightness(0.5); +} + +#delete_bulk_modal .list-group a.active{ + background-color: #dc3545; + border-color: #dc3545; +} + +#selected_peer_list{ + max-height: 80px; + overflow-y: scroll; + overflow-x: hidden; +} + +.no-response{ + width: 100%; + height: 100%; + position: fixed; + background: #000000ba; + z-index: 10000; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + opacity: 0; + transition: all 1s ease-in-out; +} + +.no-response.active{ + display: flex; +} + +.no-response.active.show{ + opacity: 100; +} + +.no-response .container > *{ + text-align: center; +} + +.no-responding{ + transition: all 1s ease-in-out; + filter: blur(10px); +} + +pre.index-alert{ + margin-bottom: 0; + padding: 1rem; + background-color: #343a40; + border: 1px solid rgba(0,0,0,.125); + border-radius: .25rem; + margin-top: 1rem; + color: white; +} \ No newline at end of file diff --git a/src/static/css/dashboard.min.css b/src/static/css/dashboard.min.css index 8df3ac2..f82f199 100644 --- a/src/static/css/dashboard.min.css +++ b/src/static/css/dashboard.min.css @@ -1 +1 @@ -@-webkit-keyframes rotating{0%{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotating{0%{-ms-transform:rotate(0deg);-moz-transform:rotate(0deg);-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-ms-transform:rotate(360deg);-moz-transform:rotate(360deg);-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}@-moz-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}body{font-size:.875rem}.feather{width:16px;height:16px;vertical-align:text-bottom}.sidebar{position:fixed;top:0;bottom:0;left:0;z-index:100;padding:48px 0 0;box-shadow:inset -1px 0 0 rgba(0,0,0,.1)}.sidebar-sticky{position:relative;top:0;height:calc(100vh - 48px);padding-top:.5rem;overflow-x:hidden;overflow-y:auto}@supports ((position:-webkit-sticky) or (position:sticky)){.sidebar-sticky{position:-webkit-sticky;position:sticky}}.sidebar .nav-link{font-weight:500;color:#333;transition:.2s cubic-bezier(.82,-.07,0,1.01)}.nav-link:hover{padding-left:30px}.sidebar .nav-link .feather{margin-right:4px;color:#999}.sidebar .nav-link.active{color:#007bff}.sidebar .nav-link.active .feather,.sidebar .nav-link:hover .feather{color:inherit}.sidebar-heading{font-size:.75rem;text-transform:uppercase}.navbar-brand{padding-top:.75rem;padding-bottom:.75rem;font-size:1rem;background-color:rgba(0,0,0,.25);box-shadow:inset -1px 0 0 rgba(0,0,0,.25)}.navbar .navbar-toggler{top:.25rem;right:1rem}.navbar .form-control{padding:.75rem 1rem;border-width:0;border-radius:0}.form-control-dark{color:#fff;background-color:rgba(255,255,255,.1);border-color:rgba(255,255,255,.1)}.form-control-dark:focus{border-color:transparent;box-shadow:0 0 0 3px rgba(255,255,255,.25)}.dot{width:10px;height:10px;border-radius:50px;display:inline-block;margin-left:10px}.dot-running{background-color:#28a745!important;box-shadow:0 0 0 .2rem #28a74545}.h6-dot-running{margin-left:.3rem}.dot-stopped{background-color:#6c757d!important}.card-running{border-color:#28a745}.info h6{line-break:anywhere;transition:.2s ease-in-out}.info .row .col-sm{display:flex;flex-direction:column}.info .row .col-sm small{display:flex}.info .row .col-sm small strong:last-child(1){margin-left:auto!important}.btn-control{border:0!important;padding:0 1rem 0 0}.btn-control:active,.btn-control:focus{background-color:transparent!important;border:0!important;box-shadow:none}.share_peer_btn_group .btn-control{padding:0 0 0 1rem}.btn-control:hover{background:#fff}.btn-delete-peer:hover{color:#dc3545}.btn-setting-peer:hover{color:#007bff}.btn-download-peer:hover{color:#17a2b8}.login-container{padding:2rem}@media (max-width:992px){.card-col{margin-bottom:1rem}}.switch{font-size:2rem}.switch:hover{text-decoration:none}.btn-group-label:hover{color:#007bff;border-color:#007bff;background:#fff}@media (max-width:768px){.peer_data_group{text-align:left}}.index-switch{text-align:right}main{margin-bottom:3rem}.peer_list{margin-bottom:7rem}@media (max-width:768px){.add_btn{bottom:1.5rem!important}.peer_list{margin-bottom:4rem!important}}.add_btn{position:fixed;bottom:3rem;right:2rem;z-index:99;border-radius:100px!important;padding:10px 20px;box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}.rotating::before{-webkit-animation:rotating .75s linear infinite;-moz-animation:rotating .75s linear infinite;-ms-animation:rotating .75s linear infinite;-o-animation:rotating .75s linear infinite;animation:rotating .75s linear infinite}.peer_private_key_textbox_switch{position:absolute;right:2rem;transform:translateY(-28px);font-size:1.2rem;cursor:pointer}#peer_private_key_textbox,#private_key,#public_key{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.progress-bar{transition:.3s ease-in-out}.key{transition:.2s ease-in-out;cursor:pointer}.key:hover{color:#007bff}.card,.form-control{border-radius:10px}.peer_list .card .button-group{height:22px}.btn{border-radius:8px}.modal-content{border-radius:10px}.tooltip-inner{font-size:.8rem}#conf_status_btn,.conf_card{transition:.2s ease-in-out}.conf_card:hover{border-color:#007bff;cursor:pointer}.info_loading{animation:loading 2s infinite ease-in-out;border-radius:5px;height:19px;transition:.3s ease-in-out}#conf_status_btn.info_loading{height:38px;border-radius:5px;animation:loading 3s infinite ease-in-out} \ No newline at end of file +@-webkit-keyframes rotating{0%{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotating{0%{-ms-transform:rotate(0deg);-moz-transform:rotate(0deg);-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-ms-transform:rotate(360deg);-moz-transform:rotate(360deg);-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}@-moz-keyframes loading{0%,to{background-color:#dfdfdf}50%{background-color:#adadad}}body{font-size:.875rem}.feather{width:16px;height:16px;vertical-align:text-bottom}.sidebar{position:fixed;top:0;bottom:0;left:0;z-index:100;padding:48px 0 0;box-shadow:inset -1px 0 0 rgba(0,0,0,.1)}.sidebar-sticky{position:relative;top:0;height:calc(100vh - 48px);padding-top:.5rem;overflow-x:hidden;overflow-y:auto}@supports ((position:-webkit-sticky) or (position:sticky)){.sidebar-sticky{position:-webkit-sticky;position:sticky}}.sidebar .nav-link{font-weight:500;color:#333;transition:.2s cubic-bezier(.82,-.07,0,1.01)}.nav-link:hover{padding-left:30px;background-color:#dfdfdf}.sidebar .nav-link .feather{margin-right:4px;color:#999}.sidebar .nav-link.active{color:#007bff}.sidebar .nav-link.active .feather,.sidebar .nav-link:hover .feather{color:inherit}.sidebar-heading{font-size:.75rem;text-transform:uppercase}.navbar-brand{padding-top:.75rem;padding-bottom:.75rem;font-size:1rem;background-color:rgba(0,0,0,.25);box-shadow:inset -1px 0 0 rgba(0,0,0,.25)}.navbar .navbar-toggler{top:.25rem;right:1rem}.form-control{transition:all .2s ease-in-out}.form-control:disabled{cursor:not-allowed}.navbar .form-control{padding:.75rem 1rem;border-width:0;border-radius:0}.form-control-dark{color:#fff;background-color:rgba(255,255,255,.1);border-color:rgba(255,255,255,.1)}.form-control-dark:focus{border-color:transparent;box-shadow:0 0 0 3px rgba(255,255,255,.25)}.dot{width:10px;height:10px;border-radius:50px;display:inline-block;margin-left:10px}.dot-running{background-color:#28a745!important;box-shadow:0 0 0 .2rem #28a74545}.h6-dot-running{margin-left:.3rem}.dot-stopped{background-color:#6c757d!important}.card-running{border-color:#28a745}.info h6{line-break:anywhere;transition:.2s ease-in-out}.info .row .col-sm{display:flex;flex-direction:column}.info .row .col-sm small{display:flex}.info .row .col-sm small strong:last-child(1){margin-left:auto!important}.btn-control{border:0!important;padding:0 1rem 0 0}.btn-control:active,.btn-control:focus{background-color:transparent!important;border:0!important;box-shadow:none}.btn-qrcode-peer{padding:0!important}.btn-qrcode-peer:active,.btn-qrcode-peer:hover{transform:scale(.9) rotate(180deg);border:0!important}.btn-download-peer:active,.btn-download-peer:hover{color:#17a2b8!important;transform:translateY(5px)}.share_peer_btn_group .btn-control{margin:0 0 0 1rem;padding:0!important;transition:all .4s cubic-bezier(1,-.43,0,1.37)}.btn-control:hover{background:#fff}.btn-delete-peer:hover{color:#dc3545}.btn-setting-peer:hover{color:#007bff}.btn-download-peer:hover{}.login-container{padding:2rem}@media (max-width:992px){.card-col{margin-bottom:1rem}}.switch{font-size:2rem}.switch:hover{text-decoration:none}.btn-group-label:hover{color:#007bff;border-color:#007bff;background:#fff}@media (max-width:768px){.peer_data_group{text-align:left}}.index-switch{text-align:right}main{margin-bottom:3rem}.peer_list{margin-bottom:7rem}@media (max-width:768px){.add_btn{bottom:1.5rem!important}.peer_list{margin-bottom:7rem!important}}.btn-manage-group{z-index:99;position:fixed;bottom:3rem;right:2rem;display:flex}.btn-manage-group .setting_btn_menu{position:absolute;top:-124px;background-color:#fff;padding:1rem 0;right:0;box-shadow:0 10px 20px rgb(0 0 0/19%),0 6px 6px rgb(0 0 0/23%);border-radius:10px;min-width:200px;display:none;transform:translateY(-30px);opacity:0;transition:all .3s cubic-bezier(.58,.03,.05,1.28)}.btn-manage-group .setting_btn_menu.show{display:block}.setting_btn_menu.showing{transform:translateY(0);opacity:1}.setting_btn_menu a{display:flex;padding:.5rem 1rem;transition:all .1s ease-in-out;font-size:1rem;align-items:center;cursor:pointer}.setting_btn_menu a:hover{background-color:#efefef;text-decoration:none}.setting_btn_menu a i{margin-right:auto!important}.add_btn{margin-right:1rem}.add_btn,.setting_btn{height:54px;z-index:99;border-radius:100px!important;padding:0 14px;box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23);font-size:1.5rem}.rotating::before{-webkit-animation:rotating .75s linear infinite;-moz-animation:rotating .75s linear infinite;-ms-animation:rotating .75s linear infinite;-o-animation:rotating .75s linear infinite;animation:rotating .75s linear infinite}.peer_private_key_textbox_switch{position:absolute;right:2rem;transform:translateY(-28px);font-size:1.2rem;cursor:pointer}#peer_private_key_textbox,#private_key,#public_key{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.progress-bar{transition:.3s ease-in-out}.key{transition:.2s ease-in-out;cursor:pointer}.key:hover{color:#007bff}.card,.form-control{border-radius:10px}.peer_list .card .button-group{height:22px}.btn{border-radius:8px}.modal-content{border-radius:10px}.tooltip-inner{font-size:.8rem}#conf_status_btn,.conf_card{transition:.2s ease-in-out}.conf_card:hover{border-color:#007bff;cursor:pointer}.info_loading{animation:loading 2s infinite ease-in-out;border-radius:5px;height:19px;transition:.3s ease-in-out}#conf_status_btn.info_loading{height:38px;border-radius:5px;animation:loading 3s infinite ease-in-out}#qrcode_img img{width:100%}#selected_ip_list .badge,#selected_peer_list .badge{margin:.1rem}#add_modal.ip_modal_open{transition:filter .2s ease-in-out;filter:brightness(.5)}#delete_bulk_modal .list-group a.active{background-color:#dc3545;border-color:#dc3545}#selected_peer_list{max-height:80px;overflow-y:scroll;overflow-x:hidden}.no-responding,.no-response{transition:all 1s ease-in-out}.no-response{width:100%;height:100%;position:fixed;background:#000000ba;z-index:10000;display:none;flex-direction:column;align-items:center;justify-content:center;opacity:0}.no-response.active{display:flex}.no-response.active.show{opacity:100}.no-response .container>*{text-align:center}.no-responding{filter:blur(10px)}pre.index-alert{margin-bottom:0;padding:1rem;background-color:#343a40;border:1px solid rgba(0,0,0,.125);border-radius:.25rem;margin-top:1rem;color:#fff} \ No newline at end of file diff --git a/src/static/img/192x192ios.png b/src/static/img/192x192ios.png new file mode 100644 index 0000000..0bfe520 Binary files /dev/null and b/src/static/img/192x192ios.png differ diff --git a/src/static/img/icon-192x192.png b/src/static/img/icon-192x192.png new file mode 100644 index 0000000..5e950a2 Binary files /dev/null and b/src/static/img/icon-192x192.png differ diff --git a/src/static/img/icon-256x256.png b/src/static/img/icon-256x256.png new file mode 100644 index 0000000..f801831 Binary files /dev/null and b/src/static/img/icon-256x256.png differ diff --git a/src/static/img/icon-384x384.png b/src/static/img/icon-384x384.png new file mode 100644 index 0000000..4b34f5c Binary files /dev/null and b/src/static/img/icon-384x384.png differ diff --git a/src/static/img/icon-512x512.png b/src/static/img/icon-512x512.png new file mode 100644 index 0000000..69b6883 Binary files /dev/null and b/src/static/img/icon-512x512.png differ diff --git a/src/static/js/configuration.js b/src/static/js/configuration.js index 7d85ef1..389f844 100644 --- a/src/static/js/configuration.js +++ b/src/static/js/configuration.js @@ -1,218 +1,847 @@ -let $body = $("body"); +/** + * configuration.js - Copyright(C) 2021 Donald Zou [https://github.com/donaldzou] + * Under Apache-2.0 License + */ -function roundN(value, digits) { - let tenToN = 10 ** digits; - return (Math.round(value * tenToN)) / tenToN; -} +(function(){ + /* global peers */ + /* global conf_name */ -// Progress Bar -let $progress_bar = $(".progress-bar"); -function startProgressBar(){ - $progress_bar.css("width","0%") - .css("opacity", "100") - .css("background", "rgb(255,69,69)") - .css("background", - "linear-gradient(145deg, rgba(255,69,69,1) 0%, rgba(0,115,186,1) 100%)") - .css("width","25%"); - setTimeout(function(){ - stillLoadingProgressBar(); - },300); -} - -function stillLoadingProgressBar(){ - $progress_bar.css("transition", "3s ease-in-out").css("width", "75%"); -} - -function endProgressBar(){ - $progress_bar.css("transition", "0.3s ease-in-out").css("width","100%"); - setTimeout(function(){ - $progress_bar.css("opacity", "0"); - },250); - -} - -function showToast(msg) { - $('#alertToast').toast('show'); - $('#alertToast .toast-body').html(msg); -} + /** + * Definitions + */ + let configuration_interval; + let configuration_timeout = 0; + let $progress_bar = $(".progress-bar"); + let bootstrapModalConfig = { + keyboard: false, + backdrop: 'static' + }; + let addModal = new bootstrap.Modal(document.getElementById('add_modal'), bootstrapModalConfig); + let deleteBulkModal = new bootstrap.Modal(document.getElementById('delete_bulk_modal'), bootstrapModalConfig); + let ipModal = new bootstrap.Modal(document.getElementById('available_ip_modal'), bootstrapModalConfig); + let qrcodeModal = new bootstrap.Modal(document.getElementById('qrcode_modal'), bootstrapModalConfig); + let settingModal = new bootstrap.Modal(document.getElementById('setting_modal'), bootstrapModalConfig); + let deleteModal = new bootstrap.Modal(document.getElementById('delete_modal'), bootstrapModalConfig); + $("[data-toggle='tooltip']").tooltip(); + $("[data-toggle='popover']").popover(); -// Config Toggle -$body.on("click", ".switch", function (){ - $(this).siblings($(".spinner-border")).css("display", "inline-block"); - $(this).remove(); - location.replace("/switch/"+$(this).attr('id')); -}); - -// Generating Keys -function generate_key(){ - $.ajax({ - "url": "/generate_peer", - "method": "GET", - }).done(function(res){ - $("#private_key").val(res.private_key); - $("#public_key").val(res.public_key); - $("#preshare_key").val(res.preshared_key); - $("#add_peer_alert").addClass("d-none"); - $("#re_generate_key i").removeClass("rotating"); - }); -} -function generate_public_key(){ - $.ajax({ - "url": "/generate_public_key", - "method": "POST", - "headers":{"Content-Type": "application/json"}, - "data": JSON.stringify({"private_key": $("#private_key").val()}) - }).done(function(res){ - if(res.status === "failed"){ - $("#add_peer_alert").html(res.msg).removeClass("d-none"); - }else{ - $("#add_peer_alert").addClass("d-none"); + /** + * To show alert on the configuration page + * @param response + */ + function configurationAlert(response) { + if (response.listen_port === "" && response.status === "stopped"){ + let configAlert = document.createElement("div"); + configAlert.classList.add("alert"); + configAlert.classList.add("alert-warning"); + configAlert.setAttribute("role", "alert"); + configAlert.innerHTML = 'Peer QR Code and configuration file download required a specified Listen Port.'; + document.querySelector("#config_info_alert").appendChild(configAlert); + } + if (response.conf_address === "N/A"){ + let configAlert = document.createElement("div"); + configAlert.classList.add("alert"); + configAlert.classList.add("alert-warning"); + configAlert.setAttribute("role", "alert"); + configAlert.innerHTML = 'Configuration Address need to be specified to have peers connect to it.'; + document.querySelector("#config_info_alert").appendChild(configAlert); } - $("#public_key").val(res.data); - $("#re_generate_key i").removeClass("rotating"); - }); -} - -// Add Peer -$("#private_key").on("change",function(){ - if ($(this).val().length > 0){ - $("#re_generate_key i").addClass("rotating"); - generate_public_key(); - }else{ - $("#public_key").removeAttr("disabled").val(""); } -}); -$('#add_modal').on('show.bs.modal', function (event) { - generate_key(); -}); -$("#re_generate_key").on("click",function (){ - $("#public_key").attr("disabled","disabled"); - $("#re_generate_key i").addClass("rotating"); - generate_key(); -}); -let addModal = new bootstrap.Modal(document.getElementById('add_modal'), { - keyboard: false -}); + /** + * Parse all responded information onto the configuration header + * @param response + */ + function configurationHeader(response) { + let $conf_status_btn = document.getElementById("conf_status_btn"); + if (response.checked === "checked"){ + $conf_status_btn.innerHTML = ` ON`; + }else{ + $conf_status_btn.innerHTML = ` OFF`; + } + $conf_status_btn.classList.remove("info_loading"); + document.querySelectorAll("#sort_by_dropdown option").forEach(ele => ele.removeAttribute("selected")); + document.querySelector(`#sort_by_dropdown option[value="${response.sort_tag}"]`).setAttribute("selected", "selected"); + document.querySelectorAll(".interval-btn-group button").forEach(ele => ele.classList.remove("active")); + document.querySelector(`button[data-refresh-interval="${response.dashboard_refresh_interval}"]`).classList.add("active"); + document.querySelectorAll(".display-btn-group button").forEach(ele => ele.classList.remove("active")); + document.querySelector(`button[data-display-mode="${response.peer_display_mode}"]`).classList.add("active"); + document.querySelector("#conf_status").innerHTML = `${response.status}`; + document.querySelector("#conf_connected_peers").innerHTML = response.running_peer; + document.querySelector("#conf_total_data_usage").innerHTML = `${response.total_data_usage[0]} GB`; + document.querySelector("#conf_total_data_received").innerHTML = `${response.total_data_usage[2]} GB`; + document.querySelector("#conf_total_data_sent").innerHTML = `${response.total_data_usage[1]} GB`; + document.querySelector("#conf_public_key").innerHTML = response.public_key; + document.querySelector("#conf_listen_port").innerHTML = response.listen_port === "" ? "N/A" : response.listen_port; + document.querySelector("#conf_address").innerHTML = response.conf_address; + document.querySelectorAll(".info h6").forEach(ele => ele.classList.remove("info_loading")); + } -$(".add_btn").on("click", function(){ - addModal.toggle(); -}); + /** + * Parse all responded information onto the peers list + * @param response + */ + function configurationPeers(response) { + let result = ""; + if (response.peer_data.length === 0){ + document.querySelector(".peer_list").innerHTML = `

Oops! No peers found ‘︿’

`; + }else{ + let display_mode = response.peer_display_mode === "list" ? "col-12" : "col-sm-6 col-lg-4"; + response.peer_data.forEach(function(peer){ + let total_r = 0; + let total_s = 0; + total_r += peer.cumu_receive; + total_s += peer.cumu_sent; + let spliter = '
'; + let peer_name = + '
' + + '
'+ (peer.name === "" ? "Untitled" : peer.name) +'
' + + '
' + + '
'; + let peer_transfer = '

'+ roundN(peer.total_receive + total_r, 4) +' GB

'+ roundN(peer.total_sent + total_s, 4) +' GB

'; + let peer_key = '
PEERCLICK TO COPY
'+peer.id+'
'; + let peer_allowed_ip = '
ALLOWED IP
'+peer.allowed_ip+'
'; + let peer_latest_handshake = '
LATEST HANDSHAKE
'+peer.latest_handshake+'
'; + let peer_endpoint = '
END POINT
'+peer.endpoint+'
'; + let peer_control = '

'; + if (peer.private_key !== ""){ + peer_control += ''; + } + peer_control += '
'; + let html = '
' + + '
' + + '
' + + '
' + + peer_name + + spliter + + peer_transfer + + peer_key + + peer_allowed_ip + + peer_latest_handshake + + spliter + + peer_endpoint + + spliter + + peer_control + + '
' + + '
' + + '
' + + '
'; + result += html; + }); + document.querySelector(".peer_list").innerHTML = result; + if (response.dashboard_refresh_interval !== configuration_timeout){ + configuration_timeout = response.dashboard_refresh_interval; + removeConfigurationInterval(); + setConfigurationInterval(); + } + } + } -$("#save_peer").on("click",function(){ - let $public_key = $("#public_key"); - let $private_key = $("#private_key"); - let $allowed_ips = $("#allowed_ips"); - let $new_add_DNS = $("#new_add_DNS"); - let $new_add_endpoint_allowed_ip = $("#new_add_endpoint_allowed_ip"); - let $new_add_name = $("#new_add_name"); - let $new_add_MTU = $("#new_add_MTU"); - let $new_add_keep_alive = $("#new_add_keep_alive"); - let $enable_preshare_key = $("#enable_preshare_key"); + /** + * Handle when adding peers by bulk + */ + function addPeersByBulk() { + let $new_add_amount = $("#new_add_amount"); + $add_peer.setAttribute("disabled","disabled"); + $add_peer.innerHTML = `Adding ${$new_add_amount.val()} peers...`; + let $new_add_DNS = $("#new_add_DNS"); + $new_add_DNS.val(window.configurations.cleanIp($new_add_DNS.val())); + let $new_add_endpoint_allowed_ip = $("#new_add_endpoint_allowed_ip"); + $new_add_endpoint_allowed_ip.val(window.configurations.cleanIp($new_add_endpoint_allowed_ip.val())); + let $new_add_MTU = $("#new_add_MTU"); + let $new_add_keep_alive = $("#new_add_keep_alive"); + let $enable_preshare_key = $("#enable_preshare_key"); + let data_list = [$new_add_DNS, $new_add_endpoint_allowed_ip,$new_add_MTU, $new_add_keep_alive]; + if ($new_add_amount.val() > 0 && !$new_add_amount.hasClass("is-invalid")){ + if ($new_add_DNS.val() !== "" && $new_add_endpoint_allowed_ip.val() !== ""){ + let conf = $add_peer.getAttribute('conf_id'); + let keys = []; + for (let i = 0; i < $new_add_amount.val(); i++) { + keys.push(window.wireguard.generateKeypair()); + } + $.ajax({ + method: "POST", + url: "/add_peer_bulk/"+conf, + headers:{ + "Content-Type": "application/json" + }, + data: JSON.stringify({ + "DNS": $new_add_DNS.val(), + "endpoint_allowed_ip": $new_add_endpoint_allowed_ip.val(), + "MTU": $new_add_MTU.val(), + "keep_alive": $new_add_keep_alive.val(), + "enable_preshared_key": $enable_preshare_key.prop("checked"), + "keys": keys, + "amount": $new_add_amount.val() + }), + success: function (response){ + if(response !== "true"){ + $("#add_peer_alert").html(response).removeClass("d-none"); + data_list.forEach((ele) => ele.removeAttr("disabled")); + $add_peer.removeAttribute("disabled"); + $add_peer.innerHTML = "Save"; + } + else{ + window.configurations.loadPeers(""); + data_list.forEach((ele) => ele.removeAttr("disabled")); + $("#add_peer_form").trigger("reset"); + $add_peer.removeAttribute("disabled"); + $add_peer.innerHTML = "Save"; + window.configurations.showToast($new_add_amount.val()+" peers added successful!"); + window.configurations.addModal().toggle(); + } + } + }); + }else{ + $("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none"); + $add_peer.removeAttribute("disabled"); + $add_peer.innerHTML = "Add"; + } + }else{ + $add_peer.removeAttribute("disabled"); + $add_peer.innerHTML = "Add"; + } + } - $(this).attr("disabled","disabled"); - $(this).html("Saving..."); - if ($allowed_ips.val() !== "" && $public_key.val() !== "" && $new_add_DNS.val() !== "" && $new_add_endpoint_allowed_ip.val() !== ""){ - let conf = $(this).attr('conf_id'); - let data_list = [$private_key, $allowed_ips, $new_add_name, $new_add_DNS, $new_add_endpoint_allowed_ip,$new_add_MTU, $new_add_keep_alive]; - data_list.forEach((ele) => ele.attr("disabled", "disabled")); + /** + * Delete one peer or by bulk + * @param config + * @param peer_ids + */ + function deletePeers(config, peer_ids){ $.ajax({ method: "POST", - url: "/add_peer/"+conf, + url: "/remove_peer/"+config, headers:{ "Content-Type": "application/json" }, - data: JSON.stringify({ - "private_key":$private_key.val(), - "public_key":$public_key.val(), - "allowed_ips": $allowed_ips.val(), - "name":$new_add_name.val(), - "DNS": $new_add_DNS.val(), - "endpoint_allowed_ip": $new_add_endpoint_allowed_ip.val(), - "MTU": $new_add_MTU.val(), - "keep_alive": $new_add_keep_alive.val(), - "enable_preshared_key": $enable_preshare_key.prop("checked"), - }), + data: JSON.stringify({"action": "delete", "peer_ids": peer_ids}), success: function (response){ if(response !== "true"){ - $("#add_peer_alert").html(response).removeClass("d-none"); - data_list.forEach((ele) => ele.removeAttr("disabled")); - $("#save_peer").removeAttr("disabled").html("Save"); + if (window.configurations.deleteModal()._isShown) { + $("#remove_peer_alert").html(response+$("#add_peer_alert").html()) + .removeClass("d-none"); + $("#delete_peer").removeAttr("disabled").html("Delete"); + } + if (window.configurations.deleteBulkModal()._isShown){ + let $bulk_remove_peer_alert = $("#bulk_remove_peer_alert"); + $bulk_remove_peer_alert.html(response+$bulk_remove_peer_alert.html()) + .removeClass("d-none"); + $("#confirm_delete_bulk_peers").removeAttr("disabled").html("Delete"); + } } else{ - load_data(""); - addModal.toggle(); + if (window.configurations.deleteModal()._isShown) { + window.configurations.deleteModal().toggle(); + } + if (window.configurations.deleteBulkModal()._isShown){ + $("#confirm_delete_bulk_peers").removeAttr("disabled").html("Delete"); + $("#selected_peer_list").html(''); + $(".delete-bulk-peer-item.active").removeClass('active'); + window.configurations.deleteBulkModal().toggle(); + } + window.configurations.loadPeers($('#search_peer_textbox').val()); + $('#alertToast').toast('show'); + $('#alertToast .toast-body').html("Peer deleted!"); + $("#delete_peer").removeAttr("disabled").html("Delete"); } } }); + } + + /** + * Handle when the server is not responding + */ + function noResponding(){ + document.querySelectorAll(".no-response").forEach(ele => ele.classList.add("active")); + setTimeout(function (){ + document.querySelectorAll(".no-response").forEach(ele => ele.classList.add("show")); + document.querySelector("#right_body").classList.add("no-responding"); + document.querySelector(".navbar").classList.add("no-responding"); + },10); + } + + /** + * Remove no responding + */ + function removeNoResponding(){ + document.querySelectorAll(".no-response").forEach(ele => ele.classList.remove("show")); + document.querySelector("#right_body").classList.remove("no-responding"); + document.querySelector(".navbar").classList.remove("no-responding"); + setTimeout(function (){ + document.querySelectorAll(".no-response").forEach(ele => ele.classList.remove("active")); + },1010); + } + + /** + * Set configuration refresh Interval + */ + function setConfigurationInterval(){ + configuration_interval = setInterval(function (){ + loadPeers($('#search_peer_textbox').val()); + }, configuration_timeout); + } + + /** + * Remove configuration refresh interval + */ + function removeConfigurationInterval(){ + clearInterval(configuration_interval); + } + + /** + * Start Progress Bar + */ + function startProgressBar(){ + $progress_bar.css("width","0%") + .css("opacity", "100") + .css("background", "rgb(255,69,69)") + .css("background", + "linear-gradient(145deg, rgba(255,69,69,1) 0%, rgba(0,115,186,1) 100%)") + .css("width","25%"); + setTimeout(function(){ + stillLoadingProgressBar(); + },300); + } + + /** + * Still Loading Progress Bar + */ + function stillLoadingProgressBar(){ + $progress_bar.css("transition", "3s ease-in-out").css("width", "75%"); + } + + /** + * End Progress Bar + */ + function endProgressBar(){ + $progress_bar.css("transition", "0.3s ease-in-out").css("width","100%"); + setTimeout(function(){ + $progress_bar.css("opacity", "0"); + },250); + } + + /** + * Round Transfer number into 4 digits + * @param value + * @param digits + * @returns {number} + */ + function roundN(value, digits) { + let tenToN = 10 ** digits; + return (Math.round(value * tenToN)) / tenToN; + } + + /** + * Load Peers from server to configuration page + * @param searchString + */ + function loadPeers(searchString){ + startProgressBar(); + $.ajax({ + method: "GET", + url: `/get_config/${conf_name}?search=${encodeURIComponent(searchString)}`, + headers:{"Content-Type": "application/json"} + }).done(function(response){ + removeNoResponding(); + peers = response.peer_data; + configurationAlert(response); + configurationHeader(response); + configurationPeers(response); + $(".dot.dot-running").attr("title","Peer Connected").tooltip(); + $(".dot.dot-stopped").attr("title","Peer Disconnected").tooltip(); + $("i[data-toggle='tooltip']").tooltip(); + endProgressBar(); + }).fail(function(){ + noResponding(); + }); + } + + /** + * Generate Private and Public key for a new peer + */ + function generate_key(){ + let keys = window.wireguard.generateKeypair(); + document.querySelector("#private_key").value = keys.privateKey; + document.querySelector("#public_key").value = keys.publicKey; + document.querySelector("#add_peer_alert").classList.add("d-none"); + document.querySelector("#re_generate_key i").classList.remove("rotating"); + document.querySelector("#enable_preshare_key").value = keys.presharedKey; + } + + /** + * Show toast + * @param msg + */ + function showToast(msg) { + $('#alertToast').toast('show'); + $('#alertToast .toast-body').html(msg); + } + + /** + * Update peer's refresh interval + * @param res + * @param interval + */ + function updateRefreshInterval(res, interval) { + if (res === "true"){ + configuration_timeout = interval; + removeConfigurationInterval(); + setConfigurationInterval(); + showToast("Refresh Interval set to "+Math.round(interval/1000)+" seconds"); + }else{ + $(".interval-btn-group button").removeClass("active"); + $('.interval-btn-group button[data-refresh-interval="'+configuration_timeout+'"]').addClass("active"); + showToast("Refresh Interval set unsuccessful"); + } + } + + /** + * Clean IP + * @param val + * @returns {string} + */ + function cleanIp(val){ + let clean_ip = val.split(','); + for (let i = 0; i < clean_ip.length; i++) { + clean_ip[i] = clean_ip[i].trim(' '); + } + return clean_ip.filter(Boolean).join(","); + } + + /** + * Trigger IP badge and item + * @param ip + */ + function trigger_ip(ip){ + let $ip_ele = document.querySelector(`.available-ip-item[data-ip='${ip}']`); + if ($ip_ele){ + if ($ip_ele.classList.contains("active")){ + $ip_ele.classList.remove("active"); + document.querySelector(`#selected_ip_list .badge[data-ip='${ip}']`).remove(); + }else{ + $ip_ele.classList.add("active"); + document.querySelector("#selected_ip_list").innerHTML += `${ip}`; + } + } + } + + /** + * Download single configuration file + * @param conf + */ + function download_one_config(conf){ + let link = document.createElement('a'); + link.download = conf.filename; + let blob = new Blob([conf.content], {type: 'text/conf'}); + link.href = window.URL.createObjectURL(blob); + link.click(); + } + + /** + * Toggle delete by bulk IP + * @param element + */ + function toggleBulkIP(element){ + let $selected_peer_list = $("#selected_peer_list"); + let id = element.data("id"); + let name = element.data("name") === "" ? "Untitled Peer" : element.data("name"); + if (element.hasClass("active")){ + element.removeClass("active"); + $("#selected_peer_list .badge[data-id='"+id+"']").remove(); + }else{ + element.addClass("active"); + $selected_peer_list.append(''+name+' - '+id+''); + } + } + + /** + * Copy public keys to clipboard + * @param element + */ + function copyToClipboard(element) { + let $temp = $(""); + $body.append($temp); + $temp.val($(element).text()).trigger( "select" ); + document.execCommand("copy"); + $temp.remove(); + } + + /** + * Get all available IP for this configuration + */ + function getAvailableIps(){ + $.ajax({ + "url": `/available_ips/${$add_peer.getAttribute("conf_id")}`, + "method": "GET", + }).done(function (res) { + available_ips = res; + let $list_group = document.querySelector("#available_ip_modal .modal-body .list-group"); + $list_group.innerHTML = ""; + document.querySelector("#allowed_ips").value = available_ips[0]; + available_ips.forEach((ip) => + $list_group.innerHTML += + `${ip}`); + }); + } + + window.configurations = { + addModal: () => { return addModal; }, + deleteBulkModal: () => { return deleteBulkModal; }, + deleteModal: () => { return deleteModal; }, + ipModal: () => { return ipModal; }, + qrcodeModal: () => { return qrcodeModal; }, + settingModal: () => { return settingModal; }, + + loadPeers: (searchString) => { loadPeers(searchString); }, + addPeersByBulk: () => { addPeersByBulk(); }, + deletePeers: (config, peers_ids) => { deletePeers(config, peers_ids); }, + + + getAvailableIps: () => { getAvailableIps(); }, + generateKeyPair: () => { generate_key(); }, + showToast: (message) => { showToast(message); }, + updateRefreshInterval: (res, interval) => { updateRefreshInterval(res, interval); }, + copyToClipboard: (element) => { copyToClipboard(element); }, + toggleDeleteByBulkIP: (element) => { toggleBulkIP(element); }, + downloadOneConfig: (conf) => { download_one_config(conf); }, + triggerIp: (ip) => { trigger_ip(ip); }, + cleanIp: (val) => { return cleanIp(val); }, + startProgressBar: () => { startProgressBar(); }, + stillLoadingProgressBar: () => { stillLoadingProgressBar(); }, + endProgressBar: () => { endProgressBar(); } + }; +})(); + +let $body = $("body"); +let available_ips = []; +let $add_peer = document.getElementById("save_peer"); + +/** + * ========== + * Add peers + * ========== + */ + +/** + * Toggle add peers modal when add button clicked + */ +document.querySelector(".add_btn").addEventListener("click", () => { + window.configurations.addModal().toggle(); +}); + +/** + * When configuration switch got click + */ +document.querySelector(".info").addEventListener("click", (event) => { + let selector = document.querySelector(".switch"); + if (selector.contains(event.target)){ + selector.style.display = "none"; + document.querySelector('div[role=status]').style.display = "inline-block"; + location.replace(`/switch/${selector.getAttribute("id")}`); + } +}); + +/** + * Generate Public key when private got change + */ +document.querySelector("#private_key").addEventListener("change", (event) => { + let publicKey = document.querySelector("#public_key"); + if (event.target.value.length === 44){ + publicKey.value = window.wireguard.generatePublicKey(event.target.value); + publicKey.setAttribute("disabled", "disabled"); }else{ - $("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none"); - $(this).removeAttr("disabled"); - $(this).html("Save"); + publicKey.attributes.removeNamedItem("disabled"); + publicKey.value = ""; + } +}); + +/** + * Handle when add modal is show and hide + */ +$('#add_modal').on('show.bs.modal', function () { + window.configurations.generateKeyPair(); + window.configurations.getAvailableIps(); +}).on('hide.bs.modal', function(){ + $("#allowed_ips_indicator").html(''); +}); + +/** + * Handle when user clicked the regenerate button + */ +$("#re_generate_key").on("click",function (){ + $("#public_key").attr("disabled","disabled"); + $("#re_generate_key i").addClass("rotating"); + window.configurations.generateKeyPair(); +}); + +/** + * Handle when user is editing in allowed ips textbox + */ +$("#allowed_ips").on("keyup", function(){ + let s = window.configurations.cleanIp($(this).val()); + s = s.split(","); + if (available_ips.includes(s[s.length - 1])){ + $("#allowed_ips_indicator").removeClass().addClass("text-success") + .html(''); + }else{ + $("#allowed_ips_indicator").removeClass().addClass("text-warning") + .html(''); + } +}); + +/** + * Change peer name when user typing in peer name textbox + */ +$("#peer_name_textbox").on("keyup", function(){ + $(".peer_name").html($(this).val()); +}); + +/** + * When Add Peer button got clicked + */ +$add_peer.addEventListener("click",function(){ + let $bulk_add = $("#bulk_add"); + if ($bulk_add.prop("checked")){ + if (!$("#new_add_amount").hasClass("is-invalid")){ + window.configurations.addPeersByBulk(); + } + } else { + let $public_key = $("#public_key"); + let $private_key = $("#private_key"); + let $allowed_ips = $("#allowed_ips"); + $allowed_ips.val(window.configurations.cleanIp($allowed_ips.val())); + let $new_add_DNS = $("#new_add_DNS"); + $new_add_DNS.val(window.configurations.cleanIp($new_add_DNS.val())); + let $new_add_endpoint_allowed_ip = $("#new_add_endpoint_allowed_ip"); + $new_add_endpoint_allowed_ip.val(window.configurations.cleanIp($new_add_endpoint_allowed_ip.val())); + let $new_add_name = $("#new_add_name"); + let $new_add_MTU = $("#new_add_MTU"); + let $new_add_keep_alive = $("#new_add_keep_alive"); + let $enable_preshare_key = $("#enable_preshare_key"); + $add_peer.setAttribute("disabled","disabled"); + $add_peer.innerHTML = "Adding..."; + if ($allowed_ips.val() !== "" && $public_key.val() !== "" && $new_add_DNS.val() !== "" && $new_add_endpoint_allowed_ip.val() !== ""){ + let conf = $add_peer.getAttribute('conf_id'); + let data_list = [$private_key, $allowed_ips, $new_add_name, $new_add_DNS, $new_add_endpoint_allowed_ip,$new_add_MTU, $new_add_keep_alive]; + data_list.forEach((ele) => ele.attr("disabled", "disabled")); + $.ajax({ + method: "POST", + url: "/add_peer/"+conf, + headers:{ + "Content-Type": "application/json" + }, + data: JSON.stringify({ + "private_key":$private_key.val(), + "public_key":$public_key.val(), + "allowed_ips": $allowed_ips.val(), + "name":$new_add_name.val(), + "DNS": $new_add_DNS.val(), + "endpoint_allowed_ip": $new_add_endpoint_allowed_ip.val(), + "MTU": $new_add_MTU.val(), + "keep_alive": $new_add_keep_alive.val(), + "enable_preshared_key": $enable_preshare_key.prop("checked"), + "preshared_key": $enable_preshare_key.val() + }), + success: function (response){ + if(response !== "true"){ + $("#add_peer_alert").html(response).removeClass("d-none"); + data_list.forEach((ele) => ele.removeAttr("disabled")); + $add_peer.removeAttribute("disabled"); + $add_peer.innerHTML = "Save"; + } + else{ + window.configurations.loadPeers(""); + data_list.forEach((ele) => ele.removeAttr("disabled")); + $("#add_peer_form").trigger("reset"); + $add_peer.removeAttribute("disabled"); + $add_peer.innerHTML = "Save"; + window.configurations.showToast("Add peer successful!"); + window.configurations.addModal().toggle(); + } + } + }); + }else{ + $("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none"); + $add_peer.removeAttribute("disabled"); + $add_peer.innerHTML = "Add"; + } + } +}); + +/** + * Handle when user is typing the amount of peers they want to add, and will check if the amount is less than 1 or + * is larger than the amount of available ips + */ +$("#new_add_amount").on("keyup", function(){ + let $bulk_amount_validation = $("#bulk_amount_validation"); + // $(this).removeClass("is-valid").addClass("is-invalid"); + if ($(this).val().length > 0){ + if (isNaN($(this).val())){ + $(this).removeClass("is-valid").addClass("is-invalid"); + $bulk_amount_validation.html("Please enter a valid integer"); + }else if ($(this).val() > available_ips.length){ + $(this).removeClass("is-valid").addClass("is-invalid"); + $bulk_amount_validation.html(`Cannot create more than ${available_ips.length} peers.`); + }else if ($(this).val() < 1){ + $(this).removeClass("is-valid").addClass("is-invalid"); + $bulk_amount_validation.html("Please enter at least 1 or more."); + }else{ + $(this).removeClass("is-invalid").addClass("is-valid"); + } + }else{ + $(this).removeClass("is-invalid").removeClass("is-valid"); + } +}); + +/** + * Handle when user toggled add peers by bulk + */ +$("#bulk_add").on("change", function (){ + let hide = $(".non-bulk").find("input"); + let amount = $("#new_add_amount"); + if ($(this).prop("checked") === true){ + for(let i = 0; i < hide.length; i++){ + $(hide[i]).attr("disabled", "disabled"); + } + amount.removeAttr("disabled"); + } + else{ + for(let i = 0; i < hide.length; i++){ + if ($(hide[i]).attr('id') !== "public_key"){ + $(hide[i]).removeAttr("disabled"); + } + } + amount.attr("disabled", "disabled"); } }); -let qrcodeModal = new bootstrap.Modal(document.getElementById('qrcode_modal'), { - keyboard: false +/** + * ======================= + * Available IP Related + * ======================= + */ + +/** + * Handle when available ip modal show and hide + */ +$("#available_ip_modal").on("show.bs.modal", () => { + document.querySelector('#add_modal').classList.add("ip_modal_open"); +}).on("hidden.bs.modal", () => { + document.querySelector('#add_modal').classList.remove("ip_modal_open"); + let ips = []; + let $selected_ip_list = document.querySelector("#selected_ip_list"); + for (let i = 0; i < $selected_ip_list.childElementCount; i++){ + ips.push($selected_ip_list.children[i].dataset.ip); + } + ips.forEach((ele) => window.configurations.triggerIp(ele)); }); -// QR Code + +/** + * When IP Badge got click + */ +$body.on("click", ".available-ip-badge", function(){ + $(".available-ip-item[data-ip='"+$(this).data("ip")+"']").removeClass("active"); + $(this).remove(); +}); + +/** + * When available ip item got click + */ +$body.on("click", ".available-ip-item", function () { + window.configurations.triggerIp($(this).data("ip")); +}); + +/** + * When search IP button got clicked + */ +$("#search_available_ip").on("click", function () { + window.configurations.ipModal().toggle(); + let $allowed_ips = document.querySelector("#allowed_ips"); + if ($allowed_ips.value.length > 0){ + let s = $allowed_ips.value.split(","); + for (let i = 0; i < s.length; i++){ + s[i] = s[i].trim(); + window.configurations.triggerIp(s[i]); + } + } +}).tooltip(); + +/** + * When confirm IP is clicked + */ +$("#confirm_ip").on("click", () => { + window.configurations.ipModal().toggle(); + let ips = []; + let $selected_ip_list = $("#selected_ip_list"); + $selected_ip_list.children().each(function(){ + ips.push($(this).data("ip")); + }); + $("#allowed_ips").val(ips.join(", ")); + ips.forEach((ele) => window.configurations.triggerIp(ele)); +}); + +/** + * ======= + * QR Code + * ======= + */ + +/** + * When the QR-code button got clicked on each peer + */ $body.on("click", ".btn-qrcode-peer", function (){ - let src = $(this).attr('img_src'); + let src = $(this).data('imgsrc'); $.ajax({ "url": src, "method": "GET" }).done(function(res){ $("#qrcode_img").attr('src', res); - qrcodeModal.toggle(); + window.configurations.qrcodeModal().toggle(); }); }); -// Delete Peer Modal -let deleteModal = new bootstrap.Modal(document.getElementById('delete_modal'), { - keyboard: false -}); +/** + * =========== + * Delete Peer + * =========== + */ +/** + * When the delete button got clicked on each peer + */ $body.on("click", ".btn-delete-peer", function(){ let peer_id = $(this).attr("id"); $("#delete_peer").attr("peer_id", peer_id); - deleteModal.toggle(); + window.configurations.deleteModal().toggle(); }); +/** + * When the confirm delete button clicked + */ $("#delete_peer").on("click",function(){ $(this).attr("disabled","disabled"); $(this).html("Deleting..."); let peer_id = $(this).attr("peer_id"); let config = $(this).attr("conf_id"); - $.ajax({ - method: "POST", - url: "/remove_peer/"+config, - headers:{ - "Content-Type": "application/json" - }, - data: JSON.stringify({"action": "delete", "peer_id": peer_id}), - success: function (response){ - if(response !== "true"){ - $("#remove_peer_alert").html(response+$("#add_peer_alert").html()).removeClass("d-none"); - } - else{ - deleteModal.toggle(); - load_data($('#search_peer_textbox').val()); - $('#alertToast').toast('show'); - $('#alertToast .toast-body').html("Peer deleted!"); - $("#delete_peer").removeAttr("disabled").html("Delete"); - } - } - }); + let peer_ids = [peer_id]; + window.configurations.deletePeers(config, peer_ids); }); -// Peer Setting Modal -let settingModal = new bootstrap.Modal(document.getElementById('setting_modal'), { - keyboard: false -}); +/** + * ============= + * Peer Settings + * ============= + */ + +/** + * Handle when setting button got clicked for each peer + */ $body.on("click", ".btn-setting-peer", function(){ - startProgressBar(); + window.configurations.startProgressBar(); let peer_id = $(this).attr("id"); $("#save_peer_setting").attr("peer_id", peer_id); $.ajax({ @@ -223,7 +852,7 @@ $body.on("click", ".btn-setting-peer", function(){ }, data: JSON.stringify({"id": peer_id}), success: function(response){ - let peer_name = ((response.name === "") ? "Untitled Peer" : response.name); + let peer_name = ((response.name === "") ? "Untitled" : response.name); $("#setting_modal .peer_name").html(peer_name); $("#setting_modal #peer_name_textbox").val(response.name); $("#setting_modal #peer_private_key_textbox").val(response.private_key); @@ -233,16 +862,22 @@ $body.on("click", ".btn-setting-peer", function(){ $("#setting_modal #peer_mtu").val(response.mtu); $("#setting_modal #peer_keep_alive").val(response.keep_alive); $("#setting_modal #peer_preshared_key_textbox").val(response.preshared_key); - settingModal.toggle(); - endProgressBar(); + window.configurations.settingModal().toggle(); + window.configurations.endProgressBar(); } }); }); -$('#setting_modal').on('hidden.bs.modal', function (event) { +/** + * Handle when setting modal is closing + */ +$('#setting_modal').on('hidden.bs.modal', function () { $("#setting_peer_alert").addClass("d-none"); }); +/** + * Handle when private key text box in setting modal got changed + */ $("#peer_private_key_textbox").on("change",function(){ let $save_peer_setting = $("#save_peer_setting"); if ($(this).val().length > 0){ @@ -264,6 +899,9 @@ $("#peer_private_key_textbox").on("change",function(){ } }); +/** + * When save peer setting button got clicked + */ $("#save_peer_setting").on("click",function (){ $(this).attr("disabled","disabled"); $(this).html("Saving..."); @@ -303,8 +941,8 @@ $("#save_peer_setting").on("click",function (){ if (response.status === "failed"){ $("#setting_peer_alert").html(response.msg).removeClass("d-none"); }else{ - settingModal.toggle(); - load_data($('#search_peer_textbox').val()); + window.configurations.settingModal().toggle(); + window.configurations.loadPeers($('#search_peer_textbox').val()); $('#alertToast').toast('show'); $('#alertToast .toast-body').html("Peer Saved!"); } @@ -318,6 +956,9 @@ $("#save_peer_setting").on("click",function (){ } }); +/** + * Toggle show or hide for the private key textbox in the setting modal + */ $(".peer_private_key_textbox_switch").on("click",function (){ let $peer_private_key_textbox = $("#peer_private_key_textbox"); let mode = (($peer_private_key_textbox.attr('type') === 'password') ? "text":"password"); @@ -326,70 +967,68 @@ $(".peer_private_key_textbox_switch").on("click",function (){ $(".peer_private_key_textbox_switch i").removeClass().addClass(icon); }); +/** + * =========== + * Search Peer + * =========== + */ -// Search Peer -let typingTimer; -let doneTypingInterval = 200; -let $input = $('#search_peer_textbox'); -$input.on('keyup', function () { +let typingTimer; // Timeout object +let doneTypingInterval = 200; // Timeout interval + +/** + * Handle when the user keyup and keydown on the search textbox + */ +$('#search_peer_textbox').on('keyup', function () { clearTimeout(typingTimer); - typingTimer = setTimeout(doneTyping, doneTypingInterval); -}); -$input.on('keydown', function () { + typingTimer = setTimeout(() => { + window.configurations.loadPeers($(this).val()); + }, doneTypingInterval); +}).on('keydown', function () { clearTimeout(typingTimer); }); -function doneTyping () { - load_data($input.val()); -} +/** + * Manage Peers + */ -// Sorting +/** + * Handle when sort peers changed + */ $body.on("change", "#sort_by_dropdown", function (){ $.ajax({ method:"POST", data: JSON.stringify({'sort':$("#sort_by_dropdown option:selected").val()}), headers:{"Content-Type": "application/json"}, url: "/update_dashboard_sort", - success: function (res){ - load_data($('#search_peer_textbox').val()); + success: function (){ + window.configurations.loadPeers($('#search_peer_textbox').val()); } }); }); -// Click key to copy animation +/** + * Handle copy public key + */ $body.on("mouseenter", ".key", function(){ let label = $(this).parent().siblings().children()[1]; label.style.opacity = "100"; -}) -$body.on("mouseout", ".key", function(){ +}).on("mouseout", ".key", function(){ let label = $(this).parent().siblings().children()[1]; label.style.opacity = "0"; setTimeout(function (){ label.innerHTML = "CLICK TO COPY"; },200); -}); - -$body.on("click", ".key", function(){ - var label = $(this).parent().siblings().children()[1]; - copyToClipboard($(this)); +}).on("click", ".key", function(){ + let label = $(this).parent().siblings().children()[1]; + window.configurations.copyToClipboard($(this)); label.innerHTML = "COPIED!"; }); /** - * CopyToClipboard - * @param element + * Handle when interval button got clicked */ -function copyToClipboard(element) { - let $temp = $(""); - $body.append($temp); - $temp.val($(element).text()).trigger( "select" ); - document.execCommand("copy"); - $temp.remove(); -} - -// Update Interval $body.on("click", ".update_interval", function(){ - let prev = $(".interval-btn-group.active button"); $(".interval-btn-group button").removeClass("active"); let _new = $(this); _new.addClass("active"); @@ -399,29 +1038,21 @@ $body.on("click", ".update_interval", function(){ data: "interval="+$(this).data("refresh-interval"), url: "/update_dashboard_refresh_interval", success: function (res){ - if (res === "true"){ - load_interval = interval; - clearInterval(load_timeout); - load_timeout = setInterval(function (){ - load_data($('#search_peer_textbox').val()); - }, interval); - showToast("Refresh Interval set to "+Math.round(interval/1000)+" seconds"); - }else{ - $(".interval-btn-group button").removeClass("active"); - $('.interval-btn-group button[data-refresh-interval="'+load_interval+'"]').addClass("active"); - showToast("Refresh Interval set unsuccessful"); - } + window.configurations.updateRefreshInterval(res, interval); } - }) + }); }); -// Refresh Button +/** + * Handle when refresh button got clicked + */ $body.on("click", ".refresh", function (){ - load_data($('#search_peer_textbox').val()); + window.configurations.loadPeers($('#search_peer_textbox').val()); }); - -// Switch display mode +/** + * Handle when display mode button got clicked + */ $body.on("click", ".display_mode", function(){ $(".display-btn-group button").removeClass("active"); $(this).addClass("active"); @@ -435,14 +1066,199 @@ $body.on("click", ".display_mode", function(){ Array($(".peer_list").children()).forEach(function(child){ $(child).removeClass().addClass("col-12"); }); - showToast("Displaying as List"); + window.configurations.showToast("Displaying as List"); }else{ Array($(".peer_list").children()).forEach(function(child){ $(child).removeClass().addClass("col-sm-6 col-lg-4"); }); - showToast("Displaying as Grids"); + window.configurations.showToast("Displaying as Grids"); } } } }); +}); + + +/** + * ================= + * Configuration Menu + * ================= + */ +let $setting_btn_menu = $(".setting_btn_menu"); +$setting_btn_menu.css("top", ($setting_btn_menu.height() + 54)*(-1)); +let $setting_btn = $(".setting_btn"); + +/** + * When the menu button got clicked + */ +$setting_btn.on("click", function(){ + if ($setting_btn_menu.hasClass("show")){ + $setting_btn_menu.removeClass("showing"); + setTimeout(function(){ + $setting_btn_menu.removeClass("show"); + }, 201); + }else{ + $setting_btn_menu.addClass("show"); + setTimeout(function(){ + $setting_btn_menu.addClass("showing"); + },10); + } +}); + +/** + * Whenever the user clicked, if it is outside the menu and the menu is opened, hide the menu + */ +$("html").on("click", function(r){ + if (document.querySelector(".setting_btn") !== r.target){ + if (!document.querySelector(".setting_btn").contains(r.target)){ + if (!document.querySelector(".setting_btn_menu").contains(r.target)){ + $setting_btn_menu.removeClass("showing"); + setTimeout(function(){ + $setting_btn_menu.removeClass("show"); + }, 310); + } + } + } +}); + + +/** + * ==================== + * Delete Peers by Bulk + * ==================== + */ + +/** + * When delete peers by bulk clicked + */ +$("#delete_peers_by_bulk_btn").on("click", () => { + let $delete_bulk_modal_list = $("#delete_bulk_modal .list-group"); + $delete_bulk_modal_list.html(''); + peers.forEach((peer) => { + let name; + if (peer.name === "") { name = "Untitled Peer"; } + else { name = peer.name; } + $delete_bulk_modal_list.append(''+name+'
'+peer.id+'
'); + }); + window.configurations.deleteBulkModal().toggle(); +}); + +/** + * When the item or tag of delete peers by bulk got clicked + */ +$body.on("click", ".delete-bulk-peer-item", function(){ + window.configurations.toggleDeleteByBulkIP($(this)); +}).on("click", ".delete-peer-bulk-badge", function(){ + window.configurations.toggleDeleteByBulkIP($(".delete-bulk-peer-item[data-id='" + $(this).data("id") + "']")); +}); + +let $selected_peer_list = document.getElementById("selected_peer_list"); + +/** + * The change observer to observe when user choose 1 or more peers to delete + * @type {MutationObserver} + */ +let changeObserver = new MutationObserver(function(){ + if ($selected_peer_list.hasChildNodes()){ + $("#confirm_delete_bulk_peers").removeAttr("disabled"); + }else{ + $("#confirm_delete_bulk_peers").attr("disabled", "disabled"); + } +}); +changeObserver.observe($selected_peer_list, { + attributes: true, + childList: true, + characterData: true +}); + +let confirm_delete_bulk_peers_interval; + +/** + * When the user clicked the delete button in the delete peers by bulk + */ +$("#confirm_delete_bulk_peers").on("click", function(){ + let btn = $(this); + if (confirm_delete_bulk_peers_interval !== undefined){ + clearInterval(confirm_delete_bulk_peers_interval); + confirm_delete_bulk_peers_interval = undefined; + btn.html("Delete"); + }else{ + let timer = 5; + btn.html(`Deleting in ${timer} secs... Click to cancel`); + confirm_delete_bulk_peers_interval = setInterval(function(){ + timer -= 1; + btn.html(`Deleting in ${timer} secs... Click to cancel`); + if (timer === 0){ + btn.html(`Deleting...`); + btn.attr("disabled", "disabled"); + let ips = []; + $selected_peer_list.childNodes.forEach((ele) => ips.push(ele.dataset.id)); + window.configurations.deletePeers(btn.data("conf"), ips); + clearInterval(confirm_delete_bulk_peers_interval); + confirm_delete_bulk_peers_interval = undefined; + } + }, 1000); + } +}); + +/** + * Select all peers to delete + */ +$("#select_all_delete_bulk_peers").on("click", function(){ + $(".delete-bulk-peer-item").each(function(){ + if (!$(this).hasClass("active")) { + window.configurations.toggleDeleteByBulkIP($(this)); + } + }); +}); + +/** + * When delete peers by bulk window is hidden + */ +$(window.configurations.deleteBulkModal()._element).on("hidden.bs.modal", function(){ + $(".delete-bulk-peer-item").each(function(){ + if ($(this).hasClass("active")) { + window.configurations.toggleDeleteByBulkIP($(this)); + } + }); +}); + +/** + * ============== + * Download Peers + * ============== + */ + +/** + * When the download peers button got clicked + */ +$body.on("click", ".btn-download-peer", function(e){ + e.preventDefault(); + let link = $(this).attr("href"); + $.ajax({ + "url": link, + "method": "GET", + success: function(res){ + window.configurations.downloadOneConfig(res); + } + }); +}); + +/** + * When the download all peers got clicked + */ +$("#download_all_peers").on("click", function(){ + $.ajax({ + "url": $(this).data("url"), + "method": "GET", + success: function(res){ + if (res.peers.length > 0){ + window.wireguard.generateZipFiles(res); + window.configurations.showToast("Peers' zip file download successful!"); + }else{ + window.configurations.showToast("Oops! There are no peer can be download."); + } + } + }); }); \ No newline at end of file diff --git a/src/static/js/configuration.min.js b/src/static/js/configuration.min.js index 2d4032c..eda4ebf 100644 --- a/src/static/js/configuration.min.js +++ b/src/static/js/configuration.min.js @@ -1 +1 @@ -let $body=$("body");function roundN(value,digits){let tenToN=10**digits;return Math.round(value*tenToN)/tenToN}let $progress_bar=$(".progress-bar");function startProgressBar(){$progress_bar.css("width","0%").css("opacity","100").css("background","rgb(255,69,69)").css("background","linear-gradient(145deg, rgba(255,69,69,1) 0%, rgba(0,115,186,1) 100%)").css("width","25%");setTimeout(function(){stillLoadingProgressBar()},300)}function stillLoadingProgressBar(){$progress_bar.css("transition","3s ease-in-out").css("width","75%")}function endProgressBar(){$progress_bar.css("transition","0.3s ease-in-out").css("width","100%");setTimeout(function(){$progress_bar.css("opacity","0")},250)}function showToast(msg){$("#alertToast").toast("show");$("#alertToast .toast-body").html(msg)}$body.on("click",".switch",function(){$(this).siblings($(".spinner-border")).css("display","inline-block");$(this).remove();location.replace("/switch/"+$(this).attr("id"))});function generate_key(){$.ajax({url:"/generate_peer",method:"GET"}).done(function(res){$("#private_key").val(res.private_key);$("#public_key").val(res.public_key);$("#preshare_key").val(res.preshared_key);$("#add_peer_alert").addClass("d-none");$("#re_generate_key i").removeClass("rotating")})}function generate_public_key(){$.ajax({url:"/generate_public_key",method:"POST",headers:{"Content-Type":"application/json"},data:JSON.stringify({private_key:$("#private_key").val()})}).done(function(res){if(res.status==="failed"){$("#add_peer_alert").html(res.msg).removeClass("d-none")}else{$("#add_peer_alert").addClass("d-none")}$("#public_key").val(res.data);$("#re_generate_key i").removeClass("rotating")})}$("#private_key").on("change",function(){if($(this).val().length>0){$("#re_generate_key i").addClass("rotating");generate_public_key()}else{$("#public_key").removeAttr("disabled").val("")}});$("#add_modal").on("show.bs.modal",function(event){generate_key()});$("#re_generate_key").on("click",function(){$("#public_key").attr("disabled","disabled");$("#re_generate_key i").addClass("rotating");generate_key()});let addModal=new bootstrap.Modal(document.getElementById("add_modal"),{keyboard:false});$(".add_btn").on("click",function(){addModal.toggle()});$("#save_peer").on("click",function(){let $public_key=$("#public_key");let $private_key=$("#private_key");let $allowed_ips=$("#allowed_ips");let $new_add_DNS=$("#new_add_DNS");let $new_add_endpoint_allowed_ip=$("#new_add_endpoint_allowed_ip");let $new_add_name=$("#new_add_name");let $new_add_MTU=$("#new_add_MTU");let $new_add_keep_alive=$("#new_add_keep_alive");let $enable_preshare_key=$("#enable_preshare_key");$(this).attr("disabled","disabled");$(this).html("Saving...");if($allowed_ips.val()!==""&&$public_key.val()!==""&&$new_add_DNS.val()!==""&&$new_add_endpoint_allowed_ip.val()!==""){let conf=$(this).attr("conf_id");let data_list=[$private_key,$allowed_ips,$new_add_name,$new_add_DNS,$new_add_endpoint_allowed_ip,$new_add_MTU,$new_add_keep_alive];data_list.forEach(ele=>ele.attr("disabled","disabled"));$.ajax({method:"POST",url:"/add_peer/"+conf,headers:{"Content-Type":"application/json"},data:JSON.stringify({private_key:$private_key.val(),public_key:$public_key.val(),allowed_ips:$allowed_ips.val(),name:$new_add_name.val(),DNS:$new_add_DNS.val(),endpoint_allowed_ip:$new_add_endpoint_allowed_ip.val(),MTU:$new_add_MTU.val(),keep_alive:$new_add_keep_alive.val(),enable_preshared_key:$enable_preshare_key.prop("checked")}),success:function(response){if(response!=="true"){$("#add_peer_alert").html(response).removeClass("d-none");data_list.forEach(ele=>ele.removeAttr("disabled"));$("#save_peer").removeAttr("disabled").html("Save")}else{load_data("");addModal.toggle()}}})}else{$("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none");$(this).removeAttr("disabled");$(this).html("Save")}});let qrcodeModal=new bootstrap.Modal(document.getElementById("qrcode_modal"),{keyboard:false});$body.on("click",".btn-qrcode-peer",function(){let src=$(this).attr("img_src");$.ajax({url:src,method:"GET"}).done(function(res){$("#qrcode_img").attr("src",res);qrcodeModal.toggle()})});let deleteModal=new bootstrap.Modal(document.getElementById("delete_modal"),{keyboard:false});$body.on("click",".btn-delete-peer",function(){let peer_id=$(this).attr("id");$("#delete_peer").attr("peer_id",peer_id);deleteModal.toggle()});$("#delete_peer").on("click",function(){$(this).attr("disabled","disabled");$(this).html("Deleting...");let peer_id=$(this).attr("peer_id");let config=$(this).attr("conf_id");$.ajax({method:"POST",url:"/remove_peer/"+config,headers:{"Content-Type":"application/json"},data:JSON.stringify({action:"delete",peer_id:peer_id}),success:function(response){if(response!=="true"){$("#remove_peer_alert").html(response+$("#add_peer_alert").html()).removeClass("d-none")}else{deleteModal.toggle();load_data($("#search_peer_textbox").val());$("#alertToast").toast("show");$("#alertToast .toast-body").html("Peer deleted!");$("#delete_peer").removeAttr("disabled").html("Delete")}}})});let settingModal=new bootstrap.Modal(document.getElementById("setting_modal"),{keyboard:false});$body.on("click",".btn-setting-peer",function(){startProgressBar();let peer_id=$(this).attr("id");$("#save_peer_setting").attr("peer_id",peer_id);$.ajax({method:"POST",url:"/get_peer_data/"+$("#setting_modal").attr("conf_id"),headers:{"Content-Type":"application/json"},data:JSON.stringify({id:peer_id}),success:function(response){let peer_name=response.name===""?"Untitled Peer":response.name;$("#setting_modal .peer_name").html(peer_name);$("#setting_modal #peer_name_textbox").val(response.name);$("#setting_modal #peer_private_key_textbox").val(response.private_key);$("#setting_modal #peer_DNS_textbox").val(response.DNS);$("#setting_modal #peer_allowed_ip_textbox").val(response.allowed_ip);$("#setting_modal #peer_endpoint_allowed_ips").val(response.endpoint_allowed_ip);$("#setting_modal #peer_mtu").val(response.mtu);$("#setting_modal #peer_keep_alive").val(response.keep_alive);$("#setting_modal #peer_preshared_key_textbox").val(response.preshared_key);settingModal.toggle();endProgressBar()}})});$("#setting_modal").on("hidden.bs.modal",function(event){$("#setting_peer_alert").addClass("d-none")});$("#peer_private_key_textbox").on("change",function(){let $save_peer_setting=$("#save_peer_setting");if($(this).val().length>0){$.ajax({url:"/check_key_match/"+$save_peer_setting.attr("conf_id"),method:"POST",headers:{"Content-Type":"application/json"},data:JSON.stringify({private_key:$("#peer_private_key_textbox").val(),public_key:$save_peer_setting.attr("peer_id")})}).done(function(res){if(res.status==="failed"){$("#setting_peer_alert").html(res.status).removeClass("d-none")}else{$("#setting_peer_alert").addClass("d-none")}})}});$("#save_peer_setting").on("click",function(){$(this).attr("disabled","disabled");$(this).html("Saving...");let $peer_DNS_textbox=$("#peer_DNS_textbox");let $peer_allowed_ip_textbox=$("#peer_allowed_ip_textbox");let $peer_endpoint_allowed_ips=$("#peer_endpoint_allowed_ips");let $peer_name_textbox=$("#peer_name_textbox");let $peer_private_key_textbox=$("#peer_private_key_textbox");let $peer_preshared_key_textbox=$("#peer_preshared_key_textbox");let $peer_mtu=$("#peer_mtu");let $peer_keep_alive=$("#peer_keep_alive");if($peer_DNS_textbox.val()!==""&&$peer_allowed_ip_textbox.val()!==""&&$peer_endpoint_allowed_ips.val()!==""){let peer_id=$(this).attr("peer_id");let conf_id=$(this).attr("conf_id");let data_list=[$peer_name_textbox,$peer_DNS_textbox,$peer_private_key_textbox,$peer_preshared_key_textbox,$peer_allowed_ip_textbox,$peer_endpoint_allowed_ips,$peer_mtu,$peer_keep_alive];data_list.forEach(ele=>ele.attr("disabled","disabled"));$.ajax({method:"POST",url:"/save_peer_setting/"+conf_id,headers:{"Content-Type":"application/json"},data:JSON.stringify({id:peer_id,name:$peer_name_textbox.val(),DNS:$peer_DNS_textbox.val(),private_key:$peer_private_key_textbox.val(),allowed_ip:$peer_allowed_ip_textbox.val(),endpoint_allowed_ip:$peer_endpoint_allowed_ips.val(),MTU:$peer_mtu.val(),keep_alive:$peer_keep_alive.val(),preshared_key:$peer_preshared_key_textbox.val()}),success:function(response){if(response.status==="failed"){$("#setting_peer_alert").html(response.msg).removeClass("d-none")}else{settingModal.toggle();load_data($("#search_peer_textbox").val());$("#alertToast").toast("show");$("#alertToast .toast-body").html("Peer Saved!")}$("#save_peer_setting").removeAttr("disabled").html("Save");data_list.forEach(ele=>ele.removeAttr("disabled"))}})}else{$("#setting_peer_alert").html("Please fill in all required box.").removeClass("d-none");$("#save_peer_setting").removeAttr("disabled").html("Save")}});$(".peer_private_key_textbox_switch").on("click",function(){let $peer_private_key_textbox=$("#peer_private_key_textbox");let mode=$peer_private_key_textbox.attr("type")==="password"?"text":"password";let icon=$peer_private_key_textbox.attr("type")==="password"?"bi bi-eye-slash-fill":"bi bi-eye-fill";$peer_private_key_textbox.attr("type",mode);$(".peer_private_key_textbox_switch i").removeClass().addClass(icon)});let typingTimer;let doneTypingInterval=200;let $input=$("#search_peer_textbox");$input.on("keyup",function(){clearTimeout(typingTimer);typingTimer=setTimeout(doneTyping,doneTypingInterval)});$input.on("keydown",function(){clearTimeout(typingTimer)});function doneTyping(){load_data($input.val())}$body.on("change","#sort_by_dropdown",function(){$.ajax({method:"POST",data:JSON.stringify({sort:$("#sort_by_dropdown option:selected").val()}),headers:{"Content-Type":"application/json"},url:"/update_dashboard_sort",success:function(res){load_data($("#search_peer_textbox").val())}})});$body.on("mouseenter",".key",function(){let label=$(this).parent().siblings().children()[1];label.style.opacity="100"});$body.on("mouseout",".key",function(){let label=$(this).parent().siblings().children()[1];label.style.opacity="0";setTimeout(function(){label.innerHTML="CLICK TO COPY"},200)});$body.on("click",".key",function(){var label=$(this).parent().siblings().children()[1];copyToClipboard($(this));label.innerHTML="COPIED!"});function copyToClipboard(element){let $temp=$("");$body.append($temp);$temp.val($(element).text()).trigger("select");document.execCommand("copy");$temp.remove()}$body.on("click",".update_interval",function(){let prev=$(".interval-btn-group.active button");$(".interval-btn-group button").removeClass("active");let _new=$(this);_new.addClass("active");let interval=$(this).data("refresh-interval");$.ajax({method:"POST",data:"interval="+$(this).data("refresh-interval"),url:"/update_dashboard_refresh_interval",success:function(res){if(res==="true"){load_interval=interval;clearInterval(load_timeout);load_timeout=setInterval(function(){load_data($("#search_peer_textbox").val())},interval);showToast("Refresh Interval set to "+Math.round(interval/1e3)+" seconds")}else{$(".interval-btn-group button").removeClass("active");$('.interval-btn-group button[data-refresh-interval="'+load_interval+'"]').addClass("active");showToast("Refresh Interval set unsuccessful")}}})});$body.on("click",".refresh",function(){load_data($("#search_peer_textbox").val())});$body.on("click",".display_mode",function(){$(".display-btn-group button").removeClass("active");$(this).addClass("active");let display_mode=$(this).data("display-mode");$.ajax({method:"GET",url:"/switch_display_mode/"+$(this).data("display-mode"),success:function(res){if(res==="true"){if(display_mode==="list"){Array($(".peer_list").children()).forEach(function(child){$(child).removeClass().addClass("col-12")});showToast("Displaying as List")}else{Array($(".peer_list").children()).forEach(function(child){$(child).removeClass().addClass("col-sm-6 col-lg-4")});showToast("Displaying as Grids")}}}})}); \ No newline at end of file +(function(){let configuration_interval;let configuration_timeout=0;let $progress_bar=$(".progress-bar");let bootstrapModalConfig={keyboard:false,backdrop:"static"};let addModal=new bootstrap.Modal(document.getElementById("add_modal"),bootstrapModalConfig);let deleteBulkModal=new bootstrap.Modal(document.getElementById("delete_bulk_modal"),bootstrapModalConfig);let ipModal=new bootstrap.Modal(document.getElementById("available_ip_modal"),bootstrapModalConfig);let qrcodeModal=new bootstrap.Modal(document.getElementById("qrcode_modal"),bootstrapModalConfig);let settingModal=new bootstrap.Modal(document.getElementById("setting_modal"),bootstrapModalConfig);let deleteModal=new bootstrap.Modal(document.getElementById("delete_modal"),bootstrapModalConfig);$("[data-toggle='tooltip']").tooltip();$("[data-toggle='popover']").popover();function configurationAlert(response){if(response.listen_port===""&&response.status==="stopped"){let configAlert=document.createElement("div");configAlert.classList.add("alert");configAlert.classList.add("alert-warning");configAlert.setAttribute("role","alert");configAlert.innerHTML="Peer QR Code and configuration file download required a specified Listen Port.";document.querySelector("#config_info_alert").appendChild(configAlert)}if(response.conf_address==="N/A"){let configAlert=document.createElement("div");configAlert.classList.add("alert");configAlert.classList.add("alert-warning");configAlert.setAttribute("role","alert");configAlert.innerHTML="Configuration Address need to be specified to have peers connect to it.";document.querySelector("#config_info_alert").appendChild(configAlert)}}function configurationHeader(response){let $conf_status_btn=document.getElementById("conf_status_btn");if(response.checked==="checked"){$conf_status_btn.innerHTML=` ON`}else{$conf_status_btn.innerHTML=` OFF`}$conf_status_btn.classList.remove("info_loading");document.querySelectorAll("#sort_by_dropdown option").forEach(ele=>ele.removeAttribute("selected"));document.querySelector(`#sort_by_dropdown option[value="${response.sort_tag}"]`).setAttribute("selected","selected");document.querySelectorAll(".interval-btn-group button").forEach(ele=>ele.classList.remove("active"));document.querySelector(`button[data-refresh-interval="${response.dashboard_refresh_interval}"]`).classList.add("active");document.querySelectorAll(".display-btn-group button").forEach(ele=>ele.classList.remove("active"));document.querySelector(`button[data-display-mode="${response.peer_display_mode}"]`).classList.add("active");document.querySelector("#conf_status").innerHTML=`${response.status}`;document.querySelector("#conf_connected_peers").innerHTML=response.running_peer;document.querySelector("#conf_total_data_usage").innerHTML=`${response.total_data_usage[0]} GB`;document.querySelector("#conf_total_data_received").innerHTML=`${response.total_data_usage[2]} GB`;document.querySelector("#conf_total_data_sent").innerHTML=`${response.total_data_usage[1]} GB`;document.querySelector("#conf_public_key").innerHTML=response.public_key;document.querySelector("#conf_listen_port").innerHTML=response.listen_port===""?"N/A":response.listen_port;document.querySelector("#conf_address").innerHTML=response.conf_address;document.querySelectorAll(".info h6").forEach(ele=>ele.classList.remove("info_loading"))}function configurationPeers(response){let result="";if(response.peer_data.length===0){document.querySelector(".peer_list").innerHTML=`

Oops! No peers found ‘︿’

`}else{let display_mode=response.peer_display_mode==="list"?"col-12":"col-sm-6 col-lg-4";response.peer_data.forEach(function(peer){let total_r=0;let total_s=0;total_r+=peer.cumu_receive;total_s+=peer.cumu_sent;let spliter='
';let peer_name='
'+'
'+(peer.name===""?"Untitled":peer.name)+"
"+'
'+"
";let peer_transfer='

'+roundN(peer.total_receive+total_r,4)+' GB

'+roundN(peer.total_sent+total_s,4)+" GB

";let peer_key='
PEERCLICK TO COPY
'+peer.id+"
";let peer_allowed_ip='
ALLOWED IP
'+peer.allowed_ip+"
";let peer_latest_handshake='
LATEST HANDSHAKE
'+peer.latest_handshake+"
";let peer_endpoint='
END POINT
'+peer.endpoint+"
";let peer_control='

';if(peer.private_key!==""){peer_control+=''}peer_control+="
";let html='
'+'
'+'
'+'
'+peer_name+spliter+peer_transfer+peer_key+peer_allowed_ip+peer_latest_handshake+spliter+peer_endpoint+spliter+peer_control+"
"+"
"+"
"+"
";result+=html});document.querySelector(".peer_list").innerHTML=result;if(response.dashboard_refresh_interval!==configuration_timeout){configuration_timeout=response.dashboard_refresh_interval;removeConfigurationInterval();setConfigurationInterval()}}}function addPeersByBulk(){let $new_add_amount=$("#new_add_amount");$add_peer.setAttribute("disabled","disabled");$add_peer.innerHTML=`Adding ${$new_add_amount.val()} peers...`;let $new_add_DNS=$("#new_add_DNS");$new_add_DNS.val(window.configurations.cleanIp($new_add_DNS.val()));let $new_add_endpoint_allowed_ip=$("#new_add_endpoint_allowed_ip");$new_add_endpoint_allowed_ip.val(window.configurations.cleanIp($new_add_endpoint_allowed_ip.val()));let $new_add_MTU=$("#new_add_MTU");let $new_add_keep_alive=$("#new_add_keep_alive");let $enable_preshare_key=$("#enable_preshare_key");let data_list=[$new_add_DNS,$new_add_endpoint_allowed_ip,$new_add_MTU,$new_add_keep_alive];if($new_add_amount.val()>0&&!$new_add_amount.hasClass("is-invalid")){if($new_add_DNS.val()!==""&&$new_add_endpoint_allowed_ip.val()!==""){let conf=$add_peer.getAttribute("conf_id");let keys=[];for(let i=0;i<$new_add_amount.val();i++){keys.push(window.wireguard.generateKeypair())}$.ajax({method:"POST",url:"/add_peer_bulk/"+conf,headers:{"Content-Type":"application/json"},data:JSON.stringify({DNS:$new_add_DNS.val(),endpoint_allowed_ip:$new_add_endpoint_allowed_ip.val(),MTU:$new_add_MTU.val(),keep_alive:$new_add_keep_alive.val(),enable_preshared_key:$enable_preshare_key.prop("checked"),keys:keys,amount:$new_add_amount.val()}),success:function(response){if(response!=="true"){$("#add_peer_alert").html(response).removeClass("d-none");data_list.forEach(ele=>ele.removeAttr("disabled"));$add_peer.removeAttribute("disabled");$add_peer.innerHTML="Save"}else{window.configurations.loadPeers("");data_list.forEach(ele=>ele.removeAttr("disabled"));$("#add_peer_form").trigger("reset");$add_peer.removeAttribute("disabled");$add_peer.innerHTML="Save";window.configurations.showToast($new_add_amount.val()+" peers added successful!");window.configurations.addModal().toggle()}}})}else{$("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none");$add_peer.removeAttribute("disabled");$add_peer.innerHTML="Add"}}else{$add_peer.removeAttribute("disabled");$add_peer.innerHTML="Add"}}function deletePeers(config,peer_ids){$.ajax({method:"POST",url:"/remove_peer/"+config,headers:{"Content-Type":"application/json"},data:JSON.stringify({action:"delete",peer_ids:peer_ids}),success:function(response){if(response!=="true"){if(window.configurations.deleteModal()._isShown){$("#remove_peer_alert").html(response+$("#add_peer_alert").html()).removeClass("d-none");$("#delete_peer").removeAttr("disabled").html("Delete")}if(window.configurations.deleteBulkModal()._isShown){let $bulk_remove_peer_alert=$("#bulk_remove_peer_alert");$bulk_remove_peer_alert.html(response+$bulk_remove_peer_alert.html()).removeClass("d-none");$("#confirm_delete_bulk_peers").removeAttr("disabled").html("Delete")}}else{if(window.configurations.deleteModal()._isShown){window.configurations.deleteModal().toggle()}if(window.configurations.deleteBulkModal()._isShown){$("#confirm_delete_bulk_peers").removeAttr("disabled").html("Delete");$("#selected_peer_list").html("");$(".delete-bulk-peer-item.active").removeClass("active");window.configurations.deleteBulkModal().toggle()}window.configurations.loadPeers($("#search_peer_textbox").val());$("#alertToast").toast("show");$("#alertToast .toast-body").html("Peer deleted!");$("#delete_peer").removeAttr("disabled").html("Delete")}}})}function noResponding(){document.querySelectorAll(".no-response").forEach(ele=>ele.classList.add("active"));setTimeout(function(){document.querySelectorAll(".no-response").forEach(ele=>ele.classList.add("show"));document.querySelector("#right_body").classList.add("no-responding");document.querySelector(".navbar").classList.add("no-responding")},10)}function removeNoResponding(){document.querySelectorAll(".no-response").forEach(ele=>ele.classList.remove("show"));document.querySelector("#right_body").classList.remove("no-responding");document.querySelector(".navbar").classList.remove("no-responding");setTimeout(function(){document.querySelectorAll(".no-response").forEach(ele=>ele.classList.remove("active"))},1010)}function setConfigurationInterval(){configuration_interval=setInterval(function(){loadPeers($("#search_peer_textbox").val())},configuration_timeout)}function removeConfigurationInterval(){clearInterval(configuration_interval)}function startProgressBar(){$progress_bar.css("width","0%").css("opacity","100").css("background","rgb(255,69,69)").css("background","linear-gradient(145deg, rgba(255,69,69,1) 0%, rgba(0,115,186,1) 100%)").css("width","25%");setTimeout(function(){stillLoadingProgressBar()},300)}function stillLoadingProgressBar(){$progress_bar.css("transition","3s ease-in-out").css("width","75%")}function endProgressBar(){$progress_bar.css("transition","0.3s ease-in-out").css("width","100%");setTimeout(function(){$progress_bar.css("opacity","0")},250)}function roundN(value,digits){let tenToN=10**digits;return Math.round(value*tenToN)/tenToN}function loadPeers(searchString){startProgressBar();$.ajax({method:"GET",url:`/get_config/${conf_name}?search=${encodeURIComponent(searchString)}`,headers:{"Content-Type":"application/json"}}).done(function(response){removeNoResponding();peers=response.peer_data;configurationAlert(response);configurationHeader(response);configurationPeers(response);$(".dot.dot-running").attr("title","Peer Connected").tooltip();$(".dot.dot-stopped").attr("title","Peer Disconnected").tooltip();$("i[data-toggle='tooltip']").tooltip();endProgressBar()}).fail(function(){noResponding()})}function generate_key(){let keys=window.wireguard.generateKeypair();document.querySelector("#private_key").value=keys.privateKey;document.querySelector("#public_key").value=keys.publicKey;document.querySelector("#add_peer_alert").classList.add("d-none");document.querySelector("#re_generate_key i").classList.remove("rotating");document.querySelector("#enable_preshare_key").value=keys.presharedKey}function showToast(msg){$("#alertToast").toast("show");$("#alertToast .toast-body").html(msg)}function updateRefreshInterval(res,interval){if(res==="true"){configuration_timeout=interval;removeConfigurationInterval();setConfigurationInterval();showToast("Refresh Interval set to "+Math.round(interval/1e3)+" seconds")}else{$(".interval-btn-group button").removeClass("active");$('.interval-btn-group button[data-refresh-interval="'+configuration_timeout+'"]').addClass("active");showToast("Refresh Interval set unsuccessful")}}function cleanIp(val){let clean_ip=val.split(",");for(let i=0;i${ip}`}}}function download_one_config(conf){let link=document.createElement("a");link.download=conf.filename;let blob=new Blob([conf.content],{type:"text/conf"});link.href=window.URL.createObjectURL(blob);link.click()}function toggleBulkIP(element){let $selected_peer_list=$("#selected_peer_list");let id=element.data("id");let name=element.data("name")===""?"Untitled Peer":element.data("name");if(element.hasClass("active")){element.removeClass("active");$("#selected_peer_list .badge[data-id='"+id+"']").remove()}else{element.addClass("active");$selected_peer_list.append(''+name+" - "+id+"")}}function copyToClipboard(element){let $temp=$("");$body.append($temp);$temp.val($(element).text()).trigger("select");document.execCommand("copy");$temp.remove()}function getAvailableIps(){$.ajax({url:`/available_ips/${$add_peer.getAttribute("conf_id")}`,method:"GET"}).done(function(res){available_ips=res;let $list_group=document.querySelector("#available_ip_modal .modal-body .list-group");$list_group.innerHTML="";document.querySelector("#allowed_ips").value=available_ips[0];available_ips.forEach(ip=>$list_group.innerHTML+=`${ip}`)})}window.configurations={addModal:()=>{return addModal},deleteBulkModal:()=>{return deleteBulkModal},deleteModal:()=>{return deleteModal},ipModal:()=>{return ipModal},qrcodeModal:()=>{return qrcodeModal},settingModal:()=>{return settingModal},loadPeers:searchString=>{loadPeers(searchString)},addPeersByBulk:()=>{addPeersByBulk()},deletePeers:(config,peers_ids)=>{deletePeers(config,peers_ids)},getAvailableIps:()=>{getAvailableIps()},generateKeyPair:()=>{generate_key()},showToast:message=>{showToast(message)},updateRefreshInterval:(res,interval)=>{updateRefreshInterval(res,interval)},copyToClipboard:element=>{copyToClipboard(element)},toggleDeleteByBulkIP:element=>{toggleBulkIP(element)},downloadOneConfig:conf=>{download_one_config(conf)},triggerIp:ip=>{trigger_ip(ip)},cleanIp:val=>{return cleanIp(val)},startProgressBar:()=>{startProgressBar()},stillLoadingProgressBar:()=>{stillLoadingProgressBar()},endProgressBar:()=>{endProgressBar()}}})();let $body=$("body");let available_ips=[];let $add_peer=document.getElementById("save_peer");document.querySelector(".add_btn").addEventListener("click",()=>{window.configurations.addModal().toggle()});document.querySelector(".info").addEventListener("click",event=>{let selector=document.querySelector(".switch");if(selector.contains(event.target)){selector.style.display="none";document.querySelector("div[role=status]").style.display="inline-block";location.replace(`/switch/${selector.getAttribute("id")}`)}});document.querySelector("#private_key").addEventListener("change",event=>{let publicKey=document.querySelector("#public_key");if(event.target.value.length===44){publicKey.value=window.wireguard.generatePublicKey(event.target.value);publicKey.setAttribute("disabled","disabled")}else{publicKey.attributes.removeNamedItem("disabled");publicKey.value=""}});$("#add_modal").on("show.bs.modal",function(){window.configurations.generateKeyPair();window.configurations.getAvailableIps()}).on("hide.bs.modal",function(){$("#allowed_ips_indicator").html("")});$("#re_generate_key").on("click",function(){$("#public_key").attr("disabled","disabled");$("#re_generate_key i").addClass("rotating");window.configurations.generateKeyPair()});$("#allowed_ips").on("keyup",function(){let s=window.configurations.cleanIp($(this).val());s=s.split(",");if(available_ips.includes(s[s.length-1])){$("#allowed_ips_indicator").removeClass().addClass("text-success").html('')}else{$("#allowed_ips_indicator").removeClass().addClass("text-warning").html('')}});$("#peer_name_textbox").on("keyup",function(){$(".peer_name").html($(this).val())});$add_peer.addEventListener("click",function(){let $bulk_add=$("#bulk_add");if($bulk_add.prop("checked")){if(!$("#new_add_amount").hasClass("is-invalid")){window.configurations.addPeersByBulk()}}else{let $public_key=$("#public_key");let $private_key=$("#private_key");let $allowed_ips=$("#allowed_ips");$allowed_ips.val(window.configurations.cleanIp($allowed_ips.val()));let $new_add_DNS=$("#new_add_DNS");$new_add_DNS.val(window.configurations.cleanIp($new_add_DNS.val()));let $new_add_endpoint_allowed_ip=$("#new_add_endpoint_allowed_ip");$new_add_endpoint_allowed_ip.val(window.configurations.cleanIp($new_add_endpoint_allowed_ip.val()));let $new_add_name=$("#new_add_name");let $new_add_MTU=$("#new_add_MTU");let $new_add_keep_alive=$("#new_add_keep_alive");let $enable_preshare_key=$("#enable_preshare_key");$add_peer.setAttribute("disabled","disabled");$add_peer.innerHTML="Adding...";if($allowed_ips.val()!==""&&$public_key.val()!==""&&$new_add_DNS.val()!==""&&$new_add_endpoint_allowed_ip.val()!==""){let conf=$add_peer.getAttribute("conf_id");let data_list=[$private_key,$allowed_ips,$new_add_name,$new_add_DNS,$new_add_endpoint_allowed_ip,$new_add_MTU,$new_add_keep_alive];data_list.forEach(ele=>ele.attr("disabled","disabled"));$.ajax({method:"POST",url:"/add_peer/"+conf,headers:{"Content-Type":"application/json"},data:JSON.stringify({private_key:$private_key.val(),public_key:$public_key.val(),allowed_ips:$allowed_ips.val(),name:$new_add_name.val(),DNS:$new_add_DNS.val(),endpoint_allowed_ip:$new_add_endpoint_allowed_ip.val(),MTU:$new_add_MTU.val(),keep_alive:$new_add_keep_alive.val(),enable_preshared_key:$enable_preshare_key.prop("checked"),preshared_key:$enable_preshare_key.val()}),success:function(response){if(response!=="true"){$("#add_peer_alert").html(response).removeClass("d-none");data_list.forEach(ele=>ele.removeAttr("disabled"));$add_peer.removeAttribute("disabled");$add_peer.innerHTML="Save"}else{window.configurations.loadPeers("");data_list.forEach(ele=>ele.removeAttr("disabled"));$("#add_peer_form").trigger("reset");$add_peer.removeAttribute("disabled");$add_peer.innerHTML="Save";window.configurations.showToast("Add peer successful!");window.configurations.addModal().toggle()}}})}else{$("#add_peer_alert").html("Please fill in all required box.").removeClass("d-none");$add_peer.removeAttribute("disabled");$add_peer.innerHTML="Add"}}});$("#new_add_amount").on("keyup",function(){let $bulk_amount_validation=$("#bulk_amount_validation");if($(this).val().length>0){if(isNaN($(this).val())){$(this).removeClass("is-valid").addClass("is-invalid");$bulk_amount_validation.html("Please enter a valid integer")}else if($(this).val()>available_ips.length){$(this).removeClass("is-valid").addClass("is-invalid");$bulk_amount_validation.html(`Cannot create more than ${available_ips.length} peers.`)}else if($(this).val()<1){$(this).removeClass("is-valid").addClass("is-invalid");$bulk_amount_validation.html("Please enter at least 1 or more.")}else{$(this).removeClass("is-invalid").addClass("is-valid")}}else{$(this).removeClass("is-invalid").removeClass("is-valid")}});$("#bulk_add").on("change",function(){let hide=$(".non-bulk").find("input");let amount=$("#new_add_amount");if($(this).prop("checked")===true){for(let i=0;i{document.querySelector("#add_modal").classList.add("ip_modal_open")}).on("hidden.bs.modal",()=>{document.querySelector("#add_modal").classList.remove("ip_modal_open");let ips=[];let $selected_ip_list=document.querySelector("#selected_ip_list");for(let i=0;i<$selected_ip_list.childElementCount;i++){ips.push($selected_ip_list.children[i].dataset.ip)}ips.forEach(ele=>window.configurations.triggerIp(ele))});$body.on("click",".available-ip-badge",function(){$(".available-ip-item[data-ip='"+$(this).data("ip")+"']").removeClass("active");$(this).remove()});$body.on("click",".available-ip-item",function(){window.configurations.triggerIp($(this).data("ip"))});$("#search_available_ip").on("click",function(){window.configurations.ipModal().toggle();let $allowed_ips=document.querySelector("#allowed_ips");if($allowed_ips.value.length>0){let s=$allowed_ips.value.split(",");for(let i=0;i{window.configurations.ipModal().toggle();let ips=[];let $selected_ip_list=$("#selected_ip_list");$selected_ip_list.children().each(function(){ips.push($(this).data("ip"))});$("#allowed_ips").val(ips.join(", "));ips.forEach(ele=>window.configurations.triggerIp(ele))});$body.on("click",".btn-qrcode-peer",function(){let src=$(this).data("imgsrc");$.ajax({url:src,method:"GET"}).done(function(res){$("#qrcode_img").attr("src",res);window.configurations.qrcodeModal().toggle()})});$body.on("click",".btn-delete-peer",function(){let peer_id=$(this).attr("id");$("#delete_peer").attr("peer_id",peer_id);window.configurations.deleteModal().toggle()});$("#delete_peer").on("click",function(){$(this).attr("disabled","disabled");$(this).html("Deleting...");let peer_id=$(this).attr("peer_id");let config=$(this).attr("conf_id");let peer_ids=[peer_id];window.configurations.deletePeers(config,peer_ids)});$body.on("click",".btn-setting-peer",function(){window.configurations.startProgressBar();let peer_id=$(this).attr("id");$("#save_peer_setting").attr("peer_id",peer_id);$.ajax({method:"POST",url:"/get_peer_data/"+$("#setting_modal").attr("conf_id"),headers:{"Content-Type":"application/json"},data:JSON.stringify({id:peer_id}),success:function(response){let peer_name=response.name===""?"Untitled":response.name;$("#setting_modal .peer_name").html(peer_name);$("#setting_modal #peer_name_textbox").val(response.name);$("#setting_modal #peer_private_key_textbox").val(response.private_key);$("#setting_modal #peer_DNS_textbox").val(response.DNS);$("#setting_modal #peer_allowed_ip_textbox").val(response.allowed_ip);$("#setting_modal #peer_endpoint_allowed_ips").val(response.endpoint_allowed_ip);$("#setting_modal #peer_mtu").val(response.mtu);$("#setting_modal #peer_keep_alive").val(response.keep_alive);$("#setting_modal #peer_preshared_key_textbox").val(response.preshared_key);window.configurations.settingModal().toggle();window.configurations.endProgressBar()}})});$("#setting_modal").on("hidden.bs.modal",function(){$("#setting_peer_alert").addClass("d-none")});$("#peer_private_key_textbox").on("change",function(){let $save_peer_setting=$("#save_peer_setting");if($(this).val().length>0){$.ajax({url:"/check_key_match/"+$save_peer_setting.attr("conf_id"),method:"POST",headers:{"Content-Type":"application/json"},data:JSON.stringify({private_key:$("#peer_private_key_textbox").val(),public_key:$save_peer_setting.attr("peer_id")})}).done(function(res){if(res.status==="failed"){$("#setting_peer_alert").html(res.status).removeClass("d-none")}else{$("#setting_peer_alert").addClass("d-none")}})}});$("#save_peer_setting").on("click",function(){$(this).attr("disabled","disabled");$(this).html("Saving...");let $peer_DNS_textbox=$("#peer_DNS_textbox");let $peer_allowed_ip_textbox=$("#peer_allowed_ip_textbox");let $peer_endpoint_allowed_ips=$("#peer_endpoint_allowed_ips");let $peer_name_textbox=$("#peer_name_textbox");let $peer_private_key_textbox=$("#peer_private_key_textbox");let $peer_preshared_key_textbox=$("#peer_preshared_key_textbox");let $peer_mtu=$("#peer_mtu");let $peer_keep_alive=$("#peer_keep_alive");if($peer_DNS_textbox.val()!==""&&$peer_allowed_ip_textbox.val()!==""&&$peer_endpoint_allowed_ips.val()!==""){let peer_id=$(this).attr("peer_id");let conf_id=$(this).attr("conf_id");let data_list=[$peer_name_textbox,$peer_DNS_textbox,$peer_private_key_textbox,$peer_preshared_key_textbox,$peer_allowed_ip_textbox,$peer_endpoint_allowed_ips,$peer_mtu,$peer_keep_alive];data_list.forEach(ele=>ele.attr("disabled","disabled"));$.ajax({method:"POST",url:"/save_peer_setting/"+conf_id,headers:{"Content-Type":"application/json"},data:JSON.stringify({id:peer_id,name:$peer_name_textbox.val(),DNS:$peer_DNS_textbox.val(),private_key:$peer_private_key_textbox.val(),allowed_ip:$peer_allowed_ip_textbox.val(),endpoint_allowed_ip:$peer_endpoint_allowed_ips.val(),MTU:$peer_mtu.val(),keep_alive:$peer_keep_alive.val(),preshared_key:$peer_preshared_key_textbox.val()}),success:function(response){if(response.status==="failed"){$("#setting_peer_alert").html(response.msg).removeClass("d-none")}else{window.configurations.settingModal().toggle();window.configurations.loadPeers($("#search_peer_textbox").val());$("#alertToast").toast("show");$("#alertToast .toast-body").html("Peer Saved!")}$("#save_peer_setting").removeAttr("disabled").html("Save");data_list.forEach(ele=>ele.removeAttr("disabled"))}})}else{$("#setting_peer_alert").html("Please fill in all required box.").removeClass("d-none");$("#save_peer_setting").removeAttr("disabled").html("Save")}});$(".peer_private_key_textbox_switch").on("click",function(){let $peer_private_key_textbox=$("#peer_private_key_textbox");let mode=$peer_private_key_textbox.attr("type")==="password"?"text":"password";let icon=$peer_private_key_textbox.attr("type")==="password"?"bi bi-eye-slash-fill":"bi bi-eye-fill";$peer_private_key_textbox.attr("type",mode);$(".peer_private_key_textbox_switch i").removeClass().addClass(icon)});let typingTimer;let doneTypingInterval=200;$("#search_peer_textbox").on("keyup",function(){clearTimeout(typingTimer);typingTimer=setTimeout(()=>{window.configurations.loadPeers($(this).val())},doneTypingInterval)}).on("keydown",function(){clearTimeout(typingTimer)});$body.on("change","#sort_by_dropdown",function(){$.ajax({method:"POST",data:JSON.stringify({sort:$("#sort_by_dropdown option:selected").val()}),headers:{"Content-Type":"application/json"},url:"/update_dashboard_sort",success:function(){window.configurations.loadPeers($("#search_peer_textbox").val())}})});$body.on("mouseenter",".key",function(){let label=$(this).parent().siblings().children()[1];label.style.opacity="100"}).on("mouseout",".key",function(){let label=$(this).parent().siblings().children()[1];label.style.opacity="0";setTimeout(function(){label.innerHTML="CLICK TO COPY"},200)}).on("click",".key",function(){let label=$(this).parent().siblings().children()[1];window.configurations.copyToClipboard($(this));label.innerHTML="COPIED!"});$body.on("click",".update_interval",function(){$(".interval-btn-group button").removeClass("active");let _new=$(this);_new.addClass("active");let interval=$(this).data("refresh-interval");$.ajax({method:"POST",data:"interval="+$(this).data("refresh-interval"),url:"/update_dashboard_refresh_interval",success:function(res){window.configurations.updateRefreshInterval(res,interval)}})});$body.on("click",".refresh",function(){window.configurations.loadPeers($("#search_peer_textbox").val())});$body.on("click",".display_mode",function(){$(".display-btn-group button").removeClass("active");$(this).addClass("active");let display_mode=$(this).data("display-mode");$.ajax({method:"GET",url:"/switch_display_mode/"+$(this).data("display-mode"),success:function(res){if(res==="true"){if(display_mode==="list"){Array($(".peer_list").children()).forEach(function(child){$(child).removeClass().addClass("col-12")});window.configurations.showToast("Displaying as List")}else{Array($(".peer_list").children()).forEach(function(child){$(child).removeClass().addClass("col-sm-6 col-lg-4")});window.configurations.showToast("Displaying as Grids")}}}})});let $setting_btn_menu=$(".setting_btn_menu");$setting_btn_menu.css("top",($setting_btn_menu.height()+54)*-1);let $setting_btn=$(".setting_btn");$setting_btn.on("click",function(){if($setting_btn_menu.hasClass("show")){$setting_btn_menu.removeClass("showing");setTimeout(function(){$setting_btn_menu.removeClass("show")},201)}else{$setting_btn_menu.addClass("show");setTimeout(function(){$setting_btn_menu.addClass("showing")},10)}});$("html").on("click",function(r){if(document.querySelector(".setting_btn")!==r.target){if(!document.querySelector(".setting_btn").contains(r.target)){if(!document.querySelector(".setting_btn_menu").contains(r.target)){$setting_btn_menu.removeClass("showing");setTimeout(function(){$setting_btn_menu.removeClass("show")},310)}}}});$("#delete_peers_by_bulk_btn").on("click",()=>{let $delete_bulk_modal_list=$("#delete_bulk_modal .list-group");$delete_bulk_modal_list.html("");peers.forEach(peer=>{let name;if(peer.name===""){name="Untitled Peer"}else{name=peer.name}$delete_bulk_modal_list.append(''+name+"
"+peer.id+"
")});window.configurations.deleteBulkModal().toggle()});$body.on("click",".delete-bulk-peer-item",function(){window.configurations.toggleDeleteByBulkIP($(this))}).on("click",".delete-peer-bulk-badge",function(){window.configurations.toggleDeleteByBulkIP($(".delete-bulk-peer-item[data-id='"+$(this).data("id")+"']"))});let $selected_peer_list=document.getElementById("selected_peer_list");let changeObserver=new MutationObserver(function(){if($selected_peer_list.hasChildNodes()){$("#confirm_delete_bulk_peers").removeAttr("disabled")}else{$("#confirm_delete_bulk_peers").attr("disabled","disabled")}});changeObserver.observe($selected_peer_list,{attributes:true,childList:true,characterData:true});let confirm_delete_bulk_peers_interval;$("#confirm_delete_bulk_peers").on("click",function(){let btn=$(this);if(confirm_delete_bulk_peers_interval!==undefined){clearInterval(confirm_delete_bulk_peers_interval);confirm_delete_bulk_peers_interval=undefined;btn.html("Delete")}else{let timer=5;btn.html(`Deleting in ${timer} secs... Click to cancel`);confirm_delete_bulk_peers_interval=setInterval(function(){timer-=1;btn.html(`Deleting in ${timer} secs... Click to cancel`);if(timer===0){btn.html(`Deleting...`);btn.attr("disabled","disabled");let ips=[];$selected_peer_list.childNodes.forEach(ele=>ips.push(ele.dataset.id));window.configurations.deletePeers(btn.data("conf"),ips);clearInterval(confirm_delete_bulk_peers_interval);confirm_delete_bulk_peers_interval=undefined}},1e3)}});$("#select_all_delete_bulk_peers").on("click",function(){$(".delete-bulk-peer-item").each(function(){if(!$(this).hasClass("active")){window.configurations.toggleDeleteByBulkIP($(this))}})});$(window.configurations.deleteBulkModal()._element).on("hidden.bs.modal",function(){$(".delete-bulk-peer-item").each(function(){if($(this).hasClass("active")){window.configurations.toggleDeleteByBulkIP($(this))}})});$body.on("click",".btn-download-peer",function(e){e.preventDefault();let link=$(this).attr("href");$.ajax({url:link,method:"GET",success:function(res){window.configurations.downloadOneConfig(res)}})});$("#download_all_peers").on("click",function(){$.ajax({url:$(this).data("url"),method:"GET",success:function(res){if(res.peers.length>0){window.wireguard.generateZipFiles(res);window.configurations.showToast("Peers' zip file download successful!")}else{window.configurations.showToast("Oops! There are no peer can be download.")}}})}); \ No newline at end of file diff --git a/src/static/js/configurationsTools.js b/src/static/js/configurationsTools.js new file mode 100644 index 0000000..e69de29 diff --git a/src/static/js/configurationsTools.min.js b/src/static/js/configurationsTools.min.js new file mode 100644 index 0000000..e69de29 diff --git a/src/static/js/tools.js b/src/static/js/tools.js index 5459e7e..8623eb3 100644 --- a/src/static/js/tools.js +++ b/src/static/js/tools.js @@ -1,63 +1,67 @@ -$(".ip_dropdown").change(function (){ - $(".modal.show .btn").removeAttr("disabled") +/** + * tools.js - Copyright(C) 2021 Donald Zou [https://github.com/donaldzou] + */ + +$(".ip_dropdown").on("change",function (){ + $(".modal.show .btn").removeAttr("disabled"); }); -$(".conf_dropdown").change(function (){ - $(".modal.show .ip_dropdown").html('