diff --git a/src/dashboard.py b/src/dashboard.py index 40121633..5d1ad584 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta import sqlalchemy from jinja2 import Template -from flask import Flask, request, render_template, session, send_file +from flask import Flask, request, render_template, session, send_file, current_app from flask_cors import CORS from icmplib import ping, traceroute from flask.json.provider import DefaultJSONProvider @@ -254,7 +254,6 @@ def auth_req(): whiteList = [ '/static/', 'validateAuthentication', 'authenticate', 'getDashboardConfiguration', 'getDashboardTheme', 'getDashboardVersion', 'sharePeer/get', 'isTotpEnabled', 'locale', - '/fileDownload', '/client', '/assets/', '/img/', '/json/', '/client/assets/', '/client/img/' @@ -608,12 +607,19 @@ def API_deleteWireguardConfigurationBackup(): @app.get(f'{APP_PREFIX}/api/downloadWireguardConfigurationBackup') def API_downloadWireguardConfigurationBackup(): - configurationName = request.args.get('configurationName') - backupFileName = request.args.get('backupFileName') + configurationName = os.path.basename(request.args.get('configurationName')) + backupFileName = os.path.basename(request.args.get('backupFileName')) + if configurationName is None or configurationName not in WireguardConfigurations.keys(): return ResponseObject(False, "Configuration does not exist", status_code=404) + status, zip = WireguardConfigurations[configurationName].downloadBackup(backupFileName) - return ResponseObject(status, data=zip, status_code=(200 if status else 404)) + + if not status: + current_app.logger.error(f"Failed to download a requested backup.\nConfiguration Name: {configurationName}\nBackup File Name: {backupFileName}") + return ResponseObject(False, "Internal server error", status_code=500) + + return send_file(os.path.join('download', zip), as_attachment=True) @app.post(f'{APP_PREFIX}/api/restoreWireguardConfigurationBackup') def API_restoreWireguardConfigurationBackup(): @@ -969,8 +975,11 @@ def API_addPeers(configName): for i in allowed_ips: found = False for subnet in availableIps.keys(): - network = ipaddress.ip_network(subnet, False) - ap = ipaddress.ip_network(i) + try: + network = ipaddress.ip_network(subnet, False) + ap = ipaddress.ip_network(i) + except ValueError as e: + return ResponseObject(False, str(e)) if network.version == ap.version and ap.subnet_of(network): found = True @@ -994,8 +1003,7 @@ def API_addPeers(configName): return ResponseObject(status=status, message=message, data=addedPeers) except Exception as e: app.logger.error("Add peers failed", e) - return ResponseObject(False, - f"Add peers failed. Reason: {message}") + return ResponseObject(False, f"Add peers failed.") return ResponseObject(False, "Configuration does not exist") @@ -1241,20 +1249,6 @@ def API_getPeerScheduleJobLogs(configName): requestAll = True return ResponseObject(data=AllPeerJobs.getPeerJobLogs(configName)) -''' -File Download -''' -@app.get(f'{APP_PREFIX}/fileDownload') -def API_download(): - file = request.args.get('file') - if file is None or len(file) == 0: - return ResponseObject(False, "Please specify a file") - if os.path.exists(os.path.join('download', file)): - return send_file(os.path.join('download', file), as_attachment=True) - else: - return ResponseObject(False, "File does not exist") - - ''' Tools ''' @@ -1742,4 +1736,4 @@ def index(): if __name__ == "__main__": startThreads() DashboardPlugins.startThreads() - app.run(host=app_ip, debug=False, port=app_port) \ No newline at end of file + app.run(host=app_ip, debug=False, port=app_port) diff --git a/src/modules/AmneziaConfiguration.py b/src/modules/AmneziaConfiguration.py index 871369bf..71ca9b0f 100644 --- a/src/modules/AmneziaConfiguration.py +++ b/src/modules/AmneziaConfiguration.py @@ -6,7 +6,7 @@ from flask import current_app from .PeerJobs import PeerJobs from .AmneziaPeer import AmneziaPeer from .PeerShareLinks import PeerShareLinks -from .Utilities import RegexMatch +from .Utilities import RegexMatch, CheckAddress from .WireguardConfiguration import WireguardConfiguration from .DashboardWebHooks import DashboardWebHooks @@ -276,13 +276,22 @@ class AmneziaConfiguration(WireguardConfiguration): 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) + newAllowedIPs = p['allowed_ip'].replace(" ", "") + if not CheckAddress(newAllowedIPs): + return False, [], "Allowed IPs entry format is incorrect" + + if not re.match(r"^[A-Za-z0-9+/]{42}[A-Ea-e0-9]=$", p["id"]): + return False, [], "Peer key format is incorrect" + + command = [self.Protocol, "set", self.Name, "peer", p['id'], "allowed-ips", newAllowedIPs, "preshared-key", uid if presharedKeyExist else "/dev/null"] + subprocess.check_output(command, stderr=subprocess.STDOUT) + if presharedKeyExist: os.remove(uid) - subprocess.check_output( - f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) + + command = [f"{self.Protocol}-quick", "save", self.Name] + subprocess.check_output(command, stderr=subprocess.STDOUT) + self.getPeers() for p in peers: p = self.searchPeer(p['id']) @@ -294,7 +303,7 @@ class AmneziaConfiguration(WireguardConfiguration): }) except Exception as e: current_app.logger.error("Add peers error", e) - return False, [], str(e) + return False, [], "Internal server error" return True, result['peers'], "" def getRestrictedPeers(self): @@ -302,4 +311,4 @@ class AmneziaConfiguration(WireguardConfiguration): with self.engine.connect() as conn: restricted = conn.execute(self.peersRestrictedTable.select()).mappings().fetchall() for i in restricted: - self.RestrictedPeers.append(AmneziaPeer(i, self)) \ No newline at end of file + self.RestrictedPeers.append(AmneziaPeer(i, self)) diff --git a/src/modules/AmneziaPeer.py b/src/modules/AmneziaPeer.py index 5a99257a..509f4305 100644 --- a/src/modules/AmneziaPeer.py +++ b/src/modules/AmneziaPeer.py @@ -1,4 +1,5 @@ import os +from flask import current_app import random import re import subprocess @@ -77,18 +78,25 @@ class AmneziaPeer(Peer): 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 psk_exist else 'preshared-key /dev/null'}", - shell=True, stderr=subprocess.STDOUT) + if not CheckAddress(newAllowedIPs): + return False, "Allowed IPs entry format is incorrect" + + command = [self.configuration.Protocol, "set", self.configuration.Name, "peer", self.id, "allowed-ips", newAllowedIPs, "preshared-key", uid if psk_exist else "/dev/null"] + + updateAllowedIp = subprocess.check_output(command, stderr=subprocess.STDOUT) if psk_exist: 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) + current_app.logger.error(f"Update peer failed when updating Allowed IPs.\nInput: {newAllowedIPs}\nOutput: {updateAllowedIp.decode().strip('\n')}") + return False, "Internal server error" + + command = [f"{self.configuration.Protocol}-quick", "save", self.configuration.Name] + saveConfig = subprocess.check_output(command, 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" + current_app.logger.error("Update peer failed when saving the configuration") + return False, "Internal server error" with self.configuration.engine.begin() as conn: conn.execute( @@ -108,4 +116,5 @@ class AmneziaPeer(Peer): self.configuration.getPeers() return True, None except subprocess.CalledProcessError as exc: - return False, exc.output.decode("UTF-8").strip() \ No newline at end of file + current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode("UTF-8")}") + return False, "Internal server error" diff --git a/src/modules/Peer.py b/src/modules/Peer.py index bba16deb..970e747f 100644 --- a/src/modules/Peer.py +++ b/src/modules/Peer.py @@ -10,6 +10,7 @@ from datetime import timedelta import jinja2 import sqlalchemy as db from .PeerJob import PeerJob +from flask import current_app from .PeerShareLink import PeerShareLink from .Utilities import GenerateWireguardPublicKey, CheckAddress, ValidateDNSAddress @@ -114,17 +115,25 @@ class Peer: 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 psk_exist else 'preshared-key /dev/null'}", - shell=True, stderr=subprocess.STDOUT) + if not CheckAddress(newAllowedIPs): + return False, "Allowed IPs entry format is incorrect" + + command = [self.configuration.Protocol, "set", self.configuration.Name, "peer", self.id, "allowed-ips", newAllowedIPs, "preshared-key", uid if psk_exist else "/dev/null"] + updateAllowedIp = subprocess.check_output(command, stderr=subprocess.STDOUT) if psk_exist: 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) + current_app.logger.error("Update peer failed when updating Allowed IPs") + return False, "Internal server error" + + command = [f"{self.configuration.Protocol}-quick", "save", self.configuration.Name] + saveConfig = subprocess.check_output(command, 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" + current_app.logger.error("Update peer failed when saving the configuration") + return False, "Internal server error" + with self.configuration.engine.begin() as conn: conn.execute( self.configuration.peersTable.update().values({ @@ -142,7 +151,8 @@ class Peer: ) return True, None except subprocess.CalledProcessError as exc: - return False, exc.output.decode("UTF-8").strip() + current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode("UTF-8")}") + return False, "Internal server error" def downloadPeer(self) -> dict[str, str]: final = { @@ -153,12 +163,14 @@ class Peer: 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, "") + + # use previous filtering code if code below is insufficient or faulty + filename = re.sub(r'[.,/?<>\\:*|"]', '', filename).rstrip(". ") # remove special characters + + reserved_pattern = r"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$" # match com1-9, lpt1-9, con, nul, prn, aux, nul + + if re.match(reserved_pattern, filename, re.IGNORECASE): + filename = f"file_{filename}" # prepend "file_" if it matches for i in filename: if re.match("^[a-zA-Z0-9_=+.-]$", i): @@ -379,4 +391,4 @@ class Peer: hours, remainder = divmod(delta.total_seconds(), 3600) minutes, seconds = divmod(remainder, 60) - return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" \ No newline at end of file + return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" diff --git a/src/modules/Utilities.py b/src/modules/Utilities.py index 5e187869..661d3500 100644 --- a/src/modules/Utilities.py +++ b/src/modules/Utilities.py @@ -54,6 +54,9 @@ def CheckAddress(ips_str: str) -> bool: return False return True +def CheckPeerKey(peer_key: str) -> bool: + return re.match(r"^[A-Za-z0-9+/]{43}=$", peer_key) + def ValidateDNSAddress(addresses_str: str) -> tuple[bool, str | None]: if len(addresses_str) == 0: return False, "Got an empty list/string to check for valid DNS-addresses" @@ -110,4 +113,4 @@ def ValidatePasswordStrength(password: str) -> tuple[bool, str] | tuple[bool, No if not re.search(r'[$&+,:;=?@#|\'<>.\-^*()%!~_-]', password): return False, "Password must contain at least 1 special character from $&+,:;=?@#|'<>.-^*()%!~_-" - return True, None \ No newline at end of file + return True, None diff --git a/src/modules/WireguardConfiguration.py b/src/modules/WireguardConfiguration.py index e77acdbe..2911b708 100644 --- a/src/modules/WireguardConfiguration.py +++ b/src/modules/WireguardConfiguration.py @@ -19,7 +19,9 @@ from .Utilities import StringToBoolean, \ GenerateWireguardPublicKey, \ RegexMatch, \ ValidateDNSAddress, \ - ValidateEndpointAllowedIPs + ValidateEndpointAllowedIPs, \ + CheckAddress, \ + CheckPeerKey from .WireguardConfigurationInfo import WireguardConfigurationInfo, PeerGroupsClass from .DashboardWebHooks import DashboardWebHooks @@ -545,12 +547,22 @@ class WireguardConfiguration: 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) + newAllowedIPs = p['allowed_ip'].replace(" ", "") + if not CheckAddress(newAllowedIPs): + return False, [], "Allowed IPs entry format is incorrect" + + if not CheckPeerKey(p["id"]): + return False, [], "Peer key format is incorrect" + + command = [self.Protocol, "set", self.Name, "peer", p['id'], "allowed-ips", newAllowedIPs, "preshared-key", uid if presharedKeyExist else "/dev/null"] + subprocess.check_output(command, stderr=subprocess.STDOUT) + if presharedKeyExist: os.remove(uid) - subprocess.check_output( - f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT) + + command = [f"{self.Protocol}-quick", "save", self.Name] + subprocess.check_output(command, stderr=subprocess.STDOUT) + self.getPeers() for p in peers: p = self.searchPeer(p['id']) @@ -562,7 +574,7 @@ class WireguardConfiguration: }) except Exception as e: current_app.logger.error("Add peers error", e) - return False, [], str(e) + return False, [], "Internal server error" return True, result['peers'], "" def searchPeer(self, publicKey): @@ -600,8 +612,16 @@ class WireguardConfiguration: 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) + newAllowedIPs = restrictedPeer['allowed_ip'].replace(" ", "") + if not CheckAddress(newAllowedIPs): + return False, "Allowed IPs entry format is incorrect" + + if not CheckPeerKey(restrictedPeer["id"]): + return False, "Peer key format is incorrect" + + command = [self.Protocol, "set", self.Name, "peer", restrictedPeer["id"], "allowed-ips", newAllowedIPs, "preshared-key", uid if presharedKeyExist else "/dev/null"] + subprocess.check_output(command, stderr=subprocess.STDOUT) + if presharedKeyExist: os.remove(uid) else: return False, "Failed to allow access of peer " + i @@ -621,8 +641,9 @@ class WireguardConfiguration: 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) + command = [self.Protocol, "set", self.Name, "peer", pf.id, "remove"] + subprocess.check_output(command, stderr=subprocess.STDOUT) + conn.execute( self.peersRestrictedTable.insert().from_select( [c.name for c in self.peersTable.columns], @@ -703,17 +724,20 @@ class WireguardConfiguration: 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) + command = [f"{self.Protocol}-quick", "save", self.Name] + subprocess.check_output(command, stderr=subprocess.STDOUT) + return True, None except subprocess.CalledProcessError as e: - return False, str(e) + current_app.logger.error(f"Failed to process command:\n{str(e)}") + return False, "Internal server error" 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) + command = [self.Protocol, "show", self.Name, "latest-handshakes"] + latestHandshake = subprocess.check_output(command, stderr=subprocess.STDOUT) except subprocess.CalledProcessError: return "stopped" latestHandshake = latestHandshake.decode("UTF-8").split() @@ -752,8 +776,9 @@ class WireguardConfiguration: if not self.getStatus(): self.toggleConfiguration() # try: - data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} transfer", - shell=True, stderr=subprocess.STDOUT) + command = [self.Protocol, "show", self.Name, "transfer"] + data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT) + data_usage = data_usage.decode("UTF-8").split("\n") data_usage = [p.split("\t") for p in data_usage] @@ -808,10 +833,11 @@ class WireguardConfiguration: if not self.getStatus(): self.toggleConfiguration() try: - data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} endpoints", - shell=True, stderr=subprocess.STDOUT) + command = [self.Protocol, "show", self.Name, "endpoints"] + data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT) except subprocess.CalledProcessError: return "stopped" + data_usage = data_usage.decode("UTF-8").split() count = 0 with self.engine.begin() as conn: @@ -829,14 +855,17 @@ class WireguardConfiguration: self.getStatus() if self.Status: try: - check = subprocess.check_output(f"{self.Protocol}-quick down {self.Name}", - shell=True, stderr=subprocess.STDOUT) + command = [f"{self.Protocol}-quick", "down", self.Name] + check = subprocess.check_output(command, stderr=subprocess.STDOUT) + self.removeAutostart() 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) + command = [f"{self.Protocol}-quick", "up", self.Name] + check = subprocess.check_output(command, stderr=subprocess.STDOUT) + self.addAutostart() except subprocess.CalledProcessError as exc: return False, str(exc.output.strip().decode("utf-8")) @@ -1015,31 +1044,33 @@ class WireguardConfiguration: return True def renameConfiguration(self, newConfigurationName) -> tuple[bool, str]: + newConfigurationName = os.path.basename(newConfigurationName) + + if len(newConfigurationName) > 15 or not re.match(r'^[a-zA-Z0-9_=\+\.\-]{1,15}$', newConfigurationName): + return False, "Configuration name is either too long or contains an illegal character" + + newConfigurationName = newConfigurationName.replace("`", "") # double check + 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}"' + def doRenameStatement(suffix): + newConfig = f"{newConfigurationName}{suffix}" + oldConfig = f"{self.Name}{suffix}" + + conn.execute( + sqlalchemy.text( + f'INSERT INTO `{newConfig}` SELECT * FROM `{oldConfig}`' + ) ) - ) - 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"' - ) - ) + + doRenameStatement("") + doRenameStatement("_restrict_access") + doRenameStatement("_deleted") + doRenameStatement("_transfer") + self.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName) shutil.copy( self.configPath, @@ -1047,8 +1078,8 @@ class WireguardConfiguration: ) self.deleteConfiguration() except Exception as e: - traceback.print_stack() - return False, str(e) + current_app.logger.error(f"Failed to rename configuration.\nNew Configuration Name: {newConfigurationName}\nError: {str(e)}") + return False, "Internal server error" return True, None def getNumberOfAvailableIP(self): diff --git a/src/static/app/src/components/configurationComponents/backupRestoreComponents/backup.vue b/src/static/app/src/components/configurationComponents/backupRestoreComponents/backup.vue index 8aceffef..6cf31e9e 100644 --- a/src/static/app/src/components/configurationComponents/backupRestoreComponents/backup.vue +++ b/src/static/app/src/components/configurationComponents/backupRestoreComponents/backup.vue @@ -46,14 +46,7 @@ const restoreBackup = () => { } const downloadBackup = () => { - fetchGet("/api/downloadWireguardConfigurationBackup", { - configurationName: route.params.id, - backupFileName: props.b.filename - }, (res) => { - if (res.status){ - window.open(getUrl(`/fileDownload?file=${res.data}`), '_blank') - } - }) + window.location.href = getUrl(`/api/downloadWireguardConfigurationBackup?configurationName=${route.params.id}&backupFileName=${props.b.filename}`); } const delaySeconds = computed(() => {