diff --git a/src/dashboard.py b/src/dashboard.py index c113310..6c0ae04 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -1,8 +1,11 @@ import random, shutil, sqlite3, configparser, hashlib, ipaddress, json, os, secrets, subprocess import time, re, uuid, bcrypt, psutil, pyotp, threading +import traceback from uuid import uuid4 from zipfile import ZipFile from datetime import datetime, timedelta + +import sqlalchemy from jinja2 import Template from flask import Flask, request, render_template, session, send_file from flask_cors import CORS @@ -22,6 +25,9 @@ from modules.SystemStatus import SystemStatus from modules.PeerShareLinks import PeerShareLinks from modules.PeerJobs import PeerJobs from modules.DashboardConfig import DashboardConfig +from modules.WireguardConfiguration import WireguardConfiguration +from modules.AmneziaWireguardConfiguration import AmneziaWireguardConfiguration + SystemStatus = SystemStatus() @@ -55,1449 +61,13 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F -""" -WireGuard Configuration -""" -class WireguardConfiguration: - class InvalidConfigurationFileException(Exception): - def __init__(self, m): - self.message = m - def __str__(self): - return self.message - - def __init__(self, name: str = None, data: dict = None, backup: dict = None, startup: bool = False, wg: bool = True): - - - self.__parser: configparser.ConfigParser = configparser.RawConfigParser(strict=False) - self.__parser.optionxform = str - self.__configFileModifiedTime = None - - self.Status: bool = False - self.Name: str = "" - self.PrivateKey: str = "" - self.PublicKey: str = "" - - self.ListenPort: str = "" - self.Address: str = "" - self.DNS: str = "" - self.Table: str = "" - self.MTU: str = "" - self.PreUp: str = "" - self.PostUp: str = "" - self.PreDown: str = "" - self.PostDown: str = "" - self.SaveConfig: bool = True - self.Name = name - self.Protocol = "wg" if wg else "awg" - self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf') if wg else os.path.join(DashboardConfig.GetConfig("Server", "awg_conf_path")[1], f'{self.Name}.conf') - - if name is not None: - if data is not None and "Backup" in data.keys(): - db = self.__importDatabase( - os.path.join( - self.__getProtocolPath(), - 'WGDashboard_Backup', - data["Backup"].replace(".conf", ".sql"))) - else: - self.createDatabase() - self.__parseConfigurationFile() - self.__initPeersList() - - else: - self.Name = data["ConfigurationName"] - self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf') - - for i in dir(self): - if str(i) in data.keys(): - if isinstance(getattr(self, i), bool): - setattr(self, i, StringToBoolean(data[i])) - else: - setattr(self, i, str(data[i])) - - self.__parser["Interface"] = { - "PrivateKey": self.PrivateKey, - "Address": self.Address, - "ListenPort": self.ListenPort, - "PreUp": f"{self.PreUp}", - "PreDown": f"{self.PreDown}", - "PostUp": f"{self.PostUp}", - "PostDown": f"{self.PostDown}", - "SaveConfig": "true" - } - - if self.Protocol == 'awg': - self.__parser["Interface"]["Jc"] = self.Jc - self.__parser["Interface"]["Jc"] = self.Jc - self.__parser["Interface"]["Jmin"] = self.Jmin - self.__parser["Interface"]["Jmax"] = self.Jmax - self.__parser["Interface"]["S1"] = self.S1 - self.__parser["Interface"]["S2"] = self.S2 - self.__parser["Interface"]["H1"] = self.H1 - self.__parser["Interface"]["H2"] = self.H2 - self.__parser["Interface"]["H3"] = self.H3 - self.__parser["Interface"]["H4"] = self.H4 - - if "Backup" not in data.keys(): - self.createDatabase() - with open(self.configPath, "w+") as configFile: - self.__parser.write(configFile) - print(f"[WGDashboard] Configuration file {self.configPath} created") - self.__initPeersList() - if not os.path.exists(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')): - os.mkdir(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')) - - print(f"[WGDashboard] Initialized Configuration: {name}") - if self.getAutostartStatus() and not self.getStatus() and startup: - self.toggleConfiguration() - print(f"[WGDashboard] Autostart Configuration: {name}") - - def __getProtocolPath(self): - return DashboardConfig.GetConfig("Server", "wg_conf_path")[1] if self.Protocol == "wg" \ - else DashboardConfig.GetConfig("Server", "awg_conf_path")[1] - def __initPeersList(self): - self.Peers: list[Peer] = [] - self.getPeersList() - self.getRestrictedPeersList() - - def getRawConfigurationFile(self): - return open(self.configPath, 'r').read() + - def updateRawConfigurationFile(self, newRawConfiguration): - backupStatus, backup = self.backupConfigurationFile() - if not backupStatus: - return False, "Cannot create backup" - - if self.Status: - self.toggleConfiguration() - - with open(self.configPath, 'w') as f: - f.write(newRawConfiguration) - - status, err = self.toggleConfiguration() - if not status: - restoreStatus = self.restoreBackup(backup['filename']) - print(f"Restore status: {restoreStatus}") - self.toggleConfiguration() - return False, err - return True, None - - def __parseConfigurationFile(self): - with open(self.configPath, 'r') as f: - original = [l.rstrip("\n") for l in f.readlines()] - try: - start = original.index("[Interface]") - - # Clean - for i in range(start, len(original)): - if original[i] == "[Peer]": - break - split = re.split(r'\s*=\s*', original[i], 1) - if len(split) == 2: - key = split[0] - if key in dir(self): - if isinstance(getattr(self, key), bool): - setattr(self, key, False) - else: - setattr(self, key, "") - - # Set - for i in range(start, len(original)): - if original[i] == "[Peer]": - break - split = re.split(r'\s*=\s*', original[i], 1) - if len(split) == 2: - key = split[0] - value = split[1] - if key in dir(self): - if isinstance(getattr(self, key), bool): - setattr(self, key, StringToBoolean(value)) - else: - if len(getattr(self, key)) > 0: - setattr(self, key, f"{getattr(self, key)}, {value}") - else: - setattr(self, key, value) - except ValueError as e: - raise self.InvalidConfigurationFileException( - "[Interface] section not found in " + self.configPath) - if self.PrivateKey: - self.PublicKey = self.__getPublicKey() - self.Status = self.getStatus() - - def __dropDatabase(self): - existingTables = [self.Name, f'{self.Name}_restrict_access', f'{self.Name}_transfer', f'{self.Name}_deleted'] - # existingTables = sqlSelect(f"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '{self.Name}%'").fetchall() - for t in existingTables: - sqlUpdate("DROP TABLE '%s'" % t) - # existingTables = sqlSelect(f"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '{self.Name}%'").fetchall() - - def createDatabase(self, dbName = None): - if dbName is None: - dbName = self.Name - - existingTables = sqlSelect("SELECT name FROM sqlite_master WHERE type='table'").fetchall() - existingTables = [t['name'] for t in existingTables] - if dbName not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s'( - 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) - ) - """ % dbName - ) - if f'{dbName}_restrict_access' not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s_restrict_access' ( - 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) - ) - """ % dbName - ) - if f'{dbName}_transfer' not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s_transfer' ( - id VARCHAR NOT NULL, total_receive FLOAT NULL, - total_sent FLOAT NULL, total_data FLOAT NULL, - cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, time DATETIME - ) - """ % dbName - ) - if f'{dbName}_deleted' not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s_deleted' ( - 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) - ) - """ % dbName - ) - - def __dumpDatabase(self): - for line in sqldb.iterdump(): - if (line.startswith(f"INSERT INTO \"{self.Name}\"") - or line.startswith(f'INSERT INTO "{self.Name}_restrict_access"') - or line.startswith(f'INSERT INTO "{self.Name}_transfer"') - or line.startswith(f'INSERT INTO "{self.Name}_deleted"') - ): - yield line - - def __importDatabase(self, sqlFilePath) -> bool: - self.__dropDatabase() - self.createDatabase() - if not os.path.exists(sqlFilePath): - return False - with open(sqlFilePath, 'r') as f: - for l in f.readlines(): - l = l.rstrip("\n") - if len(l) > 0: - sqlUpdate(l) - return True - - def __getPublicKey(self) -> str: - return GenerateWireguardPublicKey(self.PrivateKey)[1] - - def getStatus(self) -> bool: - self.Status = self.Name in psutil.net_if_addrs().keys() - return self.Status - - def getAutostartStatus(self): - s, d = DashboardConfig.GetConfig("WireGuardConfiguration", "autostart") - return self.Name in d - - def getRestrictedPeers(self): - self.RestrictedPeers = [] - restricted = sqlSelect("SELECT * FROM '%s_restrict_access'" % self.Name).fetchall() - for i in restricted: - self.RestrictedPeers.append(Peer(i, self)) - - def configurationFileChanged(self) : - mt = os.path.getmtime(self.configPath) - changed = self.__configFileModifiedTime is None or self.__configFileModifiedTime != mt - self.__configFileModifiedTime = mt - return changed - - def getPeers(self): - if self.configurationFileChanged(): - self.Peers = [] - with open(self.configPath, 'r') as configFile: - p = [] - pCounter = -1 - content = configFile.read().split('\n') - try: - peerStarts = content.index("[Peer]") - content = content[peerStarts:] - for i in content: - if not RegexMatch("#(.*)", i) and not RegexMatch(";(.*)", i): - if i == "[Peer]": - pCounter += 1 - p.append({}) - p[pCounter]["name"] = "" - else: - if len(i) > 0: - split = re.split(r'\s*=\s*', i, 1) - if len(split) == 2: - p[pCounter][split[0]] = split[1] - - if RegexMatch("#Name# = (.*)", i): - split = re.split(r'\s*=\s*', i, 1) - if len(split) == 2: - p[pCounter]["name"] = split[1] - - for i in p: - if "PublicKey" in i.keys(): - checkIfExist = sqlSelect("SELECT * FROM '%s' WHERE id = ?" % self.Name, - ((i['PublicKey']),)).fetchone() - if checkIfExist is None: - newPeer = { - "id": i['PublicKey'], - "private_key": "", - "DNS": DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1], - "endpoint_allowed_ip": DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[ - 1], - "name": i.get("name"), - "total_receive": 0, - "total_sent": 0, - "total_data": 0, - "endpoint": "N/A", - "status": "stopped", - "latest_handshake": "N/A", - "allowed_ip": i.get("AllowedIPs", "N/A"), - "cumu_receive": 0, - "cumu_sent": 0, - "cumu_data": 0, - "traffic": [], - "mtu": DashboardConfig.GetConfig("Peers", "peer_mtu")[1], - "keepalive": DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1], - "remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], - "preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else "" - } - sqlUpdate( - """ - INSERT INTO '%s' - 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); - """ % self.Name - , newPeer) - self.Peers.append(Peer(newPeer, self)) - else: - sqlUpdate("UPDATE '%s' SET allowed_ip = ? WHERE id = ?" % self.Name, - (i.get("AllowedIPs", "N/A"), i['PublicKey'],)) - self.Peers.append(Peer(checkIfExist, self)) - except Exception as e: - if __name__ == '__main__': - print(f"[WGDashboard] {self.Name} Error: {str(e)}") - else: - self.Peers.clear() - checkIfExist = sqlSelect("SELECT * FROM '%s'" % self.Name).fetchall() - for i in checkIfExist: - self.Peers.append(Peer(i, self)) - - def addPeers(self, peers: list) -> tuple[bool, dict]: - result = { - "message": None, - "peers": [] - } - try: - for i in peers: - newPeer = { - "id": i['id'], - "private_key": i['private_key'], - "DNS": i['DNS'], - "endpoint_allowed_ip": i['endpoint_allowed_ip'], - "name": i['name'], - "total_receive": 0, - "total_sent": 0, - "total_data": 0, - "endpoint": "N/A", - "status": "stopped", - "latest_handshake": "N/A", - "allowed_ip": i.get("allowed_ip", "N/A"), - "cumu_receive": 0, - "cumu_sent": 0, - "cumu_data": 0, - "traffic": [], - "mtu": i['mtu'], - "keepalive": i['keepalive'], - "remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], - "preshared_key": i["preshared_key"] - } - sqlUpdate( - """ - INSERT INTO '%s' - 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); - """ % self.Name - , newPeer) - for p in peers: - presharedKeyExist = len(p['preshared_key']) > 0 - rd = random.Random() - uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) - if presharedKeyExist: - with open(uid, "w+") as f: - f.write(p['preshared_key']) - - subprocess.check_output(f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}", - shell=True, stderr=subprocess.STDOUT) - if presharedKeyExist: - os.remove(uid) - subprocess.check_output( - f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) - self.getPeersList() - for p in peers: - p = self.searchPeer(p['id']) - if p[0]: - result['peers'].append(p[1]) - return True, result - except Exception as e: - result['message'] = str(e) - return False, result - - def searchPeer(self, publicKey): - for i in self.Peers: - if i.id == publicKey: - return True, i - return False, None - - def allowAccessPeers(self, listOfPublicKeys): - if not self.getStatus(): - self.toggleConfiguration() - - for i in listOfPublicKeys: - p = sqlSelect("SELECT * FROM '%s_restrict_access' WHERE id = ?" % self.Name, (i,)).fetchone() - if p is not None: - sqlUpdate("INSERT INTO '%s' SELECT * FROM '%s_restrict_access' WHERE id = ?" - % (self.Name, self.Name,), (p['id'],)) - sqlUpdate("DELETE FROM '%s_restrict_access' WHERE id = ?" - % self.Name, (p['id'],)) - - presharedKeyExist = len(p['preshared_key']) > 0 - rd = random.Random() - uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) - if presharedKeyExist: - with open(uid, "w+") as f: - f.write(p['preshared_key']) - - subprocess.check_output(f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}", - shell=True, stderr=subprocess.STDOUT) - if presharedKeyExist: os.remove(uid) - else: - return ResponseObject(False, "Failed to allow access of peer " + i) - if not self.__wgSave(): - return ResponseObject(False, "Failed to save configuration through WireGuard") - - self.getPeers() - return ResponseObject(True, "Allow access successfully") - - def restrictPeers(self, listOfPublicKeys): - numOfRestrictedPeers = 0 - numOfFailedToRestrictPeers = 0 - if not self.getStatus(): - self.toggleConfiguration() - for p in listOfPublicKeys: - found, pf = self.searchPeer(p) - if found: - try: - subprocess.check_output(f"{self.Protocol} set {self.Name} peer {pf.id} remove", - shell=True, stderr=subprocess.STDOUT) - sqlUpdate("INSERT INTO '%s_restrict_access' SELECT * FROM '%s' WHERE id = ?" % - (self.Name, self.Name,), (pf.id,)) - sqlUpdate("UPDATE '%s_restrict_access' SET status = 'stopped' WHERE id = ?" % - (self.Name,), (pf.id,)) - sqlUpdate("DELETE FROM '%s' WHERE id = ?" % self.Name, (pf.id,)) - numOfRestrictedPeers += 1 - except Exception as e: - numOfFailedToRestrictPeers += 1 - - if not self.__wgSave(): - return ResponseObject(False, "Failed to save configuration through WireGuard") - - self.getPeers() - - if numOfRestrictedPeers == len(listOfPublicKeys): - return ResponseObject(True, f"Restricted {numOfRestrictedPeers} peer(s)") - return ResponseObject(False, - f"Restricted {numOfRestrictedPeers} peer(s) successfully. Failed to restrict {numOfFailedToRestrictPeers} peer(s)") - - - def deletePeers(self, listOfPublicKeys): - numOfDeletedPeers = 0 - numOfFailedToDeletePeers = 0 - if not self.getStatus(): - self.toggleConfiguration() - for p in listOfPublicKeys: - found, pf = self.searchPeer(p) - if found: - try: - subprocess.check_output(f"{self.Protocol} set {self.Name} peer {pf.id} remove", - shell=True, stderr=subprocess.STDOUT) - sqlUpdate("DELETE FROM '%s' WHERE id = ?" % self.Name, (pf.id,)) - numOfDeletedPeers += 1 - except Exception as e: - numOfFailedToDeletePeers += 1 - - if not self.__wgSave(): - return ResponseObject(False, "Failed to save configuration through WireGuard") - - self.getPeers() - - if numOfDeletedPeers == 0 and numOfFailedToDeletePeers == 0: - return ResponseObject(False, "No peer(s) to delete found", responseCode=404) - - if numOfDeletedPeers == len(listOfPublicKeys): - return ResponseObject(True, f"Deleted {numOfDeletedPeers} peer(s)") - return ResponseObject(False, - f"Deleted {numOfDeletedPeers} peer(s) successfully. Failed to delete {numOfFailedToDeletePeers} peer(s)") - - def __wgSave(self) -> tuple[bool, str] | tuple[bool, None]: - try: - subprocess.check_output(f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) - return True, None - except subprocess.CalledProcessError as e: - return False, str(e) - - def getPeersLatestHandshake(self): - if not self.getStatus(): - self.toggleConfiguration() - try: - latestHandshake = subprocess.check_output(f"{self.Protocol} show {self.Name} latest-handshakes", - shell=True, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError: - return "stopped" - latestHandshake = latestHandshake.decode("UTF-8").split() - count = 0 - now = datetime.now() - time_delta = timedelta(minutes=2) - for _ in range(int(len(latestHandshake) / 2)): - minus = now - datetime.fromtimestamp(int(latestHandshake[count + 1])) - if minus < time_delta: - status = "running" - else: - status = "stopped" - if int(latestHandshake[count + 1]) > 0: - sqlUpdate("UPDATE '%s' SET latest_handshake = ?, status = ? WHERE id= ?" % self.Name - , (str(minus).split(".", maxsplit=1)[0], status, latestHandshake[count],)) - else: - sqlUpdate("UPDATE '%s' SET latest_handshake = 'No Handshake', status = ? WHERE id= ?" % self.Name - , (status, latestHandshake[count],)) - count += 2 - - def getPeersTransfer(self): - if not self.getStatus(): - self.toggleConfiguration() - try: - data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} transfer", - shell=True, stderr=subprocess.STDOUT) - data_usage = data_usage.decode("UTF-8").split("\n") - data_usage = [p.split("\t") for p in data_usage] - for i in range(len(data_usage)): - if len(data_usage[i]) == 3: - cur_i = sqlSelect( - "SELECT total_receive, total_sent, cumu_receive, cumu_sent, status FROM '%s' WHERE id= ? " - % self.Name, (data_usage[i][0],)).fetchone() - if cur_i is not None: - cur_i = dict(cur_i) - total_sent = cur_i['total_sent'] - total_receive = cur_i['total_receive'] - cur_total_sent = float(data_usage[i][2]) / (1024 ** 3) - cur_total_receive = float(data_usage[i][1]) / (1024 ** 3) - cumulative_receive = cur_i['cumu_receive'] + total_receive - cumulative_sent = cur_i['cumu_sent'] + total_sent - if total_sent <= cur_total_sent and total_receive <= cur_total_receive: - total_sent = cur_total_sent - total_receive = cur_total_receive - else: - sqlUpdate( - "UPDATE '%s' SET cumu_receive = ?, cumu_sent = ?, cumu_data = ? WHERE id = ?" % - self.Name, (cumulative_receive, cumulative_sent, - cumulative_sent + cumulative_receive, - data_usage[i][0],)) - total_sent = 0 - total_receive = 0 - _, p = self.searchPeer(data_usage[i][0]) - if p.total_receive != total_receive or p.total_sent != total_sent: - sqlUpdate( - "UPDATE '%s' SET total_receive = ?, total_sent = ?, total_data = ? WHERE id = ?" - % self.Name, (total_receive, total_sent, - total_receive + total_sent, data_usage[i][0],)) - except Exception as e: - print(f"[WGDashboard] {self.Name} Error: {str(e)} {str(e.__traceback__)}") - - def getPeersEndpoint(self): - if not self.getStatus(): - self.toggleConfiguration() - try: - data_usage = subprocess.check_output(f"{self.Protocol} show {self.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)): - sqlUpdate("UPDATE '%s' SET endpoint = ? WHERE id = ?" % self.Name - , (data_usage[count + 1], data_usage[count],)) - count += 2 - - def toggleConfiguration(self) -> [bool, str]: - self.getStatus() - if self.Status: - try: - check = subprocess.check_output(f"{self.Protocol}-quick down {self.Name}", - shell=True, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as exc: - return False, str(exc.output.strip().decode("utf-8")) - else: - try: - check = subprocess.check_output(f"{self.Protocol}-quick up {self.Name}", shell=True, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as exc: - return False, str(exc.output.strip().decode("utf-8")) - self.__parseConfigurationFile() - self.getStatus() - return True, None - - def getPeersList(self): - self.getPeers() - return self.Peers - - def getRestrictedPeersList(self) -> list: - self.getRestrictedPeers() - return self.RestrictedPeers - - def toJson(self): - self.Status = self.getStatus() - return { - "Status": self.Status, - "Name": self.Name, - "PrivateKey": self.PrivateKey, - "PublicKey": self.PublicKey, - "Address": self.Address, - "ListenPort": self.ListenPort, - "PreUp": self.PreUp, - "PreDown": self.PreDown, - "PostUp": self.PostUp, - "PostDown": self.PostDown, - "SaveConfig": self.SaveConfig, - "DataUsage": { - "Total": sum(list(map(lambda x: x.cumu_data + x.total_data, self.Peers))), - "Sent": sum(list(map(lambda x: x.cumu_sent + x.total_sent, self.Peers))), - "Receive": sum(list(map(lambda x: x.cumu_receive + x.total_receive, self.Peers))) - }, - "ConnectedPeers": len(list(filter(lambda x: x.status == "running", self.Peers))), - "TotalPeers": len(self.Peers), - "Protocol": self.Protocol, - "Table": self.Table, - } - - def backupConfigurationFile(self) -> tuple[bool, dict[str, str]]: - if not os.path.exists(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')): - os.mkdir(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')) - time = datetime.now().strftime("%Y%m%d%H%M%S") - shutil.copy( - self.configPath, - os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f'{self.Name}_{time}.conf') - ) - with open(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f'{self.Name}_{time}.sql'), 'w+') as f: - for l in self.__dumpDatabase(): - f.write(l + "\n") - - return True, { - "filename": f'{self.Name}_{time}.conf', - "backupDate": datetime.now().strftime("%Y%m%d%H%M%S") - } - - def getBackups(self, databaseContent: bool = False) -> list[dict[str: str, str: str, str: str]]: - backups = [] - - directory = os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup') - files = [(file, os.path.getctime(os.path.join(directory, file))) - for file in os.listdir(directory) if os.path.isfile(os.path.join(directory, file))] - files.sort(key=lambda x: x[1], reverse=True) - - for f, ct in files: - if RegexMatch(f"^({self.Name})_(.*)\\.(conf)$", f): - s = re.search(f"^({self.Name})_(.*)\\.(conf)$", f) - date = s.group(2) - d = { - "filename": f, - "backupDate": date, - "content": open(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f), 'r').read() - } - if f.replace(".conf", ".sql") in list(os.listdir(directory)): - d['database'] = True - if databaseContent: - d['databaseContent'] = open(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f.replace(".conf", ".sql")), 'r').read() - backups.append(d) - - return backups - - def restoreBackup(self, backupFileName: str) -> bool: - backups = list(map(lambda x : x['filename'], self.getBackups())) - if backupFileName not in backups: - return False - if self.Status: - self.toggleConfiguration() - target = os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backupFileName) - targetSQL = os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backupFileName.replace(".conf", ".sql")) - if not os.path.exists(target): - return False - targetContent = open(target, 'r').read() - try: - with open(self.configPath, 'w') as f: - f.write(targetContent) - except Exception as e: - return False - self.__parseConfigurationFile() - self.__dropDatabase() - self.__importDatabase(targetSQL) - self.__initPeersList() - return True - - def deleteBackup(self, backupFileName: str) -> bool: - backups = list(map(lambda x : x['filename'], self.getBackups())) - if backupFileName not in backups: - return False - try: - os.remove(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backupFileName)) - except Exception as e: - return False - return True - - def downloadBackup(self, backupFileName: str) -> tuple[bool, str] | tuple[bool, None]: - backup = list(filter(lambda x : x['filename'] == backupFileName, self.getBackups())) - if len(backup) == 0: - return False, None - zip = f'{str(uuid.UUID(int=random.Random().getrandbits(128), version=4))}.zip' - with ZipFile(os.path.join('download', zip), 'w') as zipF: - zipF.write( - os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename']), - os.path.basename(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename'])) - ) - if backup[0]['database']: - zipF.write( - os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename'].replace('.conf', '.sql')), - os.path.basename(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename'].replace('.conf', '.sql'))) - ) - - return True, zip - - def updateConfigurationSettings(self, newData: dict) -> tuple[bool, str]: - if self.Status: - self.toggleConfiguration() - original = [] - dataChanged = False - with open(self.configPath, 'r') as f: - original = [l.rstrip("\n") for l in f.readlines()] - allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"] - if self.Protocol == 'awg': - allowEdit += ["Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4"] - start = original.index("[Interface]") - try: - end = original.index("[Peer]") - except ValueError as e: - end = len(original) - new = ["[Interface]"] - peerFound = False - for line in range(start, end): - split = re.split(r'\s*=\s*', original[line], 1) - if len(split) == 2: - if split[0] not in allowEdit: - new.append(original[line]) - for key in allowEdit: - new.insert(1, f"{key} = {str(newData[key]).strip()}") - new.append("") - for line in range(end, len(original)): - new.append(original[line]) - self.backupConfigurationFile() - with open(self.configPath, 'w') as f: - f.write("\n".join(new)) - - status, msg = self.toggleConfiguration() - if not status: - return False, msg - for i in allowEdit: - if isinstance(getattr(self, i), bool): - setattr(self, i, _strToBool(newData[i])) - else: - setattr(self, i, str(newData[i])) - return True, "" - - def deleteConfiguration(self): - if self.getStatus(): - self.toggleConfiguration() - os.remove(self.configPath) - self.__dropDatabase() - return True - - def renameConfiguration(self, newConfigurationName) -> tuple[bool, str]: - if newConfigurationName in WireguardConfigurations.keys(): - return False, "Configuration name already exist" - try: - if self.getStatus(): - self.toggleConfiguration() - self.createDatabase(newConfigurationName) - sqlUpdate(f'INSERT INTO "{newConfigurationName}" SELECT * FROM "{self.Name}"') - sqlUpdate(f'INSERT INTO "{newConfigurationName}_restrict_access" SELECT * FROM "{self.Name}_restrict_access"') - sqlUpdate(f'INSERT INTO "{newConfigurationName}_deleted" SELECT * FROM "{self.Name}_deleted"') - sqlUpdate(f'INSERT INTO "{newConfigurationName}_transfer" SELECT * FROM "{self.Name}_transfer"') - AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName) - shutil.copy( - self.configPath, - os.path.join(self.__getProtocolPath(), f'{newConfigurationName}.conf') - ) - self.deleteConfiguration() - except Exception as e: - return False, str(e) - return True, None - - def getNumberOfAvailableIP(self): - if len(self.Address) < 0: - return False, None - existedAddress = set() - availableAddress = {} - for p in self.Peers + self.getRestrictedPeersList(): - peerAllowedIP = p.allowed_ip.split(',') - for pip in peerAllowedIP: - ppip = pip.strip().split('/') - if len(ppip) == 2: - try: - check = ipaddress.ip_network(ppip[0]) - existedAddress.add(check) - except Exception as e: - print(f"[WGDashboard] Error: {self.Name} peer {p.id} have invalid ip") - configurationAddresses = self.Address.split(',') - for ca in configurationAddresses: - ca = ca.strip() - caSplit = ca.split('/') - try: - if len(caSplit) == 2: - network = ipaddress.ip_network(ca, False) - existedAddress.add(ipaddress.ip_network(caSplit[0])) - availableAddress[ca] = network.num_addresses - for p in existedAddress: - if p.version == network.version and p.subnet_of(network): - availableAddress[ca] -= 1 - except Exception as e: - print(e) - print(f"[WGDashboard] Error: Failed to parse IP address {ca} from {self.Name}") - return True, availableAddress - - def getAvailableIP(self, threshold = 255): - if len(self.Address) < 0: - return False, None - existedAddress = set() - availableAddress = {} - for p in self.Peers + self.getRestrictedPeersList(): - peerAllowedIP = p.allowed_ip.split(',') - for pip in peerAllowedIP: - ppip = pip.strip().split('/') - if len(ppip) == 2: - try: - check = ipaddress.ip_network(ppip[0]) - existedAddress.add(check.compressed) - except Exception as e: - print(f"[WGDashboard] Error: {self.Name} peer {p.id} have invalid ip") - configurationAddresses = self.Address.split(',') - for ca in configurationAddresses: - ca = ca.strip() - caSplit = ca.split('/') - try: - if len(caSplit) == 2: - network = ipaddress.ip_network(ca, False) - existedAddress.add(ipaddress.ip_network(caSplit[0]).compressed) - if threshold == -1: - availableAddress[ca] = filter(lambda ip : ip not in existedAddress, - map(lambda iph : ipaddress.ip_network(iph).compressed, network.hosts())) - else: - availableAddress[ca] = list(islice(filter(lambda ip : ip not in existedAddress, - map(lambda iph : ipaddress.ip_network(iph).compressed, network.hosts())), threshold)) - except Exception as e: - print(e) - print(f"[WGDashboard] Error: Failed to parse IP address {ca} from {self.Name}") - print("Generated IP") - return True, availableAddress - - def getRealtimeTrafficUsage(self): - stats = psutil.net_io_counters(pernic=True, nowrap=True) - if self.Name in stats.keys(): - stat = stats[self.Name] - recv1 = stat.bytes_recv - sent1 = stat.bytes_sent - time.sleep(1) - stats = psutil.net_io_counters(pernic=True, nowrap=True) - if self.Name in stats.keys(): - stat = stats[self.Name] - recv2 = stat.bytes_recv - sent2 = stat.bytes_sent - net_in = round((recv2 - recv1) / 1024 / 1024, 3) - net_out = round((sent2 - sent1) / 1024 / 1024, 3) - return { - "sent": net_out, - "recv": net_in - } - else: - return { "sent": 0, "recv": 0 } - else: - return { "sent": 0, "recv": 0 } - -""" -AmneziaWG Configuration -""" -class AmneziaWireguardConfiguration(WireguardConfiguration): - def __init__(self, name: str = None, data: dict = None, backup: dict = None, startup: bool = False): - self.Jc = 0 - self.Jmin = 0 - self.Jmax = 0 - self.S1 = 0 - self.S2 = 0 - self.H1 = 1 - self.H2 = 2 - self.H3 = 3 - self.H4 = 4 - - super().__init__(name, data, backup, startup, wg=False) - - def toJson(self): - self.Status = self.getStatus() - return { - "Status": self.Status, - "Name": self.Name, - "PrivateKey": self.PrivateKey, - "PublicKey": self.PublicKey, - "Address": self.Address, - "ListenPort": self.ListenPort, - "PreUp": self.PreUp, - "PreDown": self.PreDown, - "PostUp": self.PostUp, - "PostDown": self.PostDown, - "SaveConfig": self.SaveConfig, - "DataUsage": { - "Total": sum(list(map(lambda x: x.cumu_data + x.total_data, self.Peers))), - "Sent": sum(list(map(lambda x: x.cumu_sent + x.total_sent, self.Peers))), - "Receive": sum(list(map(lambda x: x.cumu_receive + x.total_receive, self.Peers))) - }, - "ConnectedPeers": len(list(filter(lambda x: x.status == "running", self.Peers))), - "TotalPeers": len(self.Peers), - "Protocol": self.Protocol, - "Jc": self.Jc, - "Jmin": self.Jmin, - "Jmax": self.Jmax, - "S1": self.S1, - "S2": self.S2, - "H1": self.H1, - "H2": self.H2, - "H3": self.H3, - "H4": self.H4 - } - - def createDatabase(self, dbName = None): - if dbName is None: - dbName = self.Name - - existingTables = sqlSelect("SELECT name FROM sqlite_master WHERE type='table'").fetchall() - existingTables = [t['name'] for t in existingTables] - if dbName not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s'( - id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL, advanced_security 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) - ) - """ % dbName - ) - - if f'{dbName}_restrict_access' not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s_restrict_access' ( - id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL, advanced_security 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) - ) - """ % dbName - ) - if f'{dbName}_transfer' not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s_transfer' ( - id VARCHAR NOT NULL, total_receive FLOAT NULL, - total_sent FLOAT NULL, total_data FLOAT NULL, - cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, time DATETIME - ) - """ % dbName - ) - if f'{dbName}_deleted' not in existingTables: - sqlUpdate( - """ - CREATE TABLE '%s_deleted' ( - id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL, advanced_security 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) - ) - """ % dbName - ) - - def getPeers(self): - if self.configurationFileChanged(): - self.Peers = [] - with open(self.configPath, 'r') as configFile: - p = [] - pCounter = -1 - content = configFile.read().split('\n') - try: - peerStarts = content.index("[Peer]") - content = content[peerStarts:] - for i in content: - if not RegexMatch("#(.*)", i) and not RegexMatch(";(.*)", i): - if i == "[Peer]": - pCounter += 1 - p.append({}) - p[pCounter]["name"] = "" - else: - if len(i) > 0: - split = re.split(r'\s*=\s*', i, 1) - if len(split) == 2: - p[pCounter][split[0]] = split[1] - - if RegexMatch("#Name# = (.*)", i): - split = re.split(r'\s*=\s*', i, 1) - if len(split) == 2: - p[pCounter]["name"] = split[1] - - for i in p: - if "PublicKey" in i.keys(): - checkIfExist = sqlSelect("SELECT * FROM '%s' WHERE id = ?" % self.Name, - ((i['PublicKey']),)).fetchone() - if checkIfExist is None: - newPeer = { - "id": i['PublicKey'], - "advanced_security": i.get('AdvancedSecurity', 'off'), - "private_key": "", - "DNS": DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1], - "endpoint_allowed_ip": DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[ - 1], - "name": i.get("name"), - "total_receive": 0, - "total_sent": 0, - "total_data": 0, - "endpoint": "N/A", - "status": "stopped", - "latest_handshake": "N/A", - "allowed_ip": i.get("AllowedIPs", "N/A"), - "cumu_receive": 0, - "cumu_sent": 0, - "cumu_data": 0, - "traffic": [], - "mtu": DashboardConfig.GetConfig("Peers", "peer_mtu")[1], - "keepalive": DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1], - "remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], - "preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else "" - } - sqlUpdate( - """ - INSERT INTO '%s' - VALUES (:id, :private_key, :DNS, :advanced_security, :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); - """ % self.Name - , newPeer) - self.Peers.append(AmneziaWGPeer(newPeer, self)) - else: - sqlUpdate("UPDATE '%s' SET allowed_ip = ? WHERE id = ?" % self.Name, - (i.get("AllowedIPs", "N/A"), i['PublicKey'],)) - self.Peers.append(AmneziaWGPeer(checkIfExist, self)) - except Exception as e: - if __name__ == '__main__': - print(f"[WGDashboard] {self.Name} Error: {str(e)}") - else: - self.Peers.clear() - checkIfExist = sqlSelect("SELECT * FROM '%s'" % self.Name).fetchall() - for i in checkIfExist: - self.Peers.append(AmneziaWGPeer(i, self)) - - def addPeers(self, peers: list) -> tuple[bool, dict]: - result = { - "message": None, - "peers": [] - } - try: - for i in peers: - newPeer = { - "id": i['id'], - "private_key": i['private_key'], - "DNS": i['DNS'], - "endpoint_allowed_ip": i['endpoint_allowed_ip'], - "name": i['name'], - "total_receive": 0, - "total_sent": 0, - "total_data": 0, - "endpoint": "N/A", - "status": "stopped", - "latest_handshake": "N/A", - "allowed_ip": i.get("allowed_ip", "N/A"), - "cumu_receive": 0, - "cumu_sent": 0, - "cumu_data": 0, - "traffic": [], - "mtu": i['mtu'], - "keepalive": i['keepalive'], - "remote_endpoint": DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], - "preshared_key": i["preshared_key"], - "advanced_security": i['advanced_security'] - } - sqlUpdate( - """ - INSERT INTO '%s' - VALUES (:id, :private_key, :DNS, :advanced_security, :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); - """ % self.Name - , newPeer) - for p in peers: - presharedKeyExist = len(p['preshared_key']) > 0 - rd = random.Random() - uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) - if presharedKeyExist: - with open(uid, "w+") as f: - f.write(p['preshared_key']) - - subprocess.check_output( - f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}", - shell=True, stderr=subprocess.STDOUT) - if presharedKeyExist: - os.remove(uid) - subprocess.check_output( - f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) - self.getPeersList() - for p in peers: - p = self.searchPeer(p['id']) - if p[0]: - result['peers'].append(p[1]) - return True, result - except Exception as e: - result['message'] = str(e) - return False, result - - def getRestrictedPeers(self): - self.RestrictedPeers = [] - restricted = sqlSelect("SELECT * FROM '%s_restrict_access'" % self.Name).fetchall() - for i in restricted: - self.RestrictedPeers.append(AmneziaWGPeer(i, self)) - -""" -Peer -""" -class Peer: - def __init__(self, tableData, configuration: WireguardConfiguration): - self.configuration = configuration - self.id = tableData["id"] - self.private_key = tableData["private_key"] - self.DNS = tableData["DNS"] - self.endpoint_allowed_ip = tableData["endpoint_allowed_ip"] - self.name = tableData["name"] - self.total_receive = tableData["total_receive"] - self.total_sent = tableData["total_sent"] - self.total_data = tableData["total_data"] - self.endpoint = tableData["endpoint"] - self.status = tableData["status"] - self.latest_handshake = tableData["latest_handshake"] - self.allowed_ip = tableData["allowed_ip"] - self.cumu_receive = tableData["cumu_receive"] - self.cumu_sent = tableData["cumu_sent"] - self.cumu_data = tableData["cumu_data"] - self.mtu = tableData["mtu"] - self.keepalive = tableData["keepalive"] - self.remote_endpoint = tableData["remote_endpoint"] - self.preshared_key = tableData["preshared_key"] - self.jobs: list[PeerJob] = [] - self.ShareLink: list[PeerShareLink] = [] - self.getJobs() - self.getShareLink() - - def toJson(self): - self.getJobs() - self.getShareLink() - return self.__dict__ - - def __repr__(self): - return str(self.toJson()) - - def updatePeer(self, name: str, private_key: str, - preshared_key: str, - dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int, - keepalive: int) -> ResponseObject: - if not self.configuration.getStatus(): - self.configuration.toggleConfiguration() - - existingAllowedIps = [item for row in list( - map(lambda x: [q.strip() for q in x.split(',')], - map(lambda y: y.allowed_ip, - list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row] - - if allowed_ip in existingAllowedIps: - return ResponseObject(False, "Allowed IP already taken by another peer") - if not ValidateIPAddressesWithRange(endpoint_allowed_ip): - return ResponseObject(False, f"Endpoint Allowed IPs format is incorrect") - if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses): - return ResponseObject(False, f"DNS format is incorrect") - if mtu < 0 or mtu > 1460: - return ResponseObject(False, "MTU format is not correct") - if keepalive < 0: - return ResponseObject(False, "Persistent Keepalive format is not correct") - if len(private_key) > 0: - pubKey = GenerateWireguardPublicKey(private_key) - if not pubKey[0] or pubKey[1] != self.id: - return ResponseObject(False, "Private key does not match with the public key") - try: - rd = random.Random() - uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) - pskExist = len(preshared_key) > 0 - - if pskExist: - with open(uid, "w+") as f: - f.write(preshared_key) - newAllowedIPs = allowed_ip.replace(" ", "") - updateAllowedIp = subprocess.check_output( - f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}", - shell=True, stderr=subprocess.STDOUT) - - if pskExist: os.remove(uid) - if len(updateAllowedIp.decode().strip("\n")) != 0: - return ResponseObject(False, - "Update peer failed when updating Allowed IPs") - saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}", - shell=True, stderr=subprocess.STDOUT) - if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'): - return ResponseObject(False, - "Update peer failed when saving the configuration") - sqlUpdate( - '''UPDATE '%s' SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?, - keepalive = ?, preshared_key = ? WHERE id = ?''' % self.configuration.Name, - (name, private_key, dns_addresses, endpoint_allowed_ip, mtu, - keepalive, preshared_key, self.id,) - ) - return ResponseObject() - except subprocess.CalledProcessError as exc: - return ResponseObject(False, exc.output.decode("UTF-8").strip()) - - def downloadPeer(self) -> dict[str, str]: - filename = self.name - if len(filename) == 0: - filename = "UntitledPeer" - filename = "".join(filename.split(' ')) - filename = f"{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, "") - - finalFilename = "" - for i in filename: - if re.match("^[a-zA-Z0-9_=+.-]$", i): - finalFilename += i - - peerConfiguration = f'''[Interface] -PrivateKey = {self.private_key} -Address = {self.allowed_ip} -MTU = {str(self.mtu)} -''' - if len(self.DNS) > 0: - peerConfiguration += f"DNS = {self.DNS}\n" - - peerConfiguration += f''' -[Peer] -PublicKey = {self.configuration.PublicKey} -AllowedIPs = {self.endpoint_allowed_ip} -Endpoint = {DashboardConfig.GetConfig("Peers", "remote_endpoint")[1]}:{self.configuration.ListenPort} -PersistentKeepalive = {str(self.keepalive)} -''' - if len(self.preshared_key) > 0: - peerConfiguration += f"PresharedKey = {self.preshared_key}\n" - return { - "fileName": finalFilename, - "file": peerConfiguration - } - - def getJobs(self): - self.jobs = AllPeerJobs.searchJob(self.configuration.Name, self.id) - - def getShareLink(self): - self.ShareLink = AllPeerShareLinks.getLink(self.configuration.Name, self.id) - - def resetDataUsage(self, type): - try: - if type == "total": - sqlUpdate("UPDATE '%s' SET total_data = 0, cumu_data = 0, total_receive = 0, cumu_receive = 0, total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, )) - self.total_data = 0 - self.total_receive = 0 - self.total_sent = 0 - self.cumu_data = 0 - self.cumu_sent = 0 - self.cumu_receive = 0 - elif type == "receive": - sqlUpdate("UPDATE '%s' SET total_receive = 0, cumu_receive = 0 WHERE id = ?" % self.configuration.Name, (self.id, )) - self.cumu_receive = 0 - self.total_receive = 0 - elif type == "sent": - sqlUpdate("UPDATE '%s' SET total_sent = 0, cumu_sent = 0 WHERE id = ?" % self.configuration.Name, (self.id, )) - self.cumu_sent = 0 - self.total_sent = 0 - else: - return False - except Exception as e: - print(e) - return False - - return True - -class AmneziaWGPeer(Peer): - def __init__(self, tableData, configuration: AmneziaWireguardConfiguration): - self.advanced_security = tableData["advanced_security"] - super().__init__(tableData, configuration) - - def downloadPeer(self) -> dict[str, str]: - filename = self.name - if len(filename) == 0: - filename = "UntitledPeer" - filename = "".join(filename.split(' ')) - filename = f"{filename}_{self.configuration.Name}" - 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, "") - - finalFilename = "" - for i in filename: - if re.match("^[a-zA-Z0-9_=+.-]$", i): - finalFilename += i - - peerConfiguration = f'''[Interface] -PrivateKey = {self.private_key} -Address = {self.allowed_ip} -MTU = {str(self.mtu)} -Jc = {self.configuration.Jc} -Jmin = {self.configuration.Jmin} -Jmax = {self.configuration.Jmax} -S1 = {self.configuration.S1} -S2 = {self.configuration.S2} -H1 = {self.configuration.H1} -H2 = {self.configuration.H2} -H3 = {self.configuration.H3} -H4 = {self.configuration.H4} -''' - if len(self.DNS) > 0: - peerConfiguration += f"DNS = {self.DNS}\n" - peerConfiguration += f''' -[Peer] -PublicKey = {self.configuration.PublicKey} -AllowedIPs = {self.endpoint_allowed_ip} -Endpoint = {DashboardConfig.GetConfig("Peers", "remote_endpoint")[1]}:{self.configuration.ListenPort} -PersistentKeepalive = {str(self.keepalive)} -''' - if len(self.preshared_key) > 0: - peerConfiguration += f"PresharedKey = {self.preshared_key}\n" - return { - "fileName": finalFilename, - "file": peerConfiguration - } - - def updatePeer(self, name: str, private_key: str, - preshared_key: str, - dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int, - keepalive: int, advanced_security: str) -> ResponseObject: - if not self.configuration.getStatus(): - self.configuration.toggleConfiguration() - - existingAllowedIps = [item for row in list( - map(lambda x: [q.strip() for q in x.split(',')], - map(lambda y: y.allowed_ip, - list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row] - - if allowed_ip in existingAllowedIps: - return ResponseObject(False, "Allowed IP already taken by another peer") - if not ValidateIPAddressesWithRange(endpoint_allowed_ip): - return ResponseObject(False, f"Endpoint Allowed IPs format is incorrect") - if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses): - return ResponseObject(False, f"DNS format is incorrect") - if mtu < 0 or mtu > 1460: - return ResponseObject(False, "MTU format is not correct") - if keepalive < 0: - return ResponseObject(False, "Persistent Keepalive format is not correct") - if advanced_security != "on" and advanced_security != "off": - return ResponseObject(False, "Advanced Security can only be on or off") - if len(private_key) > 0: - pubKey = GenerateWireguardPublicKey(private_key) - if not pubKey[0] or pubKey[1] != self.id: - return ResponseObject(False, "Private key does not match with the public key") - try: - rd = random.Random() - uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) - pskExist = len(preshared_key) > 0 - - if pskExist: - with open(uid, "w+") as f: - f.write(preshared_key) - newAllowedIPs = allowed_ip.replace(" ", "") - updateAllowedIp = subprocess.check_output( - f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}", - shell=True, stderr=subprocess.STDOUT) - - if pskExist: os.remove(uid) - - if len(updateAllowedIp.decode().strip("\n")) != 0: - return ResponseObject(False, - "Update peer failed when updating Allowed IPs") - saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}", - shell=True, stderr=subprocess.STDOUT) - if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'): - return ResponseObject(False, - "Update peer failed when saving the configuration") - sqlUpdate( - '''UPDATE '%s' SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?, - keepalive = ?, preshared_key = ?, advanced_security = ? WHERE id = ?''' % self.configuration.Name, - (name, private_key, dns_addresses, endpoint_allowed_ip, mtu, - keepalive, preshared_key, advanced_security, self.id,) - ) - return ResponseObject() - except subprocess.CalledProcessError as exc: - return ResponseObject(False, exc.output.decode("UTF-8").strip()) """ Database Connection Functions @@ -1711,9 +281,13 @@ def API_addWireguardConfiguration(): os.path.join(path[protocol], 'WGDashboard_Backup', data["Backup"]), os.path.join(path[protocol], f'{data["ConfigurationName"]}.conf') ) - WireguardConfigurations[data['ConfigurationName']] = WireguardConfiguration(data=data, name=data['ConfigurationName']) if protocol == 'wg' else AmneziaWireguardConfiguration(data=data, name=data['ConfigurationName']) + WireguardConfigurations[data['ConfigurationName']] = ( + WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else ( + AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) else: - WireguardConfigurations[data['ConfigurationName']] = WireguardConfiguration(data=data) if data.get('Protocol') == 'wg' else AmneziaWireguardConfiguration(data=data) + WireguardConfigurations[data['ConfigurationName']] = ( + WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data)) if data.get('Protocol') == 'wg' else ( + AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data)) return ResponseObject() @app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration') @@ -1772,9 +346,11 @@ def API_deleteWireguardConfiguration(): data = request.get_json() if "ConfigurationName" not in data.keys() or data.get("ConfigurationName") is None or data.get("ConfigurationName") not in WireguardConfigurations.keys(): return ResponseObject(False, "Please provide the configuration name you want to delete", status_code=404) - status = WireguardConfigurations[data.get("ConfigurationName")].deleteConfiguration() - if status: - WireguardConfigurations.pop(data.get("ConfigurationName")) + rp = WireguardConfigurations.pop(data.get("ConfigurationName")) + + status = rp.deleteConfiguration() + if not status: + WireguardConfigurations[data.get("ConfigurationName")] = rp return ResponseObject(status) @app.post(f'{APP_PREFIX}/api/renameWireguardConfiguration') @@ -1785,11 +361,17 @@ def API_renameWireguardConfiguration(): if (k not in data.keys() or data.get(k) is None or len(data.get(k)) == 0 or (k == "ConfigurationName" and data.get(k) not in WireguardConfigurations.keys())): return ResponseObject(False, "Please provide the configuration name you want to rename", status_code=404) - - status, message = WireguardConfigurations[data.get("ConfigurationName")].renameConfiguration(data.get("NewConfigurationName")) + + if data.get("NewConfigurationName") in WireguardConfigurations.keys(): + return ResponseObject(False, "Configuration name already exist", status_code=400) + + rc = WireguardConfigurations.pop(data.get("ConfigurationName")) + + status, message = rc.renameConfiguration(data.get("NewConfigurationName")) if status: - WireguardConfigurations.pop(data.get("ConfigurationName")) - WireguardConfigurations[data.get("NewConfigurationName")] = WireguardConfiguration(data.get("NewConfigurationName")) + WireguardConfigurations[data.get("NewConfigurationName")] = (WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data.get("NewConfigurationName")) if rc.Protocol == 'wg' else AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data.get("NewConfigurationName"))) + else: + WireguardConfigurations[data.get("ConfigurationName")] = rc return ResponseObject(status, message) @app.get(f'{APP_PREFIX}/api/getWireguardConfigurationRealtimeTraffic') @@ -1966,11 +548,12 @@ def API_updatePeerSettings(configName): foundPeer, peer = wireguardConfig.searchPeer(id) if foundPeer: if wireguardConfig.Protocol == 'wg': - return peer.updatePeer(name, private_key, preshared_key, dns_addresses, + status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses, allowed_ip, endpoint_allowed_ip, mtu, keepalive) - - return peer.updatePeer(name, private_key, preshared_key, dns_addresses, + return ResponseObject(status, msg) + status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses, allowed_ip, endpoint_allowed_ip, mtu, keepalive, "off") + return ResponseObject(status, msg) return ResponseObject(False, "Peer does not exist") @@ -2001,7 +584,8 @@ def API_deletePeers(configName: str) -> ResponseObject: if len(peers) == 0: return ResponseObject(False, "Please specify one or more peers", status_code=400) configuration = WireguardConfigurations.get(configName) - return configuration.deletePeers(peers) + status, msg = configuration.deletePeers(peers) + return ResponseObject(status, msg) return ResponseObject(False, "Configuration does not exist", status_code=404) @@ -2013,7 +597,8 @@ def API_restrictPeers(configName: str) -> ResponseObject: if len(peers) == 0: return ResponseObject(False, "Please specify one or more peers") configuration = WireguardConfigurations.get(configName) - return configuration.restrictPeers(peers) + status, msg = configuration.restrictPeers(peers) + return ResponseObject(status, msg) return ResponseObject(False, "Configuration does not exist", status_code=404) @app.post(f'{APP_PREFIX}/api/sharePeer/create') @@ -2079,7 +664,8 @@ def API_allowAccessPeers(configName: str) -> ResponseObject: if len(peers) == 0: return ResponseObject(False, "Please specify one or more peers") configuration = WireguardConfigurations.get(configName) - return configuration.allowAccessPeers(peers) + status, msg = configuration.allowAccessPeers(peers) + return ResponseObject(status, msg) return ResponseObject(False, "Configuration does not exist") @app.post(f'{APP_PREFIX}/api/addPeers/') @@ -2686,9 +1272,9 @@ def InitWireguardConfigurationsList(startup: bool = False): try: if i in WireguardConfigurations.keys(): if WireguardConfigurations[i].configurationFileChanged(): - WireguardConfigurations[i] = WireguardConfiguration(i) + WireguardConfigurations[i] = WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, i) else: - WireguardConfigurations[i] = WireguardConfiguration(i, startup=startup) + WireguardConfigurations[i] = WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, i, startup=startup) except WireguardConfiguration.InvalidConfigurationFileException as e: print(f"{i} have an invalid configuration file.") @@ -2701,10 +1287,10 @@ def InitWireguardConfigurationsList(startup: bool = False): try: if i in WireguardConfigurations.keys(): if WireguardConfigurations[i].configurationFileChanged(): - WireguardConfigurations[i] = AmneziaWireguardConfiguration(i) + WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, i) else: - WireguardConfigurations[i] = AmneziaWireguardConfiguration(i, startup=startup) - except WireguardConfigurations.InvalidConfigurationFileException as e: + WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, i, startup=startup) + except WireguardConfiguration.InvalidConfigurationFileException as e: print(f"{i} have an invalid configuration file.") @@ -2713,7 +1299,6 @@ _, app_port = DashboardConfig.GetConfig("Server", "app_port") _, WG_CONF_PATH = DashboardConfig.GetConfig("Server", "wg_conf_path") WireguardConfigurations: dict[str, WireguardConfiguration] = {} -AmneziaWireguardConfigurations: dict[str, AmneziaWireguardConfiguration] = {} AllPeerShareLinks: PeerShareLinks = PeerShareLinks(DashboardConfig) AllPeerJobs: PeerJobs = PeerJobs(DashboardConfig, WireguardConfigurations) @@ -2729,4 +1314,4 @@ def startThreads(): if __name__ == "__main__": startThreads() - app.run(host=app_ip, debug=False, port=app_port) + app.run(host=app_ip, debug=False, port=app_port) \ No newline at end of file diff --git a/src/modules/AmneziaWGPeer.py b/src/modules/AmneziaWGPeer.py new file mode 100644 index 0000000..96da818 --- /dev/null +++ b/src/modules/AmneziaWGPeer.py @@ -0,0 +1,137 @@ +import os +import random +import re +import subprocess +import uuid + +from .Peer import Peer +from .Utilities import ValidateIPAddressesWithRange, ValidateDNSAddress, GenerateWireguardPublicKey + + +class AmneziaWGPeer(Peer): + def __init__(self, tableData, configuration): + self.advanced_security = tableData["advanced_security"] + super().__init__(tableData, configuration) + + def downloadPeer(self) -> dict[str, str]: + filename = self.name + if len(filename) == 0: + filename = "UntitledPeer" + filename = "".join(filename.split(' ')) + filename = f"{filename}_{self.configuration.Name}" + 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, "") + + finalFilename = "" + for i in filename: + if re.match("^[a-zA-Z0-9_=+.-]$", i): + finalFilename += i + + peerConfiguration = f'''[Interface] +PrivateKey = {self.private_key} +Address = {self.allowed_ip} +MTU = {str(self.mtu)} +Jc = {self.configuration.Jc} +Jmin = {self.configuration.Jmin} +Jmax = {self.configuration.Jmax} +S1 = {self.configuration.S1} +S2 = {self.configuration.S2} +H1 = {self.configuration.H1} +H2 = {self.configuration.H2} +H3 = {self.configuration.H3} +H4 = {self.configuration.H4} +''' + if len(self.DNS) > 0: + peerConfiguration += f"DNS = {self.DNS}\n" + peerConfiguration += f''' +[Peer] +PublicKey = {self.configuration.PublicKey} +AllowedIPs = {self.endpoint_allowed_ip} +Endpoint = {self.configuration.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1]}:{self.configuration.ListenPort} +PersistentKeepalive = {str(self.keepalive)} +''' + if len(self.preshared_key) > 0: + peerConfiguration += f"PresharedKey = {self.preshared_key}\n" + return { + "fileName": finalFilename, + "file": peerConfiguration + } + + def updatePeer(self, name: str, private_key: str, + preshared_key: str, + dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int, + keepalive: int, advanced_security: str) -> tuple[bool, str] or tuple[bool, None]: + if not self.configuration.getStatus(): + self.configuration.toggleConfiguration() + + existingAllowedIps = [item for row in list( + map(lambda x: [q.strip() for q in x.split(',')], + map(lambda y: y.allowed_ip, + list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row] + + if allowed_ip in existingAllowedIps: + return False, "Allowed IP already taken by another peer" + if not ValidateIPAddressesWithRange(endpoint_allowed_ip): + return False, f"Endpoint Allowed IPs format is incorrect" + if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses): + return False, f"DNS format is incorrect" + if mtu < 0 or mtu > 1460: + return False, "MTU format is not correct" + if keepalive < 0: + return False, "Persistent Keepalive format is not correct" + if advanced_security != "on" and advanced_security != "off": + return False, "Advanced Security can only be on or off" + if len(private_key) > 0: + pubKey = GenerateWireguardPublicKey(private_key) + if not pubKey[0] or pubKey[1] != self.id: + return False, "Private key does not match with the public key" + try: + rd = random.Random() + uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) + pskExist = len(preshared_key) > 0 + + if pskExist: + with open(uid, "w+") as f: + f.write(preshared_key) + newAllowedIPs = allowed_ip.replace(" ", "") + updateAllowedIp = subprocess.check_output( + f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}", + shell=True, stderr=subprocess.STDOUT) + + if pskExist: os.remove(uid) + + if len(updateAllowedIp.decode().strip("\n")) != 0: + return False, "Update peer failed when updating Allowed IPs" + saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}", + shell=True, stderr=subprocess.STDOUT) + if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'): + return False, "Update peer failed when saving the configuration" + # sqlUpdate( + # '''UPDATE '%s' SET name = ?, private_key = ?, DNS = ?, endpoint_allowed_ip = ?, mtu = ?, + # keepalive = ?, preshared_key = ?, advanced_security = ? WHERE id = ?''' % self.configuration.Name, + # (name, private_key, dns_addresses, endpoint_allowed_ip, mtu, + # keepalive, preshared_key, advanced_security, self.id,) + # ) + + with self.configuration.engine.begin() as conn: + conn.execute( + self.configuration.peersTable.update().values({ + "name": name, + "private_key": private_key, + "DNS": dns_addresses, + "endpoint_allowed_ip": endpoint_allowed_ip, + "mtu": mtu, + "keepalive": keepalive, + "preshared_key": preshared_key, + "advanced_security": advanced_security + }).where( + self.configuration.peersTable.c.id == self.id + ) + ) + + return True, None + except subprocess.CalledProcessError as exc: + return False, exc.output.decode("UTF-8").strip() \ No newline at end of file diff --git a/src/modules/AmneziaWireguardConfiguration.py b/src/modules/AmneziaWireguardConfiguration.py new file mode 100644 index 0000000..c3f2ae5 --- /dev/null +++ b/src/modules/AmneziaWireguardConfiguration.py @@ -0,0 +1,374 @@ +""" +AmneziaWG Configuration +""" +import re + +import sqlalchemy + +from .PeerJobs import PeerJobs +from .AmneziaWGPeer import AmneziaWGPeer +from .PeerShareLinks import PeerShareLinks +from .Utilities import RegexMatch +from .WireguardConfiguration import WireguardConfiguration + + +class AmneziaWireguardConfiguration(WireguardConfiguration): + def __init__(self, DashboardConfig, + AllPeerJobs: PeerJobs, + AllPeerShareLinks: PeerShareLinks, + name: str = None, data: dict = None, backup: dict = None, startup: bool = False): + self.Jc = 0 + self.Jmin = 0 + self.Jmax = 0 + self.S1 = 0 + self.S2 = 0 + self.H1 = 1 + self.H2 = 2 + self.H3 = 3 + self.H4 = 4 + + super().__init__(DashboardConfig, AllPeerJobs, AllPeerShareLinks, name, data, backup, startup, wg=False) + + def toJson(self): + self.Status = self.getStatus() + return { + "Status": self.Status, + "Name": self.Name, + "PrivateKey": self.PrivateKey, + "PublicKey": self.PublicKey, + "Address": self.Address, + "ListenPort": self.ListenPort, + "PreUp": self.PreUp, + "PreDown": self.PreDown, + "PostUp": self.PostUp, + "PostDown": self.PostDown, + "SaveConfig": self.SaveConfig, + "DataUsage": { + "Total": sum(list(map(lambda x: x.cumu_data + x.total_data, self.Peers))), + "Sent": sum(list(map(lambda x: x.cumu_sent + x.total_sent, self.Peers))), + "Receive": sum(list(map(lambda x: x.cumu_receive + x.total_receive, self.Peers))) + }, + "ConnectedPeers": len(list(filter(lambda x: x.status == "running", self.Peers))), + "TotalPeers": len(self.Peers), + "Protocol": self.Protocol, + "Jc": self.Jc, + "Jmin": self.Jmin, + "Jmax": self.Jmax, + "S1": self.S1, + "S2": self.S2, + "H1": self.H1, + "H2": self.H2, + "H3": self.H3, + "H4": self.H4 + } + + def createDatabase(self, dbName = None): + if dbName is None: + dbName = self.Name + + + self.peersTable = sqlalchemy.Table( + dbName, self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False, primary_key=True), + sqlalchemy.Column('private_key', sqlalchemy.String), + sqlalchemy.Column('DNS', sqlalchemy.String), + sqlalchemy.Column('advanced_security', sqlalchemy.String), + sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.String), + sqlalchemy.Column('name', sqlalchemy.String), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('endpoint', sqlalchemy.String), + sqlalchemy.Column('status', sqlalchemy.String), + sqlalchemy.Column('latest_handshake', sqlalchemy.String), + sqlalchemy.Column('allowed_ip', sqlalchemy.String), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('mtu', sqlalchemy.Integer), + sqlalchemy.Column('keepalive', sqlalchemy.Integer), + sqlalchemy.Column('remote_endpoint', sqlalchemy.String), + sqlalchemy.Column('preshared_key', sqlalchemy.String), + extend_existing=True + ) + self.peersRestrictedTable = sqlalchemy.Table( + f'{dbName}_restrict_access', self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False, primary_key=True), + sqlalchemy.Column('private_key', sqlalchemy.String), + sqlalchemy.Column('DNS', sqlalchemy.String), + sqlalchemy.Column('advanced_security', sqlalchemy.String), + sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.String), + sqlalchemy.Column('name', sqlalchemy.String), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('endpoint', sqlalchemy.String), + sqlalchemy.Column('status', sqlalchemy.String), + sqlalchemy.Column('latest_handshake', sqlalchemy.String), + sqlalchemy.Column('allowed_ip', sqlalchemy.String), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('mtu', sqlalchemy.Integer), + sqlalchemy.Column('keepalive', sqlalchemy.Integer), + sqlalchemy.Column('remote_endpoint', sqlalchemy.String), + sqlalchemy.Column('preshared_key', sqlalchemy.String), + extend_existing=True + ) + self.peersTransferTable = sqlalchemy.Table( + f'{dbName}_transfer', self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('time', (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP), + server_default=sqlalchemy.func.now()), + extend_existing=True + ) + self.peersDeletedTable = sqlalchemy.Table( + f'{dbName}_deleted', self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False, primary_key=True), + sqlalchemy.Column('private_key', sqlalchemy.String), + sqlalchemy.Column('advanced_security', sqlalchemy.String), + sqlalchemy.Column('DNS', sqlalchemy.String), + sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.String), + sqlalchemy.Column('name', sqlalchemy.String), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('endpoint', sqlalchemy.String), + sqlalchemy.Column('status', sqlalchemy.String), + sqlalchemy.Column('latest_handshake', sqlalchemy.String), + sqlalchemy.Column('allowed_ip', sqlalchemy.String), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('mtu', sqlalchemy.Integer), + sqlalchemy.Column('keepalive', sqlalchemy.Integer), + sqlalchemy.Column('remote_endpoint', sqlalchemy.String), + sqlalchemy.Column('preshared_key', sqlalchemy.String), + extend_existing=True + ) + + self.metadata.create_all(self.engine) + + # existingTables = sqlSelect("SELECT name FROM sqlite_master WHERE type='table'").fetchall() + # existingTables = [t['name'] for t in existingTables] + # if dbName not in existingTables: + # sqlUpdate( + # """ + # CREATE TABLE '%s'( + # id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL, advanced_security 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) + # ) + # """ % dbName + # ) + # + # if f'{dbName}_restrict_access' not in existingTables: + # sqlUpdate( + # """ + # CREATE TABLE '%s_restrict_access' ( + # id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL, advanced_security 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) + # ) + # """ % dbName + # ) + # if f'{dbName}_transfer' not in existingTables: + # sqlUpdate( + # """ + # CREATE TABLE '%s_transfer' ( + # id VARCHAR NOT NULL, total_receive FLOAT NULL, + # total_sent FLOAT NULL, total_data FLOAT NULL, + # cumu_receive FLOAT NULL, cumu_sent FLOAT NULL, cumu_data FLOAT NULL, time DATETIME + # ) + # """ % dbName + # ) + # if f'{dbName}_deleted' not in existingTables: + # sqlUpdate( + # """ + # CREATE TABLE '%s_deleted' ( + # id VARCHAR NOT NULL, private_key VARCHAR NULL, DNS VARCHAR NULL, advanced_security 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) + # ) + # """ % dbName + # ) + + def getPeers(self): + self.Peers.clear() + if self.configurationFileChanged(): + with open(self.configPath, 'r') as configFile: + p = [] + pCounter = -1 + content = configFile.read().split('\n') + try: + peerStarts = content.index("[Peer]") + content = content[peerStarts:] + for i in content: + if not RegexMatch("#(.*)", i) and not RegexMatch(";(.*)", i): + if i == "[Peer]": + pCounter += 1 + p.append({}) + p[pCounter]["name"] = "" + else: + if len(i) > 0: + split = re.split(r'\s*=\s*', i, 1) + if len(split) == 2: + p[pCounter][split[0]] = split[1] + + if RegexMatch("#Name# = (.*)", i): + split = re.split(r'\s*=\s*', i, 1) + if len(split) == 2: + p[pCounter]["name"] = split[1] + with self.engine.begin() as conn: + for i in p: + if "PublicKey" in i.keys(): + tempPeer = conn.execute(self.peersTable.select().where( + self.peersTable.columns.id == i['PublicKey'] + )).mappings().fetchone() + if tempPeer is None: + tempPeer = { + "id": i['PublicKey'], + "advanced_security": i.get('AdvancedSecurity', 'off'), + "private_key": "", + "DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1], + "endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[ + 1], + "name": i.get("name"), + "total_receive": 0, + "total_sent": 0, + "total_data": 0, + "endpoint": "N/A", + "status": "stopped", + "latest_handshake": "N/A", + "allowed_ip": i.get("AllowedIPs", "N/A"), + "cumu_receive": 0, + "cumu_sent": 0, + "cumu_data": 0, + "mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1], + "keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1], + "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], + "preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else "" + } + # sqlUpdate( + # """ + # INSERT INTO '%s' + # VALUES (:id, :private_key, :DNS, :advanced_security, :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); + # """ % self.Name + # , newPeer) + conn.execute( + self.peersTable.insert().values(tempPeer) + ) + else: + # sqlUpdate("UPDATE '%s' SET allowed_ip = ? WHERE id = ?" % self.Name, + # (i.get("AllowedIPs", "N/A"), i['PublicKey'],)) + conn.execute( + self.peersTable.update().values({ + "allowed_ip": i.get("AllowedIPs", "N/A") + }).where( + self.peersTable.columns.id == i['PublicKey'] + ) + ) + self.Peers.append(AmneziaWGPeer(tempPeer, self)) + except Exception as e: + if __name__ == '__main__': + print(f"[WGDashboard] {self.Name} getPeers() Error: {str(e)}") + else: + # checkIfExist = sqlSelect("SELECT * FROM '%s'" % self.Name).fetchall() + with self.engine.connect() as conn: + existingPeers = conn.execute(self.peersTable.select()).mappings().fetchall() + for i in existingPeers: + self.Peers.append(AmneziaWGPeer(i, self)) + + def addPeers(self, peers: list) -> tuple[bool, dict]: + result = { + "message": None, + "peers": [] + } + try: + with self.engine.begin() as conn: + for i in peers: + newPeer = { + "id": i['id'], + "private_key": i['private_key'], + "DNS": i['DNS'], + "endpoint_allowed_ip": i['endpoint_allowed_ip'], + "name": i['name'], + "total_receive": 0, + "total_sent": 0, + "total_data": 0, + "endpoint": "N/A", + "status": "stopped", + "latest_handshake": "N/A", + "allowed_ip": i.get("allowed_ip", "N/A"), + "cumu_receive": 0, + "cumu_sent": 0, + "cumu_data": 0, + "mtu": i['mtu'], + "keepalive": i['keepalive'], + "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], + "preshared_key": i["preshared_key"], + "advanced_security": i['advanced_security'] + } + conn.execute( + self.peersTable.insert().values(newPeer) + ) + # sqlUpdate( + # """ + # INSERT INTO '%s' + # VALUES (:id, :private_key, :DNS, :advanced_security, :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); + # """ % self.Name + # , newPeer) + for p in peers: + presharedKeyExist = len(p['preshared_key']) > 0 + rd = random.Random() + uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) + if presharedKeyExist: + with open(uid, "w+") as f: + f.write(p['preshared_key']) + + subprocess.check_output( + f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}", + shell=True, stderr=subprocess.STDOUT) + if presharedKeyExist: + os.remove(uid) + subprocess.check_output( + f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) + self.getPeersList() + for p in peers: + p = self.searchPeer(p['id']) + if p[0]: + result['peers'].append(p[1]) + return True, result + except Exception as e: + result['message'] = str(e) + return False, result + + def getRestrictedPeers(self): + self.RestrictedPeers = [] + # restricted = sqlSelect("SELECT * FROM '%s_restrict_access'" % self.Name).fetchall() + with self.engine.connect() as conn: + restricted = conn.execute(self.peersRestrictedTable.select()).mappings().fetchall() + for i in restricted: + self.RestrictedPeers.append(AmneziaWGPeer(i, self)) \ No newline at end of file diff --git a/src/modules/DashboardLogger.py b/src/modules/DashboardLogger.py index 119ff2d..b8e9f1d 100644 --- a/src/modules/DashboardLogger.py +++ b/src/modules/DashboardLogger.py @@ -1,16 +1,13 @@ """ Dashboard Logger Class """ -import os, uuid +import uuid import sqlalchemy as db -from datetime import datetime -from sqlalchemy_utils import database_exists, create_database + class DashboardLogger: def __init__(self, DashboardConfig): self.engine = db.create_engine(DashboardConfig.getConnectionString("wgdashboard_log")) - if not database_exists(self.engine.url): - create_database(self.engine.url) self.metadata = db.MetaData() self.dashboardLoggerTable = db.Table('DashboardLog', self.metadata, db.Column('LogID', db.VARCHAR, nullable=False, primary_key=True), diff --git a/src/modules/Peer.py b/src/modules/Peer.py new file mode 100644 index 0000000..141b89c --- /dev/null +++ b/src/modules/Peer.py @@ -0,0 +1,203 @@ +""" +Peer +""" +import os, subprocess, uuid, random, re +from .PeerJob import PeerJob +from .PeerShareLink import PeerShareLink +from .Utilities import GenerateWireguardPublicKey, ValidateIPAddressesWithRange, ValidateDNSAddress + + +class Peer: + def __init__(self, tableData, configuration): + self.configuration = configuration + self.id = tableData["id"] + self.private_key = tableData["private_key"] + self.DNS = tableData["DNS"] + self.endpoint_allowed_ip = tableData["endpoint_allowed_ip"] + self.name = tableData["name"] + self.total_receive = tableData["total_receive"] + self.total_sent = tableData["total_sent"] + self.total_data = tableData["total_data"] + self.endpoint = tableData["endpoint"] + self.status = tableData["status"] + self.latest_handshake = tableData["latest_handshake"] + self.allowed_ip = tableData["allowed_ip"] + self.cumu_receive = tableData["cumu_receive"] + self.cumu_sent = tableData["cumu_sent"] + self.cumu_data = tableData["cumu_data"] + self.mtu = tableData["mtu"] + self.keepalive = tableData["keepalive"] + self.remote_endpoint = tableData["remote_endpoint"] + self.preshared_key = tableData["preshared_key"] + self.jobs: list[PeerJob] = [] + self.ShareLink: list[PeerShareLink] = [] + self.getJobs() + self.getShareLink() + + def toJson(self): + self.getJobs() + self.getShareLink() + return self.__dict__ + + def __repr__(self): + return str(self.toJson()) + + def updatePeer(self, name: str, private_key: str, + preshared_key: str, + dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int, + keepalive: int) -> tuple[bool, str] or tuple[bool, None]: + if not self.configuration.getStatus(): + self.configuration.toggleConfiguration() + + existingAllowedIps = [item for row in list( + map(lambda x: [q.strip() for q in x.split(',')], + map(lambda y: y.allowed_ip, + list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row] + + if allowed_ip in existingAllowedIps: + return False, "Allowed IP already taken by another peer" + if not ValidateIPAddressesWithRange(endpoint_allowed_ip): + return False, f"Endpoint Allowed IPs format is incorrect" + if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses): + return False, f"DNS format is incorrect" + if mtu < 0 or mtu > 1460: + return False, "MTU format is not correct" + if keepalive < 0: + return False, "Persistent Keepalive format is not correct" + if len(private_key) > 0: + pubKey = GenerateWireguardPublicKey(private_key) + if not pubKey[0] or pubKey[1] != self.id: + return False, "Private key does not match with the public key" + try: + rd = random.Random() + uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) + pskExist = len(preshared_key) > 0 + + if pskExist: + with open(uid, "w+") as f: + f.write(preshared_key) + newAllowedIPs = allowed_ip.replace(" ", "") + updateAllowedIp = subprocess.check_output( + f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}", + shell=True, stderr=subprocess.STDOUT) + + if pskExist: os.remove(uid) + if len(updateAllowedIp.decode().strip("\n")) != 0: + return False, "Update peer failed when updating Allowed IPs" + saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}", + shell=True, stderr=subprocess.STDOUT) + if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'): + return False, "Update peer failed when saving the configuration" + with self.configuration.engine.begin() as conn: + conn.execute( + self.configuration.peersTable.update().values({ + "name": name, + "private_key": private_key, + "DNS": dns_addresses, + "endpoint_allowed_ip": endpoint_allowed_ip, + "mtu": mtu, + "keepalive": keepalive, + "preshared_key": preshared_key + }).where( + self.configuration.peersTable.c.id == self.id + ) + ) + return True, None + except subprocess.CalledProcessError as exc: + return False, exc.output.decode("UTF-8").strip() + + def downloadPeer(self) -> dict[str, str]: + filename = self.name + if len(filename) == 0: + filename = "UntitledPeer" + filename = "".join(filename.split(' ')) + filename = f"{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, "") + + finalFilename = "" + for i in filename: + if re.match("^[a-zA-Z0-9_=+.-]$", i): + finalFilename += i + + peerConfiguration = f'''[Interface] +PrivateKey = {self.private_key} +Address = {self.allowed_ip} +MTU = {str(self.mtu)} +''' + if len(self.DNS) > 0: + peerConfiguration += f"DNS = {self.DNS}\n" + + peerConfiguration += f''' +[Peer] +PublicKey = {self.configuration.PublicKey} +AllowedIPs = {self.endpoint_allowed_ip} +Endpoint = {self.configuration.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1]}:{self.configuration.ListenPort} +PersistentKeepalive = {str(self.keepalive)} +''' + if len(self.preshared_key) > 0: + peerConfiguration += f"PresharedKey = {self.preshared_key}\n" + return { + "fileName": finalFilename, + "file": peerConfiguration + } + + def getJobs(self): + self.jobs = self.configuration.AllPeerJobs.searchJob(self.configuration.Name, self.id) + + def getShareLink(self): + self.ShareLink = self.configuration.AllPeerShareLinks.getLink(self.configuration.Name, self.id) + + def resetDataUsage(self, type): + try: + with self.configuration.engine.begin() as conn: + if type == "total": + conn.execute( + self.configuration.peersTable.update().values({ + "total_data": 0, + "cumu_data": 0, + "total_receive": 0, + "cumu_receive": 0, + "total_sent": 0, + "cumu_sent": 0 + }).where( + self.configuration.peersTable.c.id == self.id + ) + ) + self.total_data = 0 + self.total_receive = 0 + self.total_sent = 0 + self.cumu_data = 0 + self.cumu_sent = 0 + self.cumu_receive = 0 + elif type == "receive": + conn.execute( + self.configuration.peersTable.update().values({ + "total_receive": 0, + "cumu_receive": 0, + }).where( + self.configuration.peersTable.c.id == self.id + ) + ) + self.cumu_receive = 0 + self.total_receive = 0 + elif type == "sent": + conn.execute( + self.configuration.peersTable.update().values({ + "total_sent": 0, + "cumu_sent": 0 + }).where( + self.configuration.peersTable.c.id == self.id + ) + ) + self.cumu_sent = 0 + self.total_sent = 0 + else: + return False + except Exception as e: + print(e) + return False + return True \ No newline at end of file diff --git a/src/modules/WireguardConfiguration.py b/src/modules/WireguardConfiguration.py new file mode 100644 index 0000000..7687a4c --- /dev/null +++ b/src/modules/WireguardConfiguration.py @@ -0,0 +1,1046 @@ +""" +WireGuard Configuration +""" +import random, shutil, configparser, ipaddress, os, subprocess +import time, re, uuid, psutil +import traceback +from zipfile import ZipFile +from datetime import datetime, timedelta + +import sqlalchemy +from itertools import islice + +from .DashboardConfig import DashboardConfig +from .Peer import Peer +from .PeerJobs import PeerJobs +from .PeerShareLinks import PeerShareLinks +from .Utilities import StringToBoolean, GenerateWireguardPublicKey, RegexMatch + + +class WireguardConfiguration: + class InvalidConfigurationFileException(Exception): + def __init__(self, m): + self.message = m + + def __str__(self): + return self.message + + def __init__(self, DashboardConfig: DashboardConfig, + AllPeerJobs: PeerJobs, + AllPeerShareLinks: PeerShareLinks, + name: str = None, + data: dict = None, + backup: dict = None, + startup: bool = False, + wg: bool = True + ): + self.Peers = [] + self.__parser: configparser.ConfigParser = configparser.RawConfigParser(strict=False) + self.__parser.optionxform = str + self.__configFileModifiedTime = None + + self.Status: bool = False + self.Name: str = "" + self.PrivateKey: str = "" + self.PublicKey: str = "" + + self.ListenPort: str = "" + self.Address: str = "" + self.DNS: str = "" + self.Table: str = "" + self.MTU: str = "" + self.PreUp: str = "" + self.PostUp: str = "" + self.PreDown: str = "" + self.PostDown: str = "" + self.SaveConfig: bool = True + self.Name = name + self.Protocol = "wg" if wg else "awg" + self.AllPeerJobs = AllPeerJobs + self.DashboardConfig = DashboardConfig + self.AllPeerShareLinks = AllPeerShareLinks + self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf') + self.engine: sqlalchemy.engine = sqlalchemy.create_engine(self.DashboardConfig.getConnectionString("wgdashboard")) + self.metadata: sqlalchemy.MetaData = sqlalchemy.MetaData() + self.dbType = self.DashboardConfig.GetConfig("Database", "type")[1] + + if name is not None: + if data is not None and "Backup" in data.keys(): + db = self.__importDatabase( + os.path.join( + self.__getProtocolPath(), + 'WGDashboard_Backup', + data["Backup"].replace(".conf", ".sql"))) + else: + self.createDatabase() + + self.__parseConfigurationFile() + self.__initPeersList() + + + + else: + self.Name = data["ConfigurationName"] + self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf') + + for i in dir(self): + if str(i) in data.keys(): + if isinstance(getattr(self, i), bool): + setattr(self, i, StringToBoolean(data[i])) + else: + setattr(self, i, str(data[i])) + + self.__parser["Interface"] = { + "PrivateKey": self.PrivateKey, + "Address": self.Address, + "ListenPort": self.ListenPort, + "PreUp": f"{self.PreUp}", + "PreDown": f"{self.PreDown}", + "PostUp": f"{self.PostUp}", + "PostDown": f"{self.PostDown}", + "SaveConfig": "true" + } + + if self.Protocol == 'awg': + self.__parser["Interface"]["Jc"] = self.Jc + self.__parser["Interface"]["Jc"] = self.Jc + self.__parser["Interface"]["Jmin"] = self.Jmin + self.__parser["Interface"]["Jmax"] = self.Jmax + self.__parser["Interface"]["S1"] = self.S1 + self.__parser["Interface"]["S2"] = self.S2 + self.__parser["Interface"]["H1"] = self.H1 + self.__parser["Interface"]["H2"] = self.H2 + self.__parser["Interface"]["H3"] = self.H3 + self.__parser["Interface"]["H4"] = self.H4 + + if "Backup" not in data.keys(): + self.createDatabase() + with open(self.configPath, "w+") as configFile: + self.__parser.write(configFile) + print(f"[WGDashboard] Configuration file {self.configPath} created") + self.__initPeersList() + + if not os.path.exists(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')): + os.mkdir(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')) + + print(f"[WGDashboard] Initialized Configuration: {name}") + self.__dumpDatabase() + if self.getAutostartStatus() and not self.getStatus() and startup: + self.toggleConfiguration() + print(f"[WGDashboard] Autostart Configuration: {name}") + + def __getProtocolPath(self): + return self.DashboardConfig.GetConfig("Server", "wg_conf_path")[1] if self.Protocol == "wg" \ + else self.DashboardConfig.GetConfig("Server", "awg_conf_path")[1] + + def __initPeersList(self): + self.Peers: list[Peer] = [] + self.getPeersList() + self.getRestrictedPeersList() + + def getRawConfigurationFile(self): + return open(self.configPath, 'r').read() + + def updateRawConfigurationFile(self, newRawConfiguration): + backupStatus, backup = self.backupConfigurationFile() + if not backupStatus: + return False, "Cannot create backup" + + if self.Status: + self.toggleConfiguration() + + with open(self.configPath, 'w') as f: + f.write(newRawConfiguration) + + status, err = self.toggleConfiguration() + if not status: + restoreStatus = self.restoreBackup(backup['filename']) + print(f"Restore status: {restoreStatus}") + self.toggleConfiguration() + return False, err + return True, None + + def __parseConfigurationFile(self): + with open(self.configPath, 'r') as f: + original = [l.rstrip("\n") for l in f.readlines()] + try: + start = original.index("[Interface]") + + # Clean + for i in range(start, len(original)): + if original[i] == "[Peer]": + break + split = re.split(r'\s*=\s*', original[i], 1) + if len(split) == 2: + key = split[0] + if key in dir(self): + if isinstance(getattr(self, key), bool): + setattr(self, key, False) + else: + setattr(self, key, "") + + # Set + for i in range(start, len(original)): + if original[i] == "[Peer]": + break + split = re.split(r'\s*=\s*', original[i], 1) + if len(split) == 2: + key = split[0] + value = split[1] + if key in dir(self): + if isinstance(getattr(self, key), bool): + setattr(self, key, StringToBoolean(value)) + else: + if len(getattr(self, key)) > 0: + setattr(self, key, f"{getattr(self, key)}, {value}") + else: + setattr(self, key, value) + except ValueError as e: + raise self.InvalidConfigurationFileException( + "[Interface] section not found in " + self.configPath) + if self.PrivateKey: + self.PublicKey = self.__getPublicKey() + self.Status = self.getStatus() + + def __dropDatabase(self): + existingTables = [self.Name, f'{self.Name}_restrict_access', f'{self.Name}_transfer', f'{self.Name}_deleted'] + try: + with self.engine.begin() as conn: + for t in existingTables: + conn.execute( + sqlalchemy.text( + f'DROP TABLE "{t}"' + ) + ) + except Exception as e: + print("[WGDashboard] Error: Drop table failed") + return False + return True + + def createDatabase(self, dbName = None): + if dbName is None: + dbName = self.Name + self.peersTable = sqlalchemy.Table( + dbName, self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False, primary_key=True), + sqlalchemy.Column('private_key', sqlalchemy.String), + sqlalchemy.Column('DNS', sqlalchemy.String), + sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.String), + sqlalchemy.Column('name', sqlalchemy.String), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('endpoint', sqlalchemy.String), + sqlalchemy.Column('status', sqlalchemy.String), + sqlalchemy.Column('latest_handshake', sqlalchemy.String), + sqlalchemy.Column('allowed_ip', sqlalchemy.String), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('mtu', sqlalchemy.Integer), + sqlalchemy.Column('keepalive', sqlalchemy.Integer), + sqlalchemy.Column('remote_endpoint', sqlalchemy.String), + sqlalchemy.Column('preshared_key', sqlalchemy.String), + extend_existing=True + ) + self.peersRestrictedTable = sqlalchemy.Table( + f'{dbName}_restrict_access', self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False, primary_key=True), + sqlalchemy.Column('private_key', sqlalchemy.String), + sqlalchemy.Column('DNS', sqlalchemy.String), + sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.String), + sqlalchemy.Column('name', sqlalchemy.String), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('endpoint', sqlalchemy.String), + sqlalchemy.Column('status', sqlalchemy.String), + sqlalchemy.Column('latest_handshake', sqlalchemy.String), + sqlalchemy.Column('allowed_ip', sqlalchemy.String), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('mtu', sqlalchemy.Integer), + sqlalchemy.Column('keepalive', sqlalchemy.Integer), + sqlalchemy.Column('remote_endpoint', sqlalchemy.String), + sqlalchemy.Column('preshared_key', sqlalchemy.String), + extend_existing=True + ) + self.peersTransferTable = sqlalchemy.Table( + f'{dbName}_transfer', self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('time', (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP), + server_default=sqlalchemy.func.now()), + extend_existing=True + ) + self.peersDeletedTable = sqlalchemy.Table( + f'{dbName}_deleted', self.metadata, + sqlalchemy.Column('id', sqlalchemy.String, nullable=False, primary_key=True), + sqlalchemy.Column('private_key', sqlalchemy.String), + sqlalchemy.Column('DNS', sqlalchemy.String), + sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.String), + sqlalchemy.Column('name', sqlalchemy.String), + sqlalchemy.Column('total_receive', sqlalchemy.Float), + sqlalchemy.Column('total_sent', sqlalchemy.Float), + sqlalchemy.Column('total_data', sqlalchemy.Float), + sqlalchemy.Column('endpoint', sqlalchemy.String), + sqlalchemy.Column('status', sqlalchemy.String), + sqlalchemy.Column('latest_handshake', sqlalchemy.String), + sqlalchemy.Column('allowed_ip', sqlalchemy.String), + sqlalchemy.Column('cumu_receive', sqlalchemy.Float), + sqlalchemy.Column('cumu_sent', sqlalchemy.Float), + sqlalchemy.Column('cumu_data', sqlalchemy.Float), + sqlalchemy.Column('mtu', sqlalchemy.Integer), + sqlalchemy.Column('keepalive', sqlalchemy.Integer), + sqlalchemy.Column('remote_endpoint', sqlalchemy.String), + sqlalchemy.Column('preshared_key', sqlalchemy.String), + extend_existing=True + ) + + self.metadata.create_all(self.engine) + + def __dumpDatabase(self): + with self.engine.connect() as conn: + tables = [self.peersTable, self.peersRestrictedTable, self.peersTransferTable, self.peersDeletedTable] + for i in tables: + rows = conn.execute(i.select()).mappings().fetchall() + for row in rows: + insert_stmt = i.insert().values(dict(row)) + yield str(insert_stmt.compile(compile_kwargs={"literal_binds": True})) + + def __importDatabase(self, sqlFilePath) -> bool: + self.__dropDatabase() + self.createDatabase() + if not os.path.exists(sqlFilePath): + return False + with self.engine.begin() as conn: + with open(sqlFilePath, 'r') as f: + for l in f.readlines(): + l = l.rstrip("\n") + if len(l) > 0: + conn.execute(sqlalchemy.text(l)) + return True + + def __getPublicKey(self) -> str: + return GenerateWireguardPublicKey(self.PrivateKey)[1] + + def getStatus(self) -> bool: + self.Status = self.Name in psutil.net_if_addrs().keys() + return self.Status + + def getAutostartStatus(self): + s, d = self.DashboardConfig.GetConfig("WireGuardConfiguration", "autostart") + return self.Name in d + + def getRestrictedPeers(self): + self.RestrictedPeers = [] + with self.engine.connect() as conn: + restricted = conn.execute(self.peersRestrictedTable.select()).mappings().fetchall() + for i in restricted: + self.RestrictedPeers.append(Peer(i, self)) + + def configurationFileChanged(self) : + mt = os.path.getmtime(self.configPath) + changed = self.__configFileModifiedTime is None or self.__configFileModifiedTime != mt + self.__configFileModifiedTime = mt + return changed + + def getPeers(self): + self.Peers = [] + if self.configurationFileChanged(): + with open(self.configPath, 'r') as configFile: + p = [] + pCounter = -1 + content = configFile.read().split('\n') + try: + peerStarts = content.index("[Peer]") + content = content[peerStarts:] + for i in content: + if not RegexMatch("#(.*)", i) and not RegexMatch(";(.*)", i): + if i == "[Peer]": + pCounter += 1 + p.append({}) + p[pCounter]["name"] = "" + else: + if len(i) > 0: + split = re.split(r'\s*=\s*', i, 1) + if len(split) == 2: + p[pCounter][split[0]] = split[1] + + if RegexMatch("#Name# = (.*)", i): + split = re.split(r'\s*=\s*', i, 1) + if len(split) == 2: + p[pCounter]["name"] = split[1] + with self.engine.begin() as conn: + for i in p: + if "PublicKey" in i.keys(): + tempPeer = conn.execute(self.peersTable.select().where( + self.peersTable.columns.id == i['PublicKey'] + )).mappings().fetchone() + + if tempPeer is None: + tempPeer = { + "id": i['PublicKey'], + "private_key": "", + "DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1], + "endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[ + 1], + "name": i.get("name"), + "total_receive": 0, + "total_sent": 0, + "total_data": 0, + "endpoint": "N/A", + "status": "stopped", + "latest_handshake": "N/A", + "allowed_ip": i.get("AllowedIPs", "N/A"), + "cumu_receive": 0, + "cumu_sent": 0, + "cumu_data": 0, + "mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1], + "keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1], + "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], + "preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else "" + } + conn.execute( + self.peersTable.insert().values(tempPeer) + ) + else: + conn.execute( + self.peersTable.update().values({ + "allowed_ip": i.get("AllowedIPs", "N/A") + }).where( + self.peersTable.columns.id == i['PublicKey'] + ) + ) + self.Peers.append(Peer(tempPeer, self)) + except Exception as e: + if __name__ == '__main__': + print(f"[WGDashboard] {self.Name} getPeers() Error: {str(e)}") + else: + with self.engine.connect() as conn: + existingPeers = conn.execute(self.peersTable.select()).mappings().fetchall() + for i in existingPeers: + self.Peers.append(Peer(i, self)) + + def addPeers(self, peers: list) -> tuple[bool, dict]: + result = { + "message": None, + "peers": [] + } + try: + with self.engine.begin() as conn: + for i in peers: + newPeer = { + "id": i['id'], + "private_key": i['private_key'], + "DNS": i['DNS'], + "endpoint_allowed_ip": i['endpoint_allowed_ip'], + "name": i['name'], + "total_receive": 0, + "total_sent": 0, + "total_data": 0, + "endpoint": "N/A", + "status": "stopped", + "latest_handshake": "N/A", + "allowed_ip": i.get("allowed_ip", "N/A"), + "cumu_receive": 0, + "cumu_sent": 0, + "cumu_data": 0, + "mtu": i['mtu'], + "keepalive": i['keepalive'], + "remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1], + "preshared_key": i["preshared_key"] + } + conn.execute( + self.peersTable.insert().values(newPeer) + ) + for p in peers: + presharedKeyExist = len(p['preshared_key']) > 0 + rd = random.Random() + uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) + if presharedKeyExist: + with open(uid, "w+") as f: + f.write(p['preshared_key']) + + subprocess.check_output(f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}", + shell=True, stderr=subprocess.STDOUT) + if presharedKeyExist: + os.remove(uid) + subprocess.check_output( + f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) + self.getPeersList() + for p in peers: + p = self.searchPeer(p['id']) + if p[0]: + result['peers'].append(p[1]) + return True, result + except Exception as e: + result['message'] = str(e) + return False, result + + def searchPeer(self, publicKey): + for i in self.Peers: + if i.id == publicKey: + return True, i + return False, None + + def allowAccessPeers(self, listOfPublicKeys) -> tuple[bool, str]: + if not self.getStatus(): + self.toggleConfiguration() + with self.engine.begin() as conn: + for i in listOfPublicKeys: + stmt = self.peersRestrictedTable.select().where( + self.peersRestrictedTable.columns.id == i + ) + restrictedPeer = conn.execute(stmt).mappings().fetchone() + if restrictedPeer is not None: + conn.execute( + self.peersTable.insert().from_select( + [c.name for c in self.peersTable.columns], + stmt + ) + ) + conn.execute( + self.peersRestrictedTable.delete().where( + self.peersRestrictedTable.columns.id == i + ) + ) + + presharedKeyExist = len(restrictedPeer['preshared_key']) > 0 + rd = random.Random() + uid = str(uuid.UUID(int=rd.getrandbits(128), version=4)) + if presharedKeyExist: + with open(uid, "w+") as f: + f.write(restrictedPeer['preshared_key']) + + subprocess.check_output(f"{self.Protocol} set {self.Name} peer {restrictedPeer['id']} allowed-ips {restrictedPeer['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}", + shell=True, stderr=subprocess.STDOUT) + if presharedKeyExist: os.remove(uid) + else: + return False, "Failed to allow access of peer " + i + if not self.__wgSave(): + return False, "Failed to save configuration through WireGuard" + self.getPeers() + return True, "Allow access successfully" + + def restrictPeers(self, listOfPublicKeys) -> tuple[bool, str]: + numOfRestrictedPeers = 0 + numOfFailedToRestrictPeers = 0 + if not self.getStatus(): + self.toggleConfiguration() + + with self.engine.begin() as conn: + for p in listOfPublicKeys: + found, pf = self.searchPeer(p) + if found: + try: + subprocess.check_output(f"{self.Protocol} set {self.Name} peer {pf.id} remove", + shell=True, stderr=subprocess.STDOUT) + conn.execute( + self.peersRestrictedTable.insert().from_select( + [c.name for c in self.peersTable.columns], + self.peersTable.select().where( + self.peersTable.columns.id == pf.id + ) + ) + ) + conn.execute( + self.peersRestrictedTable.update().values({ + "status": "stopped" + }).where( + self.peersRestrictedTable.columns.id == pf.id + ) + ) + conn.execute( + self.peersTable.delete().where( + self.peersTable.columns.id == pf.id + ) + ) + numOfRestrictedPeers += 1 + except Exception as e: + traceback.print_stack() + numOfFailedToRestrictPeers += 1 + + if not self.__wgSave(): + return False, "Failed to save configuration through WireGuard" + + self.getPeers() + + if numOfRestrictedPeers == len(listOfPublicKeys): + return True, f"Restricted {numOfRestrictedPeers} peer(s)" + return False, f"Restricted {numOfRestrictedPeers} peer(s) successfully. Failed to restrict {numOfFailedToRestrictPeers} peer(s)" + + + def deletePeers(self, listOfPublicKeys) -> tuple[bool, str]: + numOfDeletedPeers = 0 + numOfFailedToDeletePeers = 0 + if not self.getStatus(): + self.toggleConfiguration() + with self.engine.begin() as conn: + for p in listOfPublicKeys: + found, pf = self.searchPeer(p) + if found: + try: + subprocess.check_output(f"{self.Protocol} set {self.Name} peer {pf.id} remove", + shell=True, stderr=subprocess.STDOUT) + conn.execute( + self.peersTable.delete().where( + self.peersTable.columns.id == pf.id + ) + ) + numOfDeletedPeers += 1 + except Exception as e: + numOfFailedToDeletePeers += 1 + + if not self.__wgSave(): + return False, "Failed to save configuration through WireGuard" + + self.getPeers() + + if numOfDeletedPeers == 0 and numOfFailedToDeletePeers == 0: + return False, "No peer(s) to delete found" + + if numOfDeletedPeers == len(listOfPublicKeys): + return True, f"Deleted {numOfDeletedPeers} peer(s)" + return False, f"Deleted {numOfDeletedPeers} peer(s) successfully. Failed to delete {numOfFailedToDeletePeers} peer(s)" + + def __wgSave(self) -> tuple[bool, str] | tuple[bool, None]: + try: + subprocess.check_output(f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) + return True, None + except subprocess.CalledProcessError as e: + return False, str(e) + + def getPeersLatestHandshake(self): + if not self.getStatus(): + self.toggleConfiguration() + try: + latestHandshake = subprocess.check_output(f"{self.Protocol} show {self.Name} latest-handshakes", + shell=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError: + return "stopped" + latestHandshake = latestHandshake.decode("UTF-8").split() + count = 0 + now = datetime.now() + time_delta = timedelta(minutes=2) + + with self.engine.begin() as conn: + for _ in range(int(len(latestHandshake) / 2)): + minus = now - datetime.fromtimestamp(int(latestHandshake[count + 1])) + if minus < time_delta: + status = "running" + else: + status = "stopped" + if int(latestHandshake[count + 1]) > 0: + conn.execute( + self.peersTable.update().values({ + "latest_handshake": str(minus).split(".", maxsplit=1)[0], + "status": status + }).where( + self.peersTable.columns.id == latestHandshake[count] + ) + ) + else: + conn.execute( + self.peersTable.update().values({ + "latest_handshake": "No Handshake", + "status": status + }).where( + self.peersTable.columns.id == latestHandshake[count] + ) + ) + count += 2 + + def getPeersTransfer(self): + if not self.getStatus(): + self.toggleConfiguration() + try: + data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} transfer", + shell=True, stderr=subprocess.STDOUT) + data_usage = data_usage.decode("UTF-8").split("\n") + data_usage = [p.split("\t") for p in data_usage] + with self.engine.begin() as conn: + for i in range(len(data_usage)): + if len(data_usage[i]) == 3: + cur_i = conn.execute( + self.peersTable.select().where( + self.peersTable.c.id == data_usage[i][0] + ) + ).mappings().fetchone() + if cur_i is not None: + total_sent = cur_i['total_sent'] + total_receive = cur_i['total_receive'] + cur_total_sent = float(data_usage[i][2]) / (1024 ** 3) + cur_total_receive = float(data_usage[i][1]) / (1024 ** 3) + cumulative_receive = cur_i['cumu_receive'] + total_receive + cumulative_sent = cur_i['cumu_sent'] + total_sent + if total_sent <= cur_total_sent and total_receive <= cur_total_receive: + total_sent = cur_total_sent + total_receive = cur_total_receive + else: + conn.execute( + self.peersTable.update().values({ + "cumu_receive": cumulative_receive, + "cumu_sent": cumulative_sent, + "cumu_data": cumulative_sent + cumulative_receive + }).where( + self.peersTable.c.id == data_usage[i][0] + ) + ) + + total_sent = 0 + total_receive = 0 + _, p = self.searchPeer(data_usage[i][0]) + if p.total_receive != total_receive or p.total_sent != total_sent: + conn.execute( + self.peersTable.update().values({ + "total_receive": total_receive, + "total_sent": total_sent, + "total_data": total_receive + total_sent + }).where( + self.peersTable.c.id == data_usage[i][0] + ) + ) + + except Exception as e: + print(f"[WGDashboard] {self.Name} getPeersTransfer() Error: {str(e)} {str(e.__traceback__)}") + + def getPeersEndpoint(self): + if not self.getStatus(): + self.toggleConfiguration() + try: + data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} endpoints", + shell=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError: + return "stopped" + data_usage = data_usage.decode("UTF-8").split() + count = 0 + with self.engine.begin() as conn: + for _ in range(int(len(data_usage) / 2)): + conn.execute( + self.peersTable.update().values({ + "endpoint": data_usage[count + 1] + }).where( + self.peersTable.c.id == data_usage[count] + ) + ) + count += 2 + + def toggleConfiguration(self) -> [bool, str]: + self.getStatus() + if self.Status: + try: + check = subprocess.check_output(f"{self.Protocol}-quick down {self.Name}", + shell=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as exc: + return False, str(exc.output.strip().decode("utf-8")) + else: + try: + check = subprocess.check_output(f"{self.Protocol}-quick up {self.Name}", shell=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as exc: + return False, str(exc.output.strip().decode("utf-8")) + self.__parseConfigurationFile() + self.getStatus() + return True, None + + def getPeersList(self): + self.getPeers() + return self.Peers + + def getRestrictedPeersList(self) -> list: + self.getRestrictedPeers() + return self.RestrictedPeers + + def toJson(self): + self.Status = self.getStatus() + return { + "Status": self.Status, + "Name": self.Name, + "PrivateKey": self.PrivateKey, + "PublicKey": self.PublicKey, + "Address": self.Address, + "ListenPort": self.ListenPort, + "PreUp": self.PreUp, + "PreDown": self.PreDown, + "PostUp": self.PostUp, + "PostDown": self.PostDown, + "SaveConfig": self.SaveConfig, + "DataUsage": { + "Total": sum(list(map(lambda x: x.cumu_data + x.total_data, self.Peers))), + "Sent": sum(list(map(lambda x: x.cumu_sent + x.total_sent, self.Peers))), + "Receive": sum(list(map(lambda x: x.cumu_receive + x.total_receive, self.Peers))) + }, + "ConnectedPeers": len(list(filter(lambda x: x.status == "running", self.Peers))), + "TotalPeers": len(self.Peers), + "Protocol": self.Protocol, + "Table": self.Table, + } + + def backupConfigurationFile(self) -> tuple[bool, dict[str, str]]: + if not os.path.exists(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')): + os.mkdir(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')) + time = datetime.now().strftime("%Y%m%d%H%M%S") + shutil.copy( + self.configPath, + os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f'{self.Name}_{time}.conf') + ) + with open(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f'{self.Name}_{time}.sql'), 'w+') as f: + for l in self.__dumpDatabase(): + f.write(l + "\n") + + return True, { + "filename": f'{self.Name}_{time}.conf', + "backupDate": datetime.now().strftime("%Y%m%d%H%M%S") + } + + def getBackups(self, databaseContent: bool = False) -> list[dict[str: str, str: str, str: str]]: + backups = [] + + directory = os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup') + files = [(file, os.path.getctime(os.path.join(directory, file))) + for file in os.listdir(directory) if os.path.isfile(os.path.join(directory, file))] + files.sort(key=lambda x: x[1], reverse=True) + + for f, ct in files: + if RegexMatch(f"^({self.Name})_(.*)\\.(conf)$", f): + s = re.search(f"^({self.Name})_(.*)\\.(conf)$", f) + date = s.group(2) + d = { + "filename": f, + "backupDate": date, + "content": open(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f), 'r').read() + } + if f.replace(".conf", ".sql") in list(os.listdir(directory)): + d['database'] = True + if databaseContent: + d['databaseContent'] = open(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', f.replace(".conf", ".sql")), 'r').read() + backups.append(d) + + return backups + + def restoreBackup(self, backupFileName: str) -> bool: + backups = list(map(lambda x : x['filename'], self.getBackups())) + if backupFileName not in backups: + return False + if self.Status: + self.toggleConfiguration() + target = os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backupFileName) + targetSQL = os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backupFileName.replace(".conf", ".sql")) + if not os.path.exists(target): + return False + targetContent = open(target, 'r').read() + try: + with open(self.configPath, 'w') as f: + f.write(targetContent) + except Exception as e: + return False + self.__parseConfigurationFile() + self.__importDatabase(targetSQL) + self.__initPeersList() + return True + + def deleteBackup(self, backupFileName: str) -> bool: + backups = list(map(lambda x : x['filename'], self.getBackups())) + if backupFileName not in backups: + return False + try: + os.remove(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backupFileName)) + except Exception as e: + return False + return True + + def downloadBackup(self, backupFileName: str) -> tuple[bool, str] | tuple[bool, None]: + backup = list(filter(lambda x : x['filename'] == backupFileName, self.getBackups())) + if len(backup) == 0: + return False, None + zip = f'{str(uuid.UUID(int=random.Random().getrandbits(128), version=4))}.zip' + with ZipFile(os.path.join('download', zip), 'w') as zipF: + zipF.write( + os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename']), + os.path.basename(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename'])) + ) + if backup[0]['database']: + zipF.write( + os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename'].replace('.conf', '.sql')), + os.path.basename(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backup[0]['filename'].replace('.conf', '.sql'))) + ) + + return True, zip + + def updateConfigurationSettings(self, newData: dict) -> tuple[bool, str]: + if self.Status: + self.toggleConfiguration() + original = [] + dataChanged = False + with open(self.configPath, 'r') as f: + original = [l.rstrip("\n") for l in f.readlines()] + allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"] + if self.Protocol == 'awg': + allowEdit += ["Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4"] + start = original.index("[Interface]") + try: + end = original.index("[Peer]") + except ValueError as e: + end = len(original) + new = ["[Interface]"] + peerFound = False + for line in range(start, end): + split = re.split(r'\s*=\s*', original[line], 1) + if len(split) == 2: + if split[0] not in allowEdit: + new.append(original[line]) + for key in allowEdit: + new.insert(1, f"{key} = {str(newData[key]).strip()}") + new.append("") + for line in range(end, len(original)): + new.append(original[line]) + self.backupConfigurationFile() + with open(self.configPath, 'w') as f: + f.write("\n".join(new)) + + status, msg = self.toggleConfiguration() + if not status: + return False, msg + for i in allowEdit: + setattr(self, i, str(newData[i])) + + return True, "" + + def deleteConfiguration(self): + if self.getStatus(): + self.toggleConfiguration() + os.remove(self.configPath) + self.__dropDatabase() + return True + + def renameConfiguration(self, newConfigurationName) -> tuple[bool, str]: + try: + if self.getStatus(): + self.toggleConfiguration() + self.createDatabase(newConfigurationName) + with self.engine.begin() as conn: + conn.execute( + sqlalchemy.text( + f'INSERT INTO "{newConfigurationName}" SELECT * FROM "{self.Name}"' + ) + ) + conn.execute( + sqlalchemy.text( + f'INSERT INTO "{newConfigurationName}_restrict_access" SELECT * FROM "{self.Name}_restrict_access"' + ) + ) + conn.execute( + sqlalchemy.text( + f'INSERT INTO "{newConfigurationName}_deleted" SELECT * FROM "{self.Name}_deleted"' + ) + ) + conn.execute( + sqlalchemy.text( + f'INSERT INTO "{newConfigurationName}_transfer" SELECT * FROM "{self.Name}_transfer"' + ) + ) + self.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName) + shutil.copy( + self.configPath, + os.path.join(self.__getProtocolPath(), f'{newConfigurationName}.conf') + ) + self.deleteConfiguration() + except Exception as e: + traceback.print_stack() + return False, str(e) + return True, None + + def getNumberOfAvailableIP(self): + if len(self.Address) < 0: + return False, None + existedAddress = set() + availableAddress = {} + for p in self.Peers + self.getRestrictedPeersList(): + peerAllowedIP = p.allowed_ip.split(',') + for pip in peerAllowedIP: + ppip = pip.strip().split('/') + if len(ppip) == 2: + try: + check = ipaddress.ip_network(ppip[0]) + existedAddress.add(check) + except Exception as e: + print(f"[WGDashboard] Error: {self.Name} peer {p.id} have invalid ip") + configurationAddresses = self.Address.split(',') + for ca in configurationAddresses: + ca = ca.strip() + caSplit = ca.split('/') + try: + if len(caSplit) == 2: + network = ipaddress.ip_network(ca, False) + existedAddress.add(ipaddress.ip_network(caSplit[0])) + availableAddress[ca] = network.num_addresses + for p in existedAddress: + if p.version == network.version and p.subnet_of(network): + availableAddress[ca] -= 1 + except Exception as e: + print(e) + print(f"[WGDashboard] Error: Failed to parse IP address {ca} from {self.Name}") + return True, availableAddress + + def getAvailableIP(self, threshold = 255): + if len(self.Address) < 0: + return False, None + existedAddress = set() + availableAddress = {} + for p in self.Peers + self.getRestrictedPeersList(): + peerAllowedIP = p.allowed_ip.split(',') + for pip in peerAllowedIP: + ppip = pip.strip().split('/') + if len(ppip) == 2: + try: + check = ipaddress.ip_network(ppip[0]) + existedAddress.add(check.compressed) + except Exception as e: + print(f"[WGDashboard] Error: {self.Name} peer {p.id} have invalid ip") + configurationAddresses = self.Address.split(',') + for ca in configurationAddresses: + ca = ca.strip() + caSplit = ca.split('/') + try: + if len(caSplit) == 2: + network = ipaddress.ip_network(ca, False) + existedAddress.add(ipaddress.ip_network(caSplit[0]).compressed) + if threshold == -1: + availableAddress[ca] = filter(lambda ip : ip not in existedAddress, + map(lambda iph : ipaddress.ip_network(iph).compressed, network.hosts())) + else: + availableAddress[ca] = list(islice(filter(lambda ip : ip not in existedAddress, + map(lambda iph : ipaddress.ip_network(iph).compressed, network.hosts())), threshold)) + except Exception as e: + print(e) + print(f"[WGDashboard] Error: Failed to parse IP address {ca} from {self.Name}") + return True, availableAddress + + def getRealtimeTrafficUsage(self): + stats = psutil.net_io_counters(pernic=True, nowrap=True) + if self.Name in stats.keys(): + stat = stats[self.Name] + recv1 = stat.bytes_recv + sent1 = stat.bytes_sent + time.sleep(1) + stats = psutil.net_io_counters(pernic=True, nowrap=True) + if self.Name in stats.keys(): + stat = stats[self.Name] + recv2 = stat.bytes_recv + sent2 = stat.bytes_sent + net_in = round((recv2 - recv1) / 1024 / 1024, 3) + net_out = round((sent2 - sent1) / 1024 / 1024, 3) + return { + "sent": net_out, + "recv": net_in + } + else: + return { "sent": 0, "recv": 0 } + else: + return { "sent": 0, "recv": 0 } \ No newline at end of file