From 6f71fc201f092d7d9ef5909add34c3b53cc34c8e Mon Sep 17 00:00:00 2001 From: Super User Date: Fri, 30 Jan 2026 00:32:53 -0500 Subject: [PATCH] Preserve peers on config upload - Preserve uploaded configs with peer blocks by saving raw content - Populate peers from file via existing getPeers() logic - Add path validation and basic content checks for safety - Inform users in UI when peers are preserved from upload --- src/dashboard.py | 55 ++++++------ src/modules/WireguardConfiguration.py | 85 +++++++++++++------ src/static/app/src/views/newConfiguration.vue | 22 +++-- 3 files changed, 103 insertions(+), 59 deletions(-) diff --git a/src/dashboard.py b/src/dashboard.py index ec57ef9d..048113e3 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -392,32 +392,35 @@ def API_addWireguardConfiguration(): f"Already have a configuration with the address \"{data['Address']}\"", "Address") - if "Backup" in data.keys(): - path = { - "wg": DashboardConfig.GetConfig("Server", "wg_conf_path")[1], - "awg": DashboardConfig.GetConfig("Server", "awg_conf_path")[1] - } - - if (os.path.exists(os.path.join(path['wg'], 'WGDashboard_Backup', data["Backup"])) and - os.path.exists(os.path.join(path['wg'], 'WGDashboard_Backup', data["Backup"].replace('.conf', '.sql')))): - protocol = "wg" - elif (os.path.exists(os.path.join(path['awg'], 'WGDashboard_Backup', data["Backup"])) and - os.path.exists(os.path.join(path['awg'], 'WGDashboard_Backup', data["Backup"].replace('.conf', '.sql')))): - protocol = "awg" + try: + if "Backup" in data.keys(): + path = { + "wg": DashboardConfig.GetConfig("Server", "wg_conf_path")[1], + "awg": DashboardConfig.GetConfig("Server", "awg_conf_path")[1] + } + + if (os.path.exists(os.path.join(path['wg'], 'WGDashboard_Backup', data["Backup"])) and + os.path.exists(os.path.join(path['wg'], 'WGDashboard_Backup', data["Backup"].replace('.conf', '.sql')))): + protocol = "wg" + elif (os.path.exists(os.path.join(path['awg'], 'WGDashboard_Backup', data["Backup"])) and + os.path.exists(os.path.join(path['awg'], 'WGDashboard_Backup', data["Backup"].replace('.conf', '.sql')))): + protocol = "awg" + else: + return ResponseObject(False, "Backup does not exist") + + shutil.copy( + os.path.join(path[protocol], 'WGDashboard_Backup', data["Backup"]), + os.path.join(path[protocol], f'{data["ConfigurationName"]}.conf') + ) + WireguardConfigurations[data['ConfigurationName']] = ( + WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else ( + AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName'])) else: - return ResponseObject(False, "Backup does not exist") - - shutil.copy( - os.path.join(path[protocol], 'WGDashboard_Backup', data["Backup"]), - os.path.join(path[protocol], f'{data["ConfigurationName"]}.conf') - ) - WireguardConfigurations[data['ConfigurationName']] = ( - WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else ( - AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName'])) - else: - WireguardConfigurations[data['ConfigurationName']] = ( - WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) if data.get('Protocol') == 'wg' else ( - AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) + WireguardConfigurations[data['ConfigurationName']] = ( + WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) if data.get('Protocol') == 'wg' else ( + AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) + except WireguardConfiguration.InvalidConfigurationFileException as exc: + return ResponseObject(False, str(exc)) return ResponseObject() @app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration') @@ -1703,4 +1706,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/WireguardConfiguration.py b/src/modules/WireguardConfiguration.py index f1fdfe16..a3cbe3d4 100644 --- a/src/modules/WireguardConfiguration.py +++ b/src/modules/WireguardConfiguration.py @@ -91,35 +91,58 @@ class WireguardConfiguration: 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" - } + raw_configuration = data.get("RawConfiguration") if data else None + if raw_configuration: + try: + if not self.__isPathWithinBase(self.configPath, self.__getProtocolPath()): + raise self.InvalidConfigurationFileException("Configuration path is invalid") + if not raw_configuration.lstrip().startswith("[Interface]"): + raise self.InvalidConfigurationFileException("Invalid configuration file: missing [Interface]") + if any(ord(ch) == 0 for ch in raw_configuration): + raise self.InvalidConfigurationFileException("Invalid configuration file: binary content detected") + self.createDatabase() + with open(self.configPath, "w+") as configFile: + configFile.write(raw_configuration) + current_app.logger.info(f"Configuration file {self.configPath} created from upload") + self.__parseConfigurationFile() + self.__initPeersList() + except Exception: + try: + os.remove(self.configPath) + except OSError: + pass + self.__dropDatabase() + raise + else: + 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 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) - current_app.logger.info(f"Configuration file {self.configPath} created") - self.__initPeersList() + if "Backup" not in data.keys(): + self.createDatabase() + with open(self.configPath, "w+") as configFile: + self.__parser.write(configFile) + current_app.logger.info(f"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')) @@ -147,6 +170,12 @@ class WireguardConfiguration: else self.DashboardConfig.GetConfig("Server", "awg_conf_path") return path + def __isPathWithinBase(self, path: str, base: str) -> bool: + try: + return os.path.commonpath([os.path.realpath(path), os.path.realpath(base)]) == os.path.realpath(base) + except ValueError: + return False + def __initPeersList(self): self.Peers: list[Peer] = [] self.getPeers() @@ -1291,4 +1320,4 @@ class WireguardConfiguration: conn.execute(sqlalchemy.text('VACUUM;')) except Exception as e: return False - return True \ No newline at end of file + return True diff --git a/src/static/app/src/views/newConfiguration.vue b/src/static/app/src/views/newConfiguration.vue index ef101229..5f45fb7f 100644 --- a/src/static/app/src/views/newConfiguration.vue +++ b/src/static/app/src/views/newConfiguration.vue @@ -54,7 +54,9 @@ export default { success: false, loading: false, parseInterfaceResult: undefined, - parsePeersResult: undefined + parsePeersResult: undefined, + rawConfiguration: "", + rawConfigHasPeers: false } }, created() { @@ -81,7 +83,11 @@ export default { async saveNewConfiguration(){ if (this.goodToSubmit){ this.loading = true; - await fetchPost("/api/addWireguardConfiguration", this.newConfiguration, async (res) => { + const payload = {...this.newConfiguration}; + if (this.rawConfiguration && this.rawConfigHasPeers){ + payload.RawConfiguration = this.rawConfiguration; + } + await fetchPost("/api/addWireguardConfiguration", payload, async (res) => { if (res.status){ this.success = true await this.store.getConfigurations() @@ -104,8 +110,11 @@ export default { if (!file) return false; const reader = new FileReader(); reader.onload = (evt) => { - this.parseInterfaceResult = parseInterface(evt.target.result); - this.parsePeersResult = parsePeers(evt.target.result); + const raw = evt.target.result; + this.rawConfiguration = raw; + this.parseInterfaceResult = parseInterface(raw); + this.parsePeersResult = parsePeers(raw); + this.rawConfigHasPeers = Array.isArray(this.parsePeersResult) && this.parsePeersResult.length > 0; let appliedFields = 0; if (this.parseInterfaceResult){ this.newConfiguration.ConfigurationName = file.name.replace('.conf', '') @@ -118,6 +127,9 @@ export default { } if (appliedFields > 0){ this.dashboardStore.newMessage("WGDashboard", `Parse successful! Updated ${appliedFields} field(s)`, "success") + if (this.rawConfigHasPeers){ + this.dashboardStore.newMessage("WGDashboard", "Peers detected. The uploaded file will be imported as-is to preserve peer definitions.", "info") + } }else { this.dashboardStore.newMessage("WGDashboard", `Parse failed`, "danger") } @@ -415,4 +427,4 @@ export default { .protocolBtnGroup a{ transition: all 0.2s ease-in-out; } - \ No newline at end of file +