From e6723d9ebceacc24cec06021f3e8d1b02f2ef388 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 20 May 2023 17:49:13 -0400 Subject: [PATCH 001/124] Add new backup routes --- app/classes/web/routes/api/api_handlers.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index 29ee02c5..8e4b7ac3 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -26,6 +26,12 @@ from app.classes.web.routes.api.servers.server.stdin import ApiServersServerStdi from app.classes.web.routes.api.servers.server.tasks.index import ( ApiServersServerTasksIndexHandler, ) +from app.classes.web.routes.api.servers.server.backups.index import ( + ApiServersServerBackupsIndexHandler, +) +from app.classes.web.routes.api.servers.server.backups.backup.index import ( + ApiServersServerBackupsBackupIndexHandler, +) from app.classes.web.routes.api.servers.server.tasks.task.children import ( ApiServersServerTasksTaskChildrenHandler, ) @@ -112,6 +118,16 @@ def api_handlers(handler_args): ApiServersServerIndexHandler, handler_args, ), + ( + r"/api/v2/servers/([0-9]+)/backups/?", + ApiServersServerBackupsIndexHandler, + handler_args, + ), + ( + r"/api/v2/servers/([0-9]+)/backups/backup/?", + ApiServersServerBackupsBackupIndexHandler, + handler_args, + ), ( r"/api/v2/servers/([0-9]+)/tasks/?", ApiServersServerTasksIndexHandler, From fb16be6a6444bb8cd549803a585c5079dd458216 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 20 May 2023 17:49:51 -0400 Subject: [PATCH 002/124] Add index handlers (aware of the lint issue) --- .../servers/server/backups/backup/index.py | 53 +++++++++ .../api/servers/server/backups/index.py | 104 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 app/classes/web/routes/api/servers/server/backups/backup/index.py create mode 100644 app/classes/web/routes/api/servers/server/backups/index.py diff --git a/app/classes/web/routes/api/servers/server/backups/backup/index.py b/app/classes/web/routes/api/servers/server/backups/backup/index.py new file mode 100644 index 00000000..5c7a6f86 --- /dev/null +++ b/app/classes/web/routes/api/servers/server/backups/backup/index.py @@ -0,0 +1,53 @@ +import logging +import json +import os +from app.classes.models.server_permissions import EnumPermissionsServer +from app.classes.shared.file_helpers import FileHelpers +from app.classes.web.base_api_handler import BaseApiHandler + +logger = logging.getLogger(__name__) + + +class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): + def get(self, server_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + if ( + EnumPermissionsServer.BACKUP + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Schedule permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + self.finish_json(200, self.controller.management.get_backup_config(server_id)) + + def delete(self, server_id: str, backup_name: str): + auth_data = self.authenticate_user() + backup_conf = self.controller.management.get_backup_config(server_id) + if not auth_data: + return + if ( + EnumPermissionsServer.BACKUP + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Schedule permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + try: + FileHelpers.del_file(os.path.join(backup_conf["backup_path"], backup_name)) + except Exception: + return self.finish_json( + 400, {"status": "error", "error": "NO BACKUP FOUND"} + ) + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Edited server {server_id}: removed backup {backup_name}", + server_id, + self.get_remote_ip(), + ) + + return self.finish_json(200, {"status": "ok"}) diff --git a/app/classes/web/routes/api/servers/server/backups/index.py b/app/classes/web/routes/api/servers/server/backups/index.py new file mode 100644 index 00000000..ac2111db --- /dev/null +++ b/app/classes/web/routes/api/servers/server/backups/index.py @@ -0,0 +1,104 @@ +import logging +import json +import os +from jsonschema import validate +from jsonschema.exceptions import ValidationError +from app.classes.models.server_permissions import EnumPermissionsServer +from app.classes.shared.file_helpers import FileHelpers +from app.classes.web.base_api_handler import BaseApiHandler + +logger = logging.getLogger(__name__) + +backup_patch_schema = { + "type": "object", + "properties": { + "path": {"type": "string", "minLength": 1}, + "max": {"type": "int"}, + "compress": {"type": "boolean"}, + "shutdown": {"type": "boolean"}, + "before_command": {"type": "string"}, + "after_command": {"type": "string"}, + "exclusions": {"type": "string"}, + }, + "additionalProperties": False, + "minProperties": 1, +} + +basic_backup_patch_schema = { + "type": "object", + "properties": { + "max": {"type": "int"}, + "compress": {"type": "boolean"}, + "shutdown": {"type": "boolean"}, + "before_command": {"type": "string"}, + "after_command": {"type": "string"}, + "exclusions": {"type": "string"}, + }, + "additionalProperties": False, + "minProperties": 1, +} + + +class ApiServersServerBackupsIndexHandler(BaseApiHandler): + def get(self, server_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + if ( + EnumPermissionsServer.BACKUP + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Schedule permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + self.finish_json(200, self.controller.management.get_backup_config(server_id)) + + def patch(self, server_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + + try: + validate(data, backup_patch_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + + if server_id not in [str(x["server_id"]) for x in auth_data[0]]: + # if the user doesn't have access to the server, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + if ( + EnumPermissionsServer.BACKUP + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Schedule permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + self.controller.management.set_backup_config( + data["server_id"], + data["backup_path"], + data["max_backups"], + data["excluded_dirs"], + data["compress"], + data["shutdown"], + data["before"], + data["after"], + ) + return self.finish(200, {"status": "ok"}) From 7db5f400ab6f9870e9613ad5c40eefd04c5ab374 Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Sun, 4 Jun 2023 11:42:08 -0400 Subject: [PATCH 003/124] Add backup delete and restore functions to API --- .../servers/server/backups/backup/index.py | 163 +++++++++++++++++- .../api/servers/server/backups/index.py | 2 - .../templates/panel/server_backup.html | 59 ++++--- 3 files changed, 199 insertions(+), 25 deletions(-) diff --git a/app/classes/web/routes/api/servers/server/backups/backup/index.py b/app/classes/web/routes/api/servers/server/backups/backup/index.py index 5c7a6f86..23782158 100644 --- a/app/classes/web/routes/api/servers/server/backups/backup/index.py +++ b/app/classes/web/routes/api/servers/server/backups/backup/index.py @@ -1,12 +1,24 @@ import logging import json import os +from jsonschema import validate +from jsonschema.exceptions import ValidationError from app.classes.models.server_permissions import EnumPermissionsServer from app.classes.shared.file_helpers import FileHelpers from app.classes.web.base_api_handler import BaseApiHandler +from app.classes.shared.helpers import Helpers logger = logging.getLogger(__name__) +backup_schema = { + "type": "object", + "properties": { + "filename": {"type": "string", "minLength": 5}, + }, + "additionalProperties": False, + "minProperties": 1, +} + class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): def get(self, server_id: str): @@ -23,7 +35,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) self.finish_json(200, self.controller.management.get_backup_config(server_id)) - def delete(self, server_id: str, backup_name: str): + def delete(self, server_id: str): auth_data = self.authenticate_user() backup_conf = self.controller.management.get_backup_config(server_id) if not auth_data: @@ -38,14 +50,159 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) try: - FileHelpers.del_file(os.path.join(backup_conf["backup_path"], backup_name)) + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + try: + validate(data, backup_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + + try: + FileHelpers.del_file( + os.path.join(backup_conf["backup_path"], data["filename"]) + ) except Exception: return self.finish_json( 400, {"status": "error", "error": "NO BACKUP FOUND"} ) self.controller.management.add_to_audit_log( auth_data[4]["user_id"], - f"Edited server {server_id}: removed backup {backup_name}", + f"Edited server {server_id}: removed backup {data['filename']}", + server_id, + self.get_remote_ip(), + ) + + return self.finish_json(200, {"status": "ok"}) + + def post(self, server_id: str): + auth_data = self.authenticate_user() + backup_conf = self.controller.management.get_backup_config(server_id) + if not auth_data: + return + if ( + EnumPermissionsServer.BACKUP + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Schedule permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + try: + validate(data, backup_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + + try: + svr_obj = self.controller.servers.get_server_obj(server_id) + server_data = self.controller.servers.get_server_data_by_id(server_id) + zip_name = data["filename"] + # import the server again based on zipfile + if server_data["type"] == "minecraft-java": + backup_path = svr_obj.backup_path + if Helpers.validate_traversal(backup_path, zip_name): + temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name) + new_server = self.controller.import_zip_server( + svr_obj.server_name, + temp_dir, + server_data["executable"], + "1", + "2", + server_data["server_port"], + server_data["created_by"], + ) + new_server_id = new_server + new_server = self.controller.servers.get_server_data(new_server) + self.controller.rename_backup_dir( + server_id, new_server_id, new_server["server_uuid"] + ) + # preserve current schedules + for schedule in self.controller.management.get_schedules_by_server( + server_id + ): + self.tasks_manager.update_job( + schedule.schedule_id, {"server_id": new_server_id} + ) + # preserve execution command + new_server_obj = self.controller.servers.get_server_obj( + new_server_id + ) + new_server_obj.execution_command = server_data["execution_command"] + # reset executable path + if svr_obj.path in svr_obj.executable: + new_server_obj.executable = str(svr_obj.executable).replace( + svr_obj.path, new_server_obj.path + ) + # reset run command path + if svr_obj.path in svr_obj.execution_command: + new_server_obj.execution_command = str( + svr_obj.execution_command + ).replace(svr_obj.path, new_server_obj.path) + # reset log path + if svr_obj.path in svr_obj.log_path: + new_server_obj.log_path = str(svr_obj.log_path).replace( + svr_obj.path, new_server_obj.path + ) + self.controller.servers.update_server(new_server_obj) + + # preserve backup config + backup_config = self.controller.management.get_backup_config( + server_id + ) + excluded_dirs = [] + server_obj = self.controller.servers.get_server_obj(server_id) + loop_backup_path = self.helper.wtol_path(server_obj.path) + for item in self.controller.management.get_excluded_backup_dirs( + server_id + ): + item_path = self.helper.wtol_path(item) + bu_path = os.path.relpath(item_path, loop_backup_path) + bu_path = os.path.join(new_server_obj.path, bu_path) + excluded_dirs.append(bu_path) + self.controller.management.set_backup_config( + new_server_id, + new_server_obj.backup_path, + backup_config["max_backups"], + excluded_dirs, + backup_config["compress"], + backup_config["shutdown"], + ) + # remove old server's tasks + try: + self.tasks_manager.remove_all_server_tasks(server_id) + except: + logger.info("No active tasks found for server") + self.controller.remove_server(server_id, True) + except Exception: + return self.finish_json( + 400, {"status": "error", "error": "NO BACKUP FOUND"} + ) + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Restored server {server_id} backup {data['filename']}", server_id, self.get_remote_ip(), ) diff --git a/app/classes/web/routes/api/servers/server/backups/index.py b/app/classes/web/routes/api/servers/server/backups/index.py index ac2111db..b3f6f7ed 100644 --- a/app/classes/web/routes/api/servers/server/backups/index.py +++ b/app/classes/web/routes/api/servers/server/backups/index.py @@ -1,10 +1,8 @@ import logging import json -import os from jsonschema import validate from jsonschema.exceptions import ValidationError from app.classes.models.server_permissions import EnumPermissionsServer -from app.classes.shared.file_helpers import FileHelpers from app.classes.web.base_api_handler import BaseApiHandler logger = logging.getLogger(__name__) diff --git a/app/frontend/templates/panel/server_backup.html b/app/frontend/templates/panel/server_backup.html index 1cb8f087..6b9ba68f 100644 --- a/app/frontend/templates/panel/server_backup.html +++ b/app/frontend/templates/panel/server_backup.html @@ -333,25 +333,23 @@ }); return; } - - function del_backup(filename, id) { + async function del_backup(filename, id) { var token = getCookie("_xsrf") - - data_to_send = { file_name: filename } - - console.log('Sending Command to delete backup: ' + filename) - $.ajax({ - type: "DELETE", - headers: { 'X-XSRFToken': token }, - url: '/ajax/del_backup?server_id=' + id, - data: { - file_path: filename, - id: id - }, - success: function (data) { - location.reload(); + let contents = JSON.stringify({"filename": filename}) + let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, { + method: 'DELETE', + headers: { + 'token': token, }, + body: contents }); + let responseData = await res.json(); + if (responseData.status === "ok") { + window.location.reload(); + }else{ + bootbox.alert({"title": responseData.status, + "message": responseData.error}) + } } function restore_backup(filename, id) { @@ -366,9 +364,8 @@ type: "POST", headers: { 'X-XSRFToken': token }, url: '/ajax/restore_backup?server_id=' + id, - data: { - zip_file: filename, - id: id + body: { + "filename": filename, }, success: function (data) { setTimeout(function () { @@ -377,6 +374,28 @@ }, }); } + async function restore_backup(filename, id) { + var token = getCookie("_xsrf") + let contents = JSON.stringify({"filename": filename}) + var dialog = bootbox.dialog({ + message: " {{ translate('serverBackups', 'restoring', data['lang']) }}", + closeButton: false + }); + let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, { + method: 'POST', + headers: { + 'token': token, + }, + body: contents + }); + let responseData = await res.json(); + if (responseData.status === "ok") { + window.location.href = "/panel/dashboard"; + }else{ + bootbox.alert({"title": responseData.status, + "message": responseData.error}) + } + } $("#before-check").on("click", function () { if ($("#before-check:checked").val()) { @@ -457,7 +476,7 @@ console.log(result); if (result == true) { var full_path = backup_path + '/' + file_to_del; - del_backup(full_path, server_id); + del_backup(file_to_del, server_id); } } }); From 27cb3dc37abc5726f47d656ded95503cc6c5b552 Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Sun, 4 Jun 2023 11:48:39 -0400 Subject: [PATCH 004/124] Remove backups delete/restore from ajax handler --- app/classes/web/ajax_handler.py | 299 +----------------- .../servers/server/backups/backup/index.py | 1 - 2 files changed, 1 insertion(+), 299 deletions(-) diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index e3da33a8..ef634027 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -267,87 +267,12 @@ class AjaxHandler(BaseHandler): server_id = self.get_argument("id", None) - permissions = { - "Commands": EnumPermissionsServer.COMMANDS, - "Terminal": EnumPermissionsServer.TERMINAL, - "Logs": EnumPermissionsServer.LOGS, - "Schedule": EnumPermissionsServer.SCHEDULE, - "Backup": EnumPermissionsServer.BACKUP, - "Files": EnumPermissionsServer.FILES, - "Config": EnumPermissionsServer.CONFIG, - "Players": EnumPermissionsServer.PLAYERS, - } - user_perms = self.controller.server_perms.get_user_id_permissions_list( - exec_user["user_id"], server_id - ) - - if page == "send_command": - command = self.get_body_argument("command", default=None, strip=True) - server_id = self.get_argument("id", None) - - if server_id is None: - logger.warning("Server ID not found in send_command ajax call") - Console.warning("Server ID not found in send_command ajax call") - - svr_obj = self.controller.servers.get_server_instance_by_id(server_id) - - if command == svr_obj.settings["stop_command"]: - logger.info( - "Stop command detected as terminal input - intercepting." - + f"Starting Crafty's stop process for server with id: {server_id}" - ) - self.controller.management.send_command( - exec_user["user_id"], server_id, self.get_remote_ip(), "stop_server" - ) - command = None - elif command == "restart": - logger.info( - "Restart command detected as terminal input - intercepting." - + f"Starting Crafty's stop process for server with id: {server_id}" - ) - self.controller.management.send_command( - exec_user["user_id"], - server_id, - self.get_remote_ip(), - "restart_server", - ) - command = None - if command: - if svr_obj.check_running(): - svr_obj.send_command(command) - - self.controller.management.add_to_audit_log( - exec_user["user_id"], - f"Sent command to " - f"{self.controller.servers.get_server_friendly_name(server_id)} " - f"terminal: {command}", - server_id, - self.get_remote_ip(), - ) - - elif page == "send_order": + if page == "send_order": self.controller.users.update_server_order( exec_user["user_id"], bleach.clean(self.get_argument("order")) ) return - elif page == "backup_now": - server_id = self.get_argument("id", None) - if server_id is None: - logger.error("Server ID is none. Canceling backup!") - return - - server = self.controller.servers.get_server_instance_by_id(server_id) - self.controller.management.add_to_audit_log_raw( - self.controller.users.get_user_by_id(exec_user["user_id"])["username"], - exec_user["user_id"], - server_id, - f"Backup now executed for server {server_id} ", - source_ip=self.get_remote_ip(), - ) - - server.backup_server() - elif page == "select_photo": if exec_user["superuser"]: photo = urllib.parse.unquote(self.get_argument("photo", "")) @@ -387,168 +312,6 @@ class AjaxHandler(BaseHandler): svr = self.controller.servers.get_server_instance_by_id(server_id) svr.agree_eula(exec_user["user_id"]) - elif page == "restore_backup": - if not permissions["Backup"] in user_perms: - if not superuser: - self.redirect("/panel/error?error=Unauthorized access to Backups") - return - server_id = bleach.clean(self.get_argument("id", None)) - zip_name = bleach.clean(self.get_argument("zip_file", None)) - svr_obj = self.controller.servers.get_server_obj(server_id) - server_data = self.controller.servers.get_server_data_by_id(server_id) - - # import the server again based on zipfile - if server_data["type"] == "minecraft-java": - backup_path = svr_obj.backup_path - if Helpers.validate_traversal(backup_path, zip_name): - temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name) - new_server = self.controller.import_zip_server( - svr_obj.server_name, - temp_dir, - server_data["executable"], - "1", - "2", - server_data["server_port"], - server_data["created_by"], - ) - new_server_id = new_server - new_server = self.controller.servers.get_server_data(new_server) - self.controller.rename_backup_dir( - server_id, new_server_id, new_server["server_uuid"] - ) - # preserve current schedules - for schedule in self.controller.management.get_schedules_by_server( - server_id - ): - self.tasks_manager.update_job( - schedule.schedule_id, {"server_id": new_server_id} - ) - # preserve execution command - new_server_obj = self.controller.servers.get_server_obj( - new_server_id - ) - new_server_obj.execution_command = server_data["execution_command"] - # reset executable path - if svr_obj.path in svr_obj.executable: - new_server_obj.executable = str(svr_obj.executable).replace( - svr_obj.path, new_server_obj.path - ) - # reset run command path - if svr_obj.path in svr_obj.execution_command: - new_server_obj.execution_command = str( - svr_obj.execution_command - ).replace(svr_obj.path, new_server_obj.path) - # reset log path - if svr_obj.path in svr_obj.log_path: - new_server_obj.log_path = str(svr_obj.log_path).replace( - svr_obj.path, new_server_obj.path - ) - self.controller.servers.update_server(new_server_obj) - - # preserve backup config - backup_config = self.controller.management.get_backup_config( - server_id - ) - excluded_dirs = [] - server_obj = self.controller.servers.get_server_obj(server_id) - loop_backup_path = self.helper.wtol_path(server_obj.path) - for item in self.controller.management.get_excluded_backup_dirs( - server_id - ): - item_path = self.helper.wtol_path(item) - bu_path = os.path.relpath(item_path, loop_backup_path) - bu_path = os.path.join(new_server_obj.path, bu_path) - excluded_dirs.append(bu_path) - self.controller.management.set_backup_config( - new_server_id, - new_server_obj.backup_path, - backup_config["max_backups"], - excluded_dirs, - backup_config["compress"], - backup_config["shutdown"], - ) - # remove old server's tasks - try: - self.tasks_manager.remove_all_server_tasks(server_id) - except: - logger.info("No active tasks found for server") - self.controller.remove_server(server_id, True) - self.redirect("/panel/dashboard") - - else: - backup_path = svr_obj.backup_path - if Helpers.validate_traversal(backup_path, zip_name): - temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name) - new_server = self.controller.import_bedrock_zip_server( - svr_obj.server_name, - temp_dir, - server_data["executable"], - server_data["server_port"], - server_data["created_by"], - ) - new_server_id = new_server - new_server = self.controller.servers.get_server_data(new_server) - self.controller.rename_backup_dir( - server_id, new_server_id, new_server["server_uuid"] - ) - # preserve current schedules - for schedule in self.controller.management.get_schedules_by_server( - server_id - ): - self.tasks_manager.update_job( - schedule.schedule_id, {"server_id": new_server_id} - ) - # preserve execution command - new_server_obj = self.controller.servers.get_server_obj( - new_server_id - ) - new_server_obj.execution_command = server_data["execution_command"] - # reset executable path - if server_obj.path in server_obj.executable: - new_server_obj.executable = str(server_obj.executable).replace( - server_obj.path, new_server_obj.path - ) - # reset run command path - if server_obj.path in server_obj.execution_command: - new_server_obj.execution_command = str( - server_obj.execution_command - ).replace(server_obj.path, new_server_obj.path) - # reset log path - if server_obj.path in server_obj.log_path: - new_server_obj.log_path = str(server_obj.log_path).replace( - server_obj.path, new_server_obj.path - ) - self.controller.servers.update_server(new_server_obj) - - # preserve backup config - backup_config = self.controller.management.get_backup_config( - server_id - ) - excluded_dirs = [] - server_obj = self.controller.servers.get_server_obj(server_id) - loop_backup_path = self.helper.wtol_path(server_obj.path) - for item in self.controller.management.get_excluded_backup_dirs( - server_id - ): - item_path = self.helper.wtol_path(item) - bu_path = os.path.relpath(item_path, loop_backup_path) - bu_path = os.path.join(new_server_obj.path, bu_path) - excluded_dirs.append(bu_path) - self.controller.management.set_backup_config( - new_server_id, - new_server_obj.backup_path, - backup_config["max_backups"], - excluded_dirs, - backup_config["compress"], - backup_config["shutdown"], - ) - try: - self.tasks_manager.remove_all_server_tasks(server_id) - except: - logger.info("No active tasks found for server") - self.controller.remove_server(server_id, True) - self.redirect("/panel/dashboard") - elif page == "unzip_server": path = urllib.parse.unquote(self.get_argument("path", "")) if not path: @@ -615,66 +378,6 @@ class AjaxHandler(BaseHandler): self.controller.update_master_server_dir(new_dir, exec_user["user_id"]) return - @tornado.web.authenticated - def delete(self, page): - api_key, _, exec_user = self.current_user - superuser = exec_user["superuser"] - if api_key is not None: - superuser = superuser and api_key.superuser - - server_id = self.get_argument("id", None) - - permissions = { - "Commands": EnumPermissionsServer.COMMANDS, - "Terminal": EnumPermissionsServer.TERMINAL, - "Logs": EnumPermissionsServer.LOGS, - "Schedule": EnumPermissionsServer.SCHEDULE, - "Backup": EnumPermissionsServer.BACKUP, - "Files": EnumPermissionsServer.FILES, - "Config": EnumPermissionsServer.CONFIG, - "Players": EnumPermissionsServer.PLAYERS, - } - user_perms = self.controller.server_perms.get_user_id_permissions_list( - exec_user["user_id"], server_id - ) - - if page == "del_backup": - if not permissions["Backup"] in user_perms: - if not superuser: - self.redirect("/panel/error?error=Unauthorized access to Backups") - return - file_path = Helpers.get_os_understandable_path( - self.get_body_argument("file_path", default=None, strip=True) - ) - server_id = self.get_argument("id", None) - - Console.warning(f"Delete {file_path} for server {server_id}") - - if not self.check_server_id(server_id, "del_backup"): - return - server_id = bleach.clean(server_id) - - server_info = self.controller.servers.get_server_data_by_id(server_id) - if not ( - Helpers.in_path( - Helpers.get_os_understandable_path(server_info["path"]), file_path - ) - or Helpers.in_path( - Helpers.get_os_understandable_path(server_info["backup_path"]), - file_path, - ) - ) or not Helpers.check_file_exists(os.path.abspath(file_path)): - logger.warning(f"Invalid path in del_backup ajax call ({file_path})") - Console.warning(f"Invalid path in del_backup ajax call ({file_path})") - return - - # Delete the file - if Helpers.validate_traversal( - Helpers.get_os_understandable_path(server_info["backup_path"]), - file_path, - ): - os.remove(file_path) - def check_server_id(self, server_id, page_name): if server_id is None: logger.warning( diff --git a/app/classes/web/routes/api/servers/server/backups/backup/index.py b/app/classes/web/routes/api/servers/server/backups/backup/index.py index 23782158..56a42cbb 100644 --- a/app/classes/web/routes/api/servers/server/backups/backup/index.py +++ b/app/classes/web/routes/api/servers/server/backups/backup/index.py @@ -86,7 +86,6 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): def post(self, server_id: str): auth_data = self.authenticate_user() - backup_conf = self.controller.management.get_backup_config(server_id) if not auth_data: return if ( From 1ff9bc9e83d024930361beea7e60f8edc4573c77 Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Sun, 4 Jun 2023 14:43:45 -0400 Subject: [PATCH 005/124] Setup backup file trees through API --- app/classes/web/ajax_handler.py | 132 ------------------ app/classes/web/routes/api/api_handlers.py | 8 ++ .../api/servers/server/backups/files.py | 125 +++++++++++++++++ .../templates/panel/server_backup.html | 132 ++++++++++-------- 4 files changed, 208 insertions(+), 189 deletions(-) create mode 100644 app/classes/web/routes/api/servers/server/backups/files.py diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index ef634027..9419d82d 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -108,138 +108,6 @@ class AjaxHandler(BaseHandler): ) self.finish() - elif page == "get_backup_tree": - server_id = self.get_argument("id", None) - folder = self.get_argument("path", None) - - output = "" - - dir_list = [] - unsorted_files = [] - file_list = os.listdir(folder) - for item in file_list: - if os.path.isdir(os.path.join(folder, item)): - dir_list.append(item) - else: - unsorted_files.append(item) - file_list = sorted(dir_list, key=str.casefold) + sorted( - unsorted_files, key=str.casefold - ) - output += f"""
    """ - for raw_filename in file_list: - filename = html.escape(raw_filename) - rel = os.path.join(folder, raw_filename) - dpath = os.path.join(folder, filename) - if str(dpath) in self.controller.management.get_excluded_backup_dirs( - server_id - ): - if os.path.isdir(rel): - output += f"""
  • - \n
    - - - - - {filename} - -
  • - \n""" - else: - output += f"""
  • - {filename}
  • """ - - else: - if os.path.isdir(rel): - output += f"""
  • - \n
    - - - - - {filename} - -
  • - \n""" - else: - output += f"""
  • - - {filename}
  • """ - self.write(Helpers.get_os_understandable_path(folder) + "\n" + output) - self.finish() - - elif page == "get_backup_dir": - server_id = self.get_argument("id", None) - folder = self.get_argument("path", None) - output = "" - - dir_list = [] - unsorted_files = [] - file_list = os.listdir(folder) - for item in file_list: - if os.path.isdir(os.path.join(folder, item)): - dir_list.append(item) - else: - unsorted_files.append(item) - file_list = sorted(dir_list, key=str.casefold) + sorted( - unsorted_files, key=str.casefold - ) - output += f"""
      """ - for raw_filename in file_list: - filename = html.escape(raw_filename) - rel = os.path.join(folder, raw_filename) - dpath = os.path.join(folder, filename) - if str(dpath) in self.controller.management.get_excluded_backup_dirs( - server_id - ): - if os.path.isdir(rel): - output += f"""
    • - \n
      - - - - - {filename} - -
    • """ - else: - output += f"""
    • - {filename}
    • """ - - else: - if os.path.isdir(rel): - output += f"""
    • - \n
      - - - - - {filename} - -
    • """ - else: - output += f"""
    • - - {filename}
    • """ - - self.write(Helpers.get_os_understandable_path(folder) + "\n" + output) - self.finish() - elif page == "get_dir": server_id = self.get_argument("id", None) path = self.get_argument("path", None) diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index 8e4b7ac3..eb5d1297 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -32,6 +32,9 @@ from app.classes.web.routes.api.servers.server.backups.index import ( from app.classes.web.routes.api.servers.server.backups.backup.index import ( ApiServersServerBackupsBackupIndexHandler, ) +from app.classes.web.routes.api.servers.server.backups.files import ( + ApiServersServerBackupsFilesIndexHandler, +) from app.classes.web.routes.api.servers.server.tasks.task.children import ( ApiServersServerTasksTaskChildrenHandler, ) @@ -128,6 +131,11 @@ def api_handlers(handler_args): ApiServersServerBackupsBackupIndexHandler, handler_args, ), + ( + r"/api/v2/servers/([0-9]+)/backups/files/?", + ApiServersServerBackupsFilesIndexHandler, + handler_args, + ), ( r"/api/v2/servers/([0-9]+)/tasks/?", ApiServersServerTasksIndexHandler, diff --git a/app/classes/web/routes/api/servers/server/backups/files.py b/app/classes/web/routes/api/servers/server/backups/files.py new file mode 100644 index 00000000..44945409 --- /dev/null +++ b/app/classes/web/routes/api/servers/server/backups/files.py @@ -0,0 +1,125 @@ +import os +import logging +import json +import html +from jsonschema import validate +from jsonschema.exceptions import ValidationError +from app.classes.models.server_permissions import EnumPermissionsServer +from app.classes.shared.helpers import Helpers +from app.classes.web.base_api_handler import BaseApiHandler + +logger = logging.getLogger(__name__) + +files_get_schema = { + "type": "object", + "properties": { + "page": {"type": "string", "minLength": 1}, + "folder": {"type": "string"}, + }, + "additionalProperties": False, + "minProperties": 1, +} + + +class ApiServersServerBackupsFilesIndexHandler(BaseApiHandler): + def post(self, server_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + + if server_id not in [str(x["server_id"]) for x in auth_data[0]]: + # if the user doesn't have access to the server, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + if ( + EnumPermissionsServer.BACKUP + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Config permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + try: + validate(data, files_get_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + if not Helpers.validate_traversal( + self.controller.servers.get_server_data_by_id(server_id)["path"], + data["folder"], + ): + return self.finish_json( + 400, + { + "status": "error", + "error": "TRAVERSAL DETECTED", + "error_data": str(e), + }, + ) + # TODO: limit some columns for specific permissions? + folder = data["folder"] + return_json = { + "root_path": { + "path": folder, + "top": data["folder"] + == self.controller.servers.get_server_data_by_id(server_id)["path"], + } + } + + dir_list = [] + unsorted_files = [] + file_list = os.listdir(folder) + for item in file_list: + if os.path.isdir(os.path.join(folder, item)): + dir_list.append(item) + else: + unsorted_files.append(item) + file_list = sorted(dir_list, key=str.casefold) + sorted( + unsorted_files, key=str.casefold + ) + for raw_filename in file_list: + filename = html.escape(raw_filename) + rel = os.path.join(folder, raw_filename) + dpath = os.path.join(folder, filename) + if str(dpath) in self.controller.management.get_excluded_backup_dirs( + server_id + ): + if os.path.isdir(rel): + return_json[filename] = { + "path": dpath, + "dir": True, + "excluded": True, + } + else: + return_json[filename] = { + "path": dpath, + "dir": False, + "excluded": True, + } + else: + if os.path.isdir(rel): + return_json[filename] = { + "path": dpath, + "dir": True, + "excluded": False, + } + else: + return_json[filename] = { + "path": dpath, + "dir": False, + "excluded": False, + } + self.finish_json(200, {"status": "ok", "data": return_json}) diff --git a/app/frontend/templates/panel/server_backup.html b/app/frontend/templates/panel/server_backup.html index 6b9ba68f..195eb4d9 100644 --- a/app/frontend/templates/panel/server_backup.html +++ b/app/frontend/templates/panel/server_backup.html @@ -577,68 +577,81 @@ }); } - function getTreeView(path) { - path = path + function getDirView(event){ + let path = event.target.parentElement.getAttribute("data-path"); + if (document.getElementById(path).classList.contains('clicked')) { + return; + }else{ + getTreeView(path); + } - $.ajax({ - type: "GET", - url: '/ajax/get_backup_tree?id=' + server_id + '&path=' + path, - dataType: 'text', - success: function (data) { - console.log("got response:"); - console.log(data); + } + async function getTreeView(path){ + var token = getCookie("_xsrf"); + let res = await fetch(`/api/v2/servers/${server_id}/backups/files`, { + method: 'POST', + headers: { + 'X-XSRFToken': token + }, + body: JSON.stringify({"page": "backups", "folder": path}), + }); + let responseData = await res.json(); + if (responseData.status === "ok") { + console.log(responseData); + process_tree_response(responseData); - dataArr = data.split('\n'); - serverDir = dataArr.shift(); // Remove & return first element (server directory) - text = dataArr.join('\n'); + } else { - try { + bootbox.alert({ + title: responseData.status, + message: responseData.error + }); + } + } + + function process_tree_response(response) { + let path = response.data.root_path.path; + let text = `
        `; + Object.entries(response.data).forEach(([key, value]) => { + if (key === "root_path" || key === "db_stats"){ + //continue is not valid in for each. Return acts as a continue. + return; + } + let checked = "" + let dpath = value.path; + let filename = key; + if (value.dir){ + if (value.excluded){ + checked = "checked" + } + text += `
      • + \n
        + + + + + ${filename} + +
      • ` + }else{ + text += `
      • + ${filename}
      • ` + } + }); + text += `
      `; + if(response.data.root_path.top){ + try { document.getElementById('main-tree-div').innerHTML += text; document.getElementById('main-tree').parentElement.classList.add("clicked"); } catch { document.getElementById('files-tree').innerHTML = text; } - - - document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir); - document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files'); - - }, - }); - } - function getToggleMain(event) { - path = event.target.parentElement.getAttribute('data-path'); - document.getElementById("files-tree").classList.toggle("d-block"); - document.getElementById(path + "span").classList.toggle("tree-caret-down"); - document.getElementById(path + "span").classList.toggle("tree-caret"); - } - - - function getDirView(event) { - path = event.target.parentElement.getAttribute('data-path'); - - if (document.getElementById(path).classList.contains('clicked')) { - - var toggler = document.getElementById(path + "span"); - - if (toggler.classList.contains('files-tree-title')) { - document.getElementById(path + "ul").classList.toggle("d-block"); - document.getElementById(path + "span").classList.toggle("tree-caret-down"); - } - return; - } else { - $.ajax({ - type: "GET", - url: '/ajax/get_backup_dir?id=' + server_id + '&path=' + path, - dataType: 'text', - success: function (data) { - console.log("got response:"); - - dataArr = data.split('\n'); - serverDir = dataArr.shift(); // Remove & return first element (server directory) - text = dataArr.join('\n'); - - try { + }else{ + try { document.getElementById(path + "span").classList.add('tree-caret-down'); document.getElementById(path).innerHTML += text; document.getElementById(path).classList.add("clicked"); @@ -646,7 +659,7 @@ console.log("Bad") } - var toggler = document.getElementById(path); + var toggler = document.getElementById(path + "span"); if (toggler.classList.contains('files-tree-title')) { document.getElementById(path + "span").addEventListener("click", function caretListener() { @@ -654,10 +667,15 @@ document.getElementById(path + "span").classList.toggle("tree-caret-down"); }); } - }, - }); } } + + function getToggleMain(event) { + path = event.target.parentElement.getAttribute('data-path'); + document.getElementById("files-tree").classList.toggle("d-block"); + document.getElementById(path + "span").classList.toggle("tree-caret-down"); + document.getElementById(path + "span").classList.toggle("tree-caret"); + } function show_file_tree() { $("#dir_select").modal(); } From 768d3c0d14a81e9d5f5c32941ff5479b90367e8a Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Sun, 4 Jun 2023 20:55:48 -0400 Subject: [PATCH 006/124] Move files request to files API --- app/classes/web/routes/api/api_handlers.py | 8 ++++---- .../web/routes/api/servers/server/{backups => }/files.py | 2 +- app/frontend/templates/panel/server_backup.html | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename app/classes/web/routes/api/servers/server/{backups => }/files.py (98%) diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index eb5d1297..249899ef 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -32,8 +32,8 @@ from app.classes.web.routes.api.servers.server.backups.index import ( from app.classes.web.routes.api.servers.server.backups.backup.index import ( ApiServersServerBackupsBackupIndexHandler, ) -from app.classes.web.routes.api.servers.server.backups.files import ( - ApiServersServerBackupsFilesIndexHandler, +from app.classes.web.routes.api.servers.server.files import ( + ApiServersServerFilesIndexHandler, ) from app.classes.web.routes.api.servers.server.tasks.task.children import ( ApiServersServerTasksTaskChildrenHandler, @@ -132,8 +132,8 @@ def api_handlers(handler_args): handler_args, ), ( - r"/api/v2/servers/([0-9]+)/backups/files/?", - ApiServersServerBackupsFilesIndexHandler, + r"/api/v2/servers/([0-9]+)/files/?", + ApiServersServerFilesIndexHandler, handler_args, ), ( diff --git a/app/classes/web/routes/api/servers/server/backups/files.py b/app/classes/web/routes/api/servers/server/files.py similarity index 98% rename from app/classes/web/routes/api/servers/server/backups/files.py rename to app/classes/web/routes/api/servers/server/files.py index 44945409..a6baf1d6 100644 --- a/app/classes/web/routes/api/servers/server/backups/files.py +++ b/app/classes/web/routes/api/servers/server/files.py @@ -21,7 +21,7 @@ files_get_schema = { } -class ApiServersServerBackupsFilesIndexHandler(BaseApiHandler): +class ApiServersServerFilesIndexHandler(BaseApiHandler): def post(self, server_id: str): auth_data = self.authenticate_user() if not auth_data: diff --git a/app/frontend/templates/panel/server_backup.html b/app/frontend/templates/panel/server_backup.html index 195eb4d9..4061d58d 100644 --- a/app/frontend/templates/panel/server_backup.html +++ b/app/frontend/templates/panel/server_backup.html @@ -588,7 +588,7 @@ } async function getTreeView(path){ var token = getCookie("_xsrf"); - let res = await fetch(`/api/v2/servers/${server_id}/backups/files`, { + let res = await fetch(`/api/v2/servers/${server_id}/files`, { method: 'POST', headers: { 'X-XSRFToken': token From 2e58a2adc109ccb6f4edbe4a712347800cd25871 Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Sun, 4 Jun 2023 23:08:27 -0400 Subject: [PATCH 007/124] Remove files from ajax --- app/classes/shared/helpers.py | 81 ---------- app/classes/web/ajax_handler.py | 18 --- .../templates/panel/server_files.html | 145 ++++++++++-------- 3 files changed, 83 insertions(+), 161 deletions(-) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 65bc853a..e150cd0b 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -1092,87 +1092,6 @@ class Helpers: return data - def generate_tree(self, folder, output=""): - dir_list = [] - unsorted_files = [] - file_list = os.listdir(folder) - for item in file_list: - if os.path.isdir(os.path.join(folder, item)): - dir_list.append(item) - elif str(item) != self.ignored_names: - unsorted_files.append(item) - file_list = sorted(dir_list, key=str.casefold) + sorted( - unsorted_files, key=str.casefold - ) - for raw_filename in file_list: - filename = html.escape(raw_filename) - rel = os.path.join(folder, raw_filename) - dpath = os.path.join(folder, filename) - if os.path.isdir(rel): - if filename not in self.ignored_names: - output += f"""
    • - \n
      - - - - {filename} - -
    • - \n""" - else: - if filename not in self.ignored_names: - output += f"""
    • - {filename}
    • """ - return output - - def generate_dir(self, folder, output=""): - dir_list = [] - unsorted_files = [] - file_list = os.listdir(folder) - for item in file_list: - if os.path.isdir(os.path.join(folder, item)): - dir_list.append(item) - elif str(item) != self.ignored_names: - unsorted_files.append(item) - file_list = sorted(dir_list, key=str.casefold) + sorted( - unsorted_files, key=str.casefold - ) - output += f"""
        """ - for raw_filename in file_list: - filename = html.escape(raw_filename) - dpath = os.path.join(folder, filename) - rel = os.path.join(folder, raw_filename) - if os.path.isdir(rel): - if filename not in self.ignored_names: - output += f"""
      • - \n
        - - - - {filename} - -
      • """ - else: - if filename not in self.ignored_names: - output += f"""
      • - {filename}
      • """ - output += "
      \n" - return output - @staticmethod def generate_zip_tree(folder, output=""): file_list = os.listdir(folder) diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index 9419d82d..f3b22d32 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -108,24 +108,6 @@ class AjaxHandler(BaseHandler): ) self.finish() - elif page == "get_dir": - server_id = self.get_argument("id", None) - path = self.get_argument("path", None) - - if not self.check_server_id(server_id, "get_tree"): - return - server_id = bleach.clean(server_id) - - if Helpers.validate_traversal( - self.controller.servers.get_server_data_by_id(server_id)["path"], path - ): - self.write( - Helpers.get_os_understandable_path(path) - + "\n" - + Helpers.generate_dir(path) - ) - self.finish() - @tornado.web.authenticated def post(self, page): api_key, _, exec_user = self.current_user diff --git a/app/frontend/templates/panel/server_files.html b/app/frontend/templates/panel/server_files.html index 7dbcebb6..f3fa1179 100644 --- a/app/frontend/templates/panel/server_files.html +++ b/app/frontend/templates/panel/server_files.html @@ -882,68 +882,86 @@ }); } - function getTreeView(event) { - const path = $('#root_dir').data('path');; + function getDirView(event){ + let path = event.target.parentElement.getAttribute("data-path"); + if (document.getElementById(path).classList.contains('clicked')) { + return; + }else{ + getTreeView(path); + } - $.ajax({ - type: "GET", - url: "/files/get_tree?id=" + serverId + "&path=" + path, - dataType: 'text', - success: function (data) { - console.log("got response:"); + } + async function getTreeView(path){ + var token = getCookie("_xsrf"); + let res = await fetch(`/api/v2/servers/${serverId}/files`, { + method: 'POST', + headers: { + 'X-XSRFToken': token + }, + body: JSON.stringify({"page": "files", "folder": path}), + }); + let responseData = await res.json(); + if (responseData.status === "ok") { + console.log(responseData); + process_tree_response(responseData); - dataArr = data.split('\n'); - serverDir = dataArr.shift(); // Remove & return first element (server directory) - text = dataArr.join('\n'); + } else { - try { - document.getElementById(path).innerHTML += text; - event.target.parentElement.classList.add("clicked"); + bootbox.alert({ + title: responseData.status, + message: responseData.error + }); + } + } + + function process_tree_response(response) { + let path = response.data.root_path.path; + let text = ``; + if (!response.data.root_path.top){ + text = `
        `; + } + Object.entries(response.data).forEach(([key, value]) => { + if (key === "root_path" || key === "db_stats"){ + //continue is not valid in for each. Return acts as a continue. + return; + } + let checked = "" + let dpath = value.path; + let filename = key; + if (value.dir){ + if (value.excluded){ + checked = "checked" + } + text += `
      • + \n
        + + + + + ${filename} + +
      • ` + }else{ + text += `
      • + ${filename}
      • ` + } + }); + if (! response.data.root_path.top){ + text += `
      `; + } + if(response.data.root_path.top){ + try { + document.getElementById('main-tree-div').innerHTML += text; + document.getElementById('main-tree').parentElement.classList.add("clicked"); } catch { document.getElementById('files-tree').innerHTML = text; } - - - document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir); - document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files'); - - setTimeout(function () { setTreeViewContext() }, 1000); - }, - }); - } - - function getToggleMain(event) { - path = event.target.parentElement.getAttribute('data-path'); - document.getElementById("files-tree").classList.toggle("d-block"); - document.getElementById(path + "span").classList.toggle("tree-caret-down"); - document.getElementById(path + "span").classList.toggle("tree-caret"); - } - - function getDirView(event) { - let path = event.target.parentElement.getAttribute('data-path'); - - if (document.getElementById(path).classList.contains('clicked')) { - - var toggler = document.getElementById(path + "span"); - - if (toggler.classList.contains('files-tree-title')) { - document.getElementById(path + "ul").classList.toggle("d-block"); - document.getElementById(path + "span").classList.toggle("tree-caret-down"); - } - return; - } else { - $.ajax({ - type: "GET", - url: "/files/get_dir?id=" + serverId + "&path=" + path, - dataType: 'text', - success: function (data) { - console.log("got response:"); - - dataArr = data.split('\n'); - serverDir = dataArr.shift(); // Remove & return first element (server directory) - text = dataArr.join('\n'); - - try { + }else{ + try { document.getElementById(path + "span").classList.add('tree-caret-down'); document.getElementById(path).innerHTML += text; document.getElementById(path).classList.add("clicked"); @@ -951,9 +969,7 @@ console.log("Bad") } - setTimeout(function () { setTreeViewContext() }, 1000); - - var toggler = document.getElementById(path); + var toggler = document.getElementById(path + "span"); if (toggler.classList.contains('files-tree-title')) { document.getElementById(path + "span").addEventListener("click", function caretListener() { @@ -961,9 +977,14 @@ document.getElementById(path + "span").classList.toggle("tree-caret-down"); }); } - }, - }); } + setTimeout(function () { setTreeViewContext() }, 1000); + } + function getToggleMain(event) { + path = event.target.parentElement.getAttribute('data-path'); + document.getElementById("files-tree").classList.toggle("d-block"); + document.getElementById(path + "span").classList.toggle("tree-caret-down"); + document.getElementById(path + "span").classList.toggle("tree-caret"); } function setTreeViewContext() { @@ -1172,7 +1193,7 @@ }); } - getTreeView(); + getTreeView($('#root_dir').data('path')); setTreeViewContext(); function setKeyboard(target) { From 4ef31864cadee5a0d9679ff494e8283d09f6d4dd Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Mon, 5 Jun 2023 18:03:06 -0400 Subject: [PATCH 008/124] Add delete file --- .../web/routes/api/servers/server/files.py | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/app/classes/web/routes/api/servers/server/files.py b/app/classes/web/routes/api/servers/server/files.py index a6baf1d6..466e5292 100644 --- a/app/classes/web/routes/api/servers/server/files.py +++ b/app/classes/web/routes/api/servers/server/files.py @@ -6,6 +6,7 @@ from jsonschema import validate from jsonschema.exceptions import ValidationError from app.classes.models.server_permissions import EnumPermissionsServer from app.classes.shared.helpers import Helpers +from app.classes.shared.file_helpers import FileHelpers from app.classes.web.base_api_handler import BaseApiHandler logger = logging.getLogger(__name__) @@ -20,6 +21,15 @@ files_get_schema = { "minProperties": 1, } +file_delete_schema = { + "type": "object", + "properties": { + "filename": {"type": "string", "minLength": 5}, + }, + "additionalProperties": False, + "minProperties": 1, +} + class ApiServersServerFilesIndexHandler(BaseApiHandler): def post(self, server_id: str): @@ -32,12 +42,16 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler): return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) if ( - EnumPermissionsServer.BACKUP + EnumPermissionsServer.FILES not in self.controller.server_perms.get_user_id_permissions_list( auth_data[4]["user_id"], server_id ) + or EnumPermissionsServer.BACKUP + in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) ): - # if the user doesn't have Config permission, return an error + # if the user doesn't have Files or Backup permission, return an error return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) try: @@ -123,3 +137,56 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler): "excluded": False, } self.finish_json(200, {"status": "ok", "data": return_json}) + + def delete(self, server_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + + if server_id not in [str(x["server_id"]) for x in auth_data[0]]: + # if the user doesn't have access to the server, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + if ( + EnumPermissionsServer.FILES + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Files permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + try: + validate(data, file_delete_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + if not Helpers.validate_traversal( + self.controller.servers.get_server_data_by_id(server_id)["path"], + data["filename"], + ): + return self.finish_json( + 400, + { + "status": "error", + "error": "TRAVERSAL DETECTED", + "error_data": str(e), + }, + ) + + if os.path.isdir(data["filename"]): + FileHelpers.del_dirs(data["filename"]) + else: + FileHelpers.del_file(data["filename"]) + return self.finish_json(200, {"status": "ok"}) From ad7ba9d6a71ccae181385c872c7af00d5f8e406e Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Mon, 5 Jun 2023 18:20:37 -0400 Subject: [PATCH 009/124] Fix logical issue --- app/classes/web/routes/api/servers/server/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/classes/web/routes/api/servers/server/files.py b/app/classes/web/routes/api/servers/server/files.py index 466e5292..becd3681 100644 --- a/app/classes/web/routes/api/servers/server/files.py +++ b/app/classes/web/routes/api/servers/server/files.py @@ -46,7 +46,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler): not in self.controller.server_perms.get_user_id_permissions_list( auth_data[4]["user_id"], server_id ) - or EnumPermissionsServer.BACKUP + or not EnumPermissionsServer.BACKUP in self.controller.server_perms.get_user_id_permissions_list( auth_data[4]["user_id"], server_id ) From c0fb5cf179277ac7ca0461cc0ef9e391feb71d4e Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Mon, 5 Jun 2023 18:20:45 -0400 Subject: [PATCH 010/124] Add delete to front end --- .../templates/panel/server_files.html | 62 +++++++------------ 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/app/frontend/templates/panel/server_files.html b/app/frontend/templates/panel/server_files.html index f3fa1179..dcc315a4 100644 --- a/app/frontend/templates/panel/server_files.html +++ b/app/frontend/templates/panel/server_files.html @@ -648,37 +648,27 @@ }); } - function deleteFile(path, callback) { - console.log('Deleting: ' + path) - var token = getCookie("_xsrf") - $.ajax({ - type: "DELETE", - headers: { 'X-XSRFToken': token }, - url: "/files/del_file?id=" + serverId, - data: { - file_path: path - }, - success: function (data) { - console.log("got response:"); - callback(); - }, - }); - } + async function deleteItem(path, el, callback) { + var token = getCookie("_xsrf"); + let res = await fetch(`/api/v2/servers/${serverId}/files`, { + method: 'DELETE', + headers: { + 'X-XSRFToken': token + }, + body: JSON.stringify({"filename": path}), + }); + let responseData = await res.json(); + if (responseData.status === "ok") { + el = document.getElementById(path + "li"); + $(el).remove(); + document.getElementById('files-tree-nav').style.display = 'none'; + } else { - function deleteDir(path, callback) { - var token = getCookie("_xsrf") - $.ajax({ - type: "DELETE", - headers: { 'X-XSRFToken': token }, - url: "/files/del_dir?id=" + serverId, - data: { - dir_path: path - }, - success: function (data) { - console.log("got response:"); - callback(); - }, - }); + bootbox.alert({ + title: responseData.status, + message: responseData.error + }); + } } function unZip(path, callback) { @@ -1155,11 +1145,7 @@ }, callback: function (result) { if (!result) return; - deleteFile(path, function () { - el = document.getElementById(path + "li"); - $(el).remove(); - document.getElementById('files-tree-nav').style.display = 'none'; - }); + deleteItem(path); } }); } @@ -1184,11 +1170,7 @@ }, callback: function (result) { if (!result) return; - deleteDir(path, function () { - el = document.getElementById(path + "li"); - $(el).remove(); - document.getElementById('files-tree-nav').style.display = 'none'; - }); + deleteItem(path); } }); } From 7f8feb859110fc4105422ed3a5370b3724e0eb92 Mon Sep 17 00:00:00 2001 From: bobsfriend12 Date: Wed, 5 Jul 2023 15:34:35 -0500 Subject: [PATCH 011/124] get rid of errors in console log --- app/frontend/templates/panel/denied.html | 220 ++++++++++-------- app/frontend/templates/public/login.html | 2 +- app/frontend/templates/public_base.html | 269 +++++++++++++---------- 3 files changed, 278 insertions(+), 213 deletions(-) diff --git a/app/frontend/templates/panel/denied.html b/app/frontend/templates/panel/denied.html index d4548c05..28afd7b0 100644 --- a/app/frontend/templates/panel/denied.html +++ b/app/frontend/templates/panel/denied.html @@ -1,106 +1,142 @@ + + + + + Crafty Controller + + + + + + - - - - - Crafty Controller - - - - - - + + + + + - - - - - - + + + + + + + + + + - - - - - - - - - - - - - -
      -
      -
      -
      -
      - -
      -
      -

      -
      -