diff --git a/.gitlab/docker-build.yml b/.gitlab/docker-build.yml index 4c906e4e..aa578e97 100644 --- a/.gitlab/docker-build.yml +++ b/.gitlab/docker-build.yml @@ -28,7 +28,7 @@ docker-build-dev: docker version - docker run --rm --privileged aptman/qus -- -r - docker run --rm --privileged aptman/qus -s -- -p aarch64 x86_64 - - echo $CI_BUILD_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY + - echo $CI_JOB_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY - echo $DOCKERHUB_TOKEN | docker login -u "$DOCKERHUB_USER" --password-stdin $DOCKERHUB_REGISTRY script: - | @@ -45,6 +45,7 @@ docker-build-dev: --build-arg "BUILD_DATE=$(date +"%Y-%m-%dT%H:%M:%SZ")" --build-arg "BUILD_REF=${CI_COMMIT_SHA}" --build-arg "CRAFTY_VER=${VERSION}" + --provenance false --tag "$CI_REGISTRY_IMAGE${tag}" --tag "arcadiatechnology/crafty-4${tag}" --platform linux/arm64/v8,linux/amd64 @@ -84,7 +85,7 @@ docker-build-prod: docker version - docker run --rm --privileged aptman/qus -- -r - docker run --rm --privileged aptman/qus -s -- -p aarch64 x86_64 - - echo $CI_BUILD_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY + - echo $CI_JOB_TOKEN | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY - echo $DOCKERHUB_TOKEN | docker login -u "$DOCKERHUB_USER" --password-stdin $DOCKERHUB_REGISTRY script: - | @@ -100,6 +101,7 @@ docker-build-prod: --build-arg "BUILD_DATE=$(date +"%Y-%m-%dT%H:%M:%SZ")" --build-arg "BUILD_REF=${CI_COMMIT_SHA}" --build-arg "CRAFTY_VER=${VERSION}" + --provenance false --tag "$CI_REGISTRY_IMAGE:$VERSION" --tag "$CI_REGISTRY_IMAGE:latest" --tag "arcadiatechnology/crafty-4:$VERSION" diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f813fc..9466965d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ # Changelog -## --- [4.0.23] - 2023/TBD +## --- [4.1.0] - 2023/TBD ### New features TBD +### Refactor +- Frontend Ajax Refactor | Start using API to send Remote Comms to Server ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/565)) ### Bug fixes -TBD +- Fix pipelines failing to build from gitlab pre-defined variable deprecation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/582)) +- Fix incompatible buildx provenance meta, causing digest issues on GL/DH container registries ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/582)) ### Tweaks TBD ### Lang diff --git a/README.md b/README.md index a351e6c0..637b6a7a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com) -# Crafty Controller 4.0.23 +# Crafty Controller 4.1.0 > Python based Control Panel for your Minecraft Server ## What is Crafty Controller? diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index ec508edf..029518b6 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -278,11 +278,10 @@ class ServersController(metaclass=Singleton): for role in roles_list: role_users = HelperUsers.get_users_from_role(role.role_id) for user_role in role_users: - user_ids.add(user_role.user_id) + user_ids.add(user_role.user_id.user_id) for user_id in HelperUsers.get_super_user_list(): user_ids.add(user_id) - return user_ids def get_all_servers_stats(self): diff --git a/app/classes/controllers/users_controller.py b/app/classes/controllers/users_controller.py index b9c019e8..667e01b4 100644 --- a/app/classes/controllers/users_controller.py +++ b/app/classes/controllers/users_controller.py @@ -89,6 +89,7 @@ class UsersController: }, }, "hints": {"type": "boolean"}, + "server_order": {"type": "string"}, } # ********************************************************************************** diff --git a/app/classes/models/users.py b/app/classes/models/users.py index 496e8d2c..b0612017 100644 --- a/app/classes/models/users.py +++ b/app/classes/models/users.py @@ -386,7 +386,7 @@ class HelperUsers: @staticmethod def get_users_from_role(role_id): - UserRoles.select().where(UserRoles.role_id == role_id).execute() + return UserRoles.select().where(UserRoles.role_id == role_id).execute() # ********************************************************************************** # ApiKeys Methods diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 9efb8b0a..b955db26 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -16,6 +16,7 @@ import zipfile import pathlib import ctypes import shutil +import shlex import subprocess import itertools from datetime import datetime @@ -147,6 +148,29 @@ class Helpers: logger.error(f"Unable to resolve remote bedrock download url! \n{e}") return False + def get_execution_java(self, value, execution_command): + if self.is_os_windows(): + execution_list = shlex.split(execution_command, posix=False) + else: + execution_list = shlex.split(execution_command, posix=True) + if ( + not any(value in path for path in self.find_java_installs()) + and value != "java" + ): + return + if value != "java": + if self.is_os_windows(): + execution_list[0] = '"' + value + '/bin/java"' + else: + execution_list[0] = '"' + value + '"' + else: + execution_list[0] = "java" + execution_command = "" + for item in execution_list: + execution_command += item + " " + + return execution_command + def detect_java(self): if len(self.find_java_installs()) > 0: return True diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index dccaf4e3..50384171 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -421,6 +421,7 @@ class TasksManager: ) for item in jobs: logger.info(f"JOB: {item}") + return task.schedule_id def remove_all_server_tasks(self, server_id): schedules = HelpersManagement.get_schedules_by_server(server_id) @@ -450,7 +451,6 @@ class TasksManager: # created task a child of itself. if str(job_data.get("parent")) == str(sch_id): job_data["parent"] = None - HelpersManagement.update_scheduled_task(sch_id, job_data) if not ( diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index cd1ccc04..0dde06a1 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -281,74 +281,7 @@ class AjaxHandler(BaseHandler): 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") - - srv_obj = self.controller.servers.get_server_instance_by_id(server_id) - - if command == srv_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 srv_obj.check_running(): - srv_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": - 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 page == "select_photo": if exec_user["superuser"]: photo = urllib.parse.unquote(self.get_argument("photo", "")) opacity = self.get_argument("opacity", 100) @@ -382,23 +315,6 @@ class AjaxHandler(BaseHandler): self.controller.cached_login = "login_1.jpg" return - elif page == "kill": - if not permissions["Commands"] in user_perms: - if not superuser: - self.redirect("/panel/error?error=Unauthorized access to Commands") - return - server_id = self.get_argument("id", None) - svr = self.controller.servers.get_server_instance_by_id(server_id) - try: - svr.kill() - time.sleep(5) - svr.cleanup_server_object() - svr.record_server_stats() - except Exception as e: - logger.error( - f"Could not find PID for requested termsig. Full error: {e}" - ) - return elif page == "eula": server_id = self.get_argument("id", None) svr = self.controller.servers.get_server_instance_by_id(server_id) @@ -624,12 +540,6 @@ class AjaxHandler(BaseHandler): user_perms = self.controller.server_perms.get_user_id_permissions_list( exec_user["user_id"], server_id ) - if page == "del_task": - if not permissions["Schedule"] in user_perms: - self.redirect("/panel/error?error=Unauthorized access to Tasks") - else: - sch_id = self.get_argument("schedule_id", "-404") - self.tasks_manager.remove_job(sch_id) if page == "del_backup": if not permissions["Backup"] in user_perms: @@ -668,84 +578,6 @@ class AjaxHandler(BaseHandler): ): os.remove(file_path) - elif page == "delete_server": - if not permissions["Config"] in user_perms: - if not superuser: - self.redirect("/panel/error?error=Unauthorized access to Config") - return - server_id = self.get_argument("id", None) - logger.info( - f"Removing server from panel for server: " - f"{self.controller.servers.get_server_friendly_name(server_id)}" - ) - - server_data = self.controller.servers.get_server_data(server_id) - server_name = server_data["server_name"] - - self.controller.management.add_to_audit_log( - exec_user["user_id"], - f"Deleted server {server_id} named {server_name}", - server_id, - self.get_remote_ip(), - ) - - self.tasks_manager.remove_all_server_tasks(server_id) - self.controller.remove_server(server_id, False) - - elif page == "delete_server_files": - if not permissions["Config"] in user_perms: - if not superuser: - self.redirect("/panel/error?error=Unauthorized access to Config") - return - server_id = self.get_argument("id", None) - logger.info( - f"Removing server and all associated files for server: " - f"{self.controller.servers.get_server_friendly_name(server_id)}" - ) - - server_data = self.controller.servers.get_server_data(server_id) - server_name = server_data["server_name"] - - self.controller.management.add_to_audit_log( - exec_user["user_id"], - f"Deleted server {server_id} named {server_name}", - server_id, - self.get_remote_ip(), - ) - - for server in self.controller.servers.failed_servers: - if server["server_id"] == int(server_id): - return - self.tasks_manager.remove_all_server_tasks(server_id) - self.controller.remove_server(server_id, True) - - elif page == "delete_unloaded_server": - if not permissions["Config"] in user_perms: - if not superuser: - self.redirect("/panel/error?error=Unauthorized access to Config") - return - server_id = self.get_argument("id", None) - logger.info( - f"Removing server and all associated files for server: " - f"{self.controller.servers.get_server_friendly_name(server_id)}" - ) - - server_data = self.controller.servers.get_server_data_by_id(server_id) - server_name = server_data["server_name"] - - self.controller.management.add_to_audit_log( - exec_user["user_id"], - f"Deleted server {server_id} named {server_name}", - server_id, - self.get_remote_ip(), - ) - - self.tasks_manager.remove_all_server_tasks(server_id) - for item in self.controller.servers.failed_servers[:]: - if item["server_id"] == int(server_id): - self.controller.servers.failed_servers.remove(item) - self.controller.remove_unloaded_server(server_id) - def check_server_id(self, server_id, page_name): if server_id is None: logger.warning( diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 0883ecef..b7219ac6 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -6,7 +6,6 @@ import typing as t import json import logging import threading -import shlex import urllib.parse import bleach import requests @@ -17,7 +16,6 @@ from tornado import iostream # TZLocal is set as a hidden import on win pipeline from tzlocal import get_localzone from tzlocal.utils import ZoneInfoNotFoundError -from croniter import croniter from app.classes.models.servers import Servers from app.classes.models.server_permissions import EnumPermissionsServer @@ -1052,7 +1050,7 @@ class PanelHandler(BaseHandler): page_data["schedule"]["cron_string"] = "" page_data["schedule"]["delay"] = 0 page_data["schedule"]["time"] = "" - page_data["schedule"]["interval"] = "" + page_data["schedule"]["interval"] = 1 # we don't need to check difficulty here. # We'll just default to basic for new schedules page_data["schedule"]["difficulty"] = "basic" @@ -1555,156 +1553,6 @@ class PanelHandler(BaseHandler): role = self.controller.roles.get_role(r) exec_user_role.add(role["role_name"]) - if page == "server_detail": - if not permissions[ - "Config" - ] in self.controller.server_perms.get_user_id_permissions_list( - exec_user["user_id"], server_id - ): - if not superuser: - self.redirect("/panel/error?error=Unauthorized access to Config") - return - server_name = self.get_argument("server_name", None) - server_obj = self.controller.servers.get_server_obj(server_id) - shutdown_timeout = self.get_argument("shutdown_timeout", 60) - if superuser: - log_path = self.get_argument("log_path", "") - if log_path: - if Helpers.is_os_windows(): - log_path.replace(" ", "^ ") - log_path = Helpers.wtol_path(log_path) - if not self.helper.validate_traversal(server_obj.path, log_path): - log_path = "" - executable = self.get_argument("executable", None) - execution_command = self.get_argument("execution_command", None) - server_ip = self.get_argument("server_ip", None) - server_port = self.get_argument("server_port", None) - if int(server_port) < 1 or int(server_port) > 65535: - self.redirect( - "/panel/error?error=Constraint Error: " - "Port must be greater than 0 and less than 65535" - ) - return - executable_update_url = self.get_argument("executable_update_url", "") - show_status = int(float(self.get_argument("show_status", "0"))) - else: - execution_command = server_obj.execution_command - executable = server_obj.executable - stop_command = self.get_argument("stop_command", None) - auto_start_delay = self.get_argument("auto_start_delay", "10") - auto_start = int(float(self.get_argument("auto_start", "0"))) - crash_detection = int(float(self.get_argument("crash_detection", "0"))) - logs_delete_after = int(float(self.get_argument("logs_delete_after", "0"))) - java_selection = self.get_argument("java_selection", None) - # make sure there is no whitespace - ignored_exits = self.get_argument("ignored_exits", "").replace(" ", "") - # subpage = self.get_argument('subpage', None) - - server_id = self.check_server_id() - if server_id is None: - return - if java_selection: - try: - if self.helper.is_os_windows(): - execution_list = shlex.split(execution_command, posix=False) - else: - execution_list = shlex.split(execution_command, posix=True) - except ValueError: - self.redirect( - "/panel/error?error=Invalid execution command. Java path" - " must be surrounded by quotes." - " (Are you missing a closing quote?)" - ) - if ( - not any( - java_selection in path for path in Helpers.find_java_installs() - ) - and java_selection != "java" - ): - self.redirect( - "/panel/error?error=Attack attempted." - + " A copy of this report is being sent to server owner." - ) - self.controller.management.add_to_audit_log_raw( - exec_user["username"], - exec_user["user_id"], - server_id, - f"Attempted to send bad java path for {server_id}." - + " Possible attack. Act accordingly.", - self.get_remote_ip(), - ) - return - if java_selection != "java": - if self.helper.is_os_windows(): - execution_list[0] = '"' + java_selection + '/bin/java"' - else: - execution_list[0] = '"' + java_selection + '"' - else: - execution_list[0] = "java" - execution_command = "" - for item in execution_list: - execution_command += item + " " - - server_obj: Servers = self.controller.servers.get_server_obj(server_id) - stale_executable = server_obj.executable - # Compares old jar name to page data being passed. - # If they are different we replace the executable name in the - if str(stale_executable) != str(executable): - execution_command = execution_command.replace( - str(stale_executable), str(executable) - ) - - server_obj.server_name = server_name - server_obj.shutdown_timeout = shutdown_timeout - if superuser: - if Helpers.validate_traversal( - self.helper.get_servers_root_dir(), server_obj.path - ): - server_obj.log_path = log_path - if Helpers.validate_traversal( - self.helper.get_servers_root_dir(), executable - ): - server_obj.executable = executable - server_obj.execution_command = execution_command - server_obj.server_ip = server_ip - server_obj.server_port = server_port - server_obj.executable_update_url = executable_update_url - server_obj.show_status = show_status - else: - server_obj.log_path = server_obj.log_path - server_obj.executable = server_obj.executable - server_obj.execution_command = execution_command - server_obj.server_ip = server_obj.server_ip - server_obj.server_port = server_obj.server_port - server_obj.executable_update_url = server_obj.executable_update_url - server_obj.stop_command = stop_command - server_obj.auto_start_delay = auto_start_delay - server_obj.auto_start = auto_start - server_obj.crash_detection = crash_detection - server_obj.logs_delete_after = logs_delete_after - server_obj.ignored_exits = ignored_exits - failed = False - for servers in self.controller.servers.failed_servers: - if servers["server_id"] == int(server_id): - failed = True - if not failed: - self.controller.servers.update_server(server_obj) - else: - self.controller.servers.update_unloaded_server(server_obj) - self.controller.servers.init_all_servers() - self.controller.servers.crash_detection(server_obj) - - self.controller.servers.refresh_server_settings(server_id) - - self.controller.management.add_to_audit_log( - exec_user["user_id"], - f"Edited server {server_id} named {server_name}", - server_id, - self.get_remote_ip(), - ) - - self.redirect(f"/panel/server_detail?id={server_id}&subpage=config") - if page == "server_backup": logger.debug(self.request.arguments) @@ -1801,336 +1649,6 @@ class PanelHandler(BaseHandler): self.redirect("/panel/config_json") - if page == "new_schedule": - server_id = self.check_server_id() - if not server_id: - return - - if ( - not permissions["Schedule"] - in self.controller.server_perms.get_user_id_permissions_list( - exec_user["user_id"], server_id - ) - and not superuser - ): - self.redirect( - "/panel/error?error=Unauthorized access: User not authorized" - ) - return - - difficulty = bleach.clean(self.get_argument("difficulty", None)) - server_obj = self.controller.servers.get_server_obj(server_id) - enabled = bleach.clean(self.get_argument("enabled", "0")) - name = bleach.clean(self.get_argument("name", "")) - if difficulty == "basic": - action = bleach.clean(self.get_argument("action", None)) - interval = bleach.clean(self.get_argument("interval", None)) - interval_type = bleach.clean(self.get_argument("interval_type", None)) - # only check for time if it's number of days - if interval_type == "days": - sch_time = bleach.clean(self.get_argument("time", None)) - if int(interval) > 30: - self.redirect( - "/panel/error?error=Invalid argument." - " Days must be 30 or fewer." - ) - return - if action == "command": - command = self.get_argument("command", None) - elif action == "start": - command = "start_server" - elif action == "stop": - command = "stop_server" - elif action == "restart": - command = "restart_server" - elif action == "backup": - command = "backup_server" - - elif difficulty == "reaction": - interval_type = "reaction" - action = bleach.clean(self.get_argument("action", None)) - delay = bleach.clean(self.get_argument("delay", None)) - parent = bleach.clean(self.get_argument("parent", None)) - if action == "command": - command = self.get_argument("command", None) - elif action == "start": - command = "start_server" - elif action == "stop": - command = "stop_server" - elif action == "restart": - command = "restart_server" - elif action == "backup": - command = "backup_server" - - else: - interval_type = "" - cron_string = bleach.clean(self.get_argument("cron", "")) - if not croniter.is_valid(cron_string): - self.redirect( - "/panel/error?error=INVALID FORMAT: Invalid Cron Format." - ) - return - action = bleach.clean(self.get_argument("action", None)) - if action == "command": - command = self.get_argument("command", None) - elif action == "start": - command = "start_server" - elif action == "stop": - command = "stop_server" - elif action == "restart": - command = "restart_server" - elif action == "backup": - command = "backup_server" - if bleach.clean(self.get_argument("enabled", "0")) == "1": - enabled = True - else: - enabled = False - if bleach.clean(self.get_argument("one_time", "0")) == "1": - one_time = True - else: - one_time = False - - if interval_type == "days": - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": interval_type, - "interval": interval, - "command": command, - "start_time": sch_time, - "enabled": enabled, - "one_time": one_time, - "cron_string": "", - "parent": None, - "delay": 0, - } - elif difficulty == "reaction": - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": interval_type, - "interval": "", - # We'll base every interval off of a midnight start time. - "start_time": "", - "command": command, - "cron_string": "", - "enabled": enabled, - "one_time": one_time, - "parent": parent, - "delay": delay, - } - elif difficulty == "advanced": - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": "", - "interval": "", - # We'll base every interval off of a midnight start time. - "start_time": "", - "command": command, - "cron_string": cron_string, - "enabled": enabled, - "one_time": one_time, - "parent": None, - "delay": 0, - } - else: - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": interval_type, - "interval": interval, - "command": command, - "enabled": enabled, - # We'll base every interval off of a midnight start time. - "start_time": "00:00", - "one_time": one_time, - "cron_string": "", - "parent": None, - "delay": 0, - } - - self.tasks_manager.schedule_job(job_data) - - self.controller.management.add_to_audit_log( - exec_user["user_id"], - f"Edited server {server_id}: added scheduled job", - server_id, - self.get_remote_ip(), - ) - self.tasks_manager.reload_schedule_from_db() - self.redirect(f"/panel/server_detail?id={server_id}&subpage=schedules") - - if page == "edit_schedule": - server_id = self.check_server_id() - if not server_id: - return - - if ( - not permissions["Schedule"] - in self.controller.server_perms.get_user_id_permissions_list( - exec_user["user_id"], server_id - ) - and not superuser - ): - self.redirect( - "/panel/error?error=Unauthorized access: User not authorized" - ) - return - - sch_id = self.get_argument("sch_id", None) - if sch_id is None: - self.redirect("/panel/error?error=Invalid Schedule ID") - - difficulty = bleach.clean(self.get_argument("difficulty", None)) - server_obj = self.controller.servers.get_server_obj(server_id) - enabled = bleach.clean(self.get_argument("enabled", "0")) - name = bleach.clean(self.get_argument("name", "")) - if difficulty == "basic": - action = bleach.clean(self.get_argument("action", None)) - interval = bleach.clean(self.get_argument("interval", None)) - interval_type = bleach.clean(self.get_argument("interval_type", None)) - # only check for time if it's number of days - if interval_type == "days": - sch_time = bleach.clean(self.get_argument("time", None)) - if int(interval) > 30: - self.redirect( - "/panel/error?error=Invalid argument." - " Days must be 30 or fewer." - ) - return - if action == "command": - command = self.get_argument("command", None) - elif action == "start": - command = "start_server" - elif action == "stop": - command = "stop_server" - elif action == "restart": - command = "restart_server" - elif action == "backup": - command = "backup_server" - elif difficulty == "reaction": - interval_type = "reaction" - action = bleach.clean(self.get_argument("action", None)) - delay = bleach.clean(self.get_argument("delay", None)) - parent = bleach.clean(self.get_argument("parent", None)) - if action == "command": - command = self.get_argument("command", None) - elif action == "start": - command = "start_server" - elif action == "stop": - command = "stop_server" - elif action == "restart": - command = "restart_server" - elif action == "backup": - command = "backup_server" - parent = bleach.clean(self.get_argument("parent", None)) - else: - interval_type = "" - cron_string = bleach.clean(self.get_argument("cron", "")) - if not croniter.is_valid(cron_string): - self.redirect( - "/panel/error?error=INVALID FORMAT: Invalid Cron Format." - ) - return - action = bleach.clean(self.get_argument("action", None)) - if action == "command": - command = self.get_argument("command", None) - elif action == "start": - command = "start_server" - elif action == "stop": - command = "stop_server" - elif action == "restart": - command = "restart_server" - elif action == "backup": - command = "backup_server" - if bleach.clean(self.get_argument("enabled", "0")) == "1": - enabled = True - else: - enabled = False - if bleach.clean(self.get_argument("one_time", "0")) == "1": - one_time = True - else: - one_time = False - - if interval_type == "days": - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": interval_type, - "interval": interval, - "command": command, - "start_time": sch_time, - "enabled": enabled, - "one_time": one_time, - "cron_string": "", - "parent": None, - "delay": 0, - } - elif difficulty == "advanced": - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": "", - "interval": "", - # We'll base every interval off of a midnight start time. - "start_time": "", - "command": command, - "cron_string": cron_string, - "delay": "", - "parent": "", - "enabled": enabled, - "one_time": one_time, - } - elif difficulty == "reaction": - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": interval_type, - "interval": "", - # We'll base every interval off of a midnight start time. - "start_time": "", - "command": command, - "cron_string": "", - "enabled": enabled, - "one_time": one_time, - "parent": parent, - "delay": delay, - } - else: - job_data = { - "name": name, - "server_id": server_id, - "action": action, - "interval_type": interval_type, - "interval": interval, - "command": command, - "enabled": enabled, - # We'll base every interval off of a midnight start time. - "start_time": "00:00", - "delay": "", - "parent": "", - "one_time": one_time, - "cron_string": "", - } - self.tasks_manager.update_job(sch_id, job_data) - - self.controller.management.add_to_audit_log( - exec_user["user_id"], - f"Edited server {server_id}: updated schedule", - server_id, - self.get_remote_ip(), - ) - self.tasks_manager.reload_schedule_from_db() - self.redirect(f"/panel/server_detail?id={server_id}&subpage=schedules") - elif page == "edit_user": if bleach.clean(self.get_argument("username", None)).lower() == "system": self.redirect( diff --git a/app/classes/web/routes/api/roles/role/index.py b/app/classes/web/routes/api/roles/role/index.py index c0601b5e..20354722 100644 --- a/app/classes/web/routes/api/roles/role/index.py +++ b/app/classes/web/routes/api/roles/role/index.py @@ -4,6 +4,36 @@ from peewee import DoesNotExist from app.classes.web.base_api_handler import BaseApiHandler modify_role_schema = { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + }, + "servers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "server_id": { + "type": "integer", + "minimum": 1, + }, + "permissions": { + "type": "string", + "pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer + }, + }, + "required": ["server_id", "permissions"], + }, + }, + "manager": {"type": ["integer", "null"]}, + }, + "additionalProperties": False, + "minProperties": 1, +} + +basic_modify_role_schema = { "type": "object", "properties": { "name": { @@ -109,7 +139,10 @@ class ApiRolesRoleIndexHandler(BaseApiHandler): ) try: - validate(data, modify_role_schema) + if auth_data[4]["superuser"]: + validate(data, modify_role_schema) + else: + validate(data, basic_modify_role_schema) except ValidationError as e: return self.finish_json( 400, diff --git a/app/classes/web/routes/api/servers/server/index.py b/app/classes/web/routes/api/servers/server/index.py index 3d5e3e2f..afe02a0b 100644 --- a/app/classes/web/routes/api/servers/server/index.py +++ b/app/classes/web/routes/api/servers/server/index.py @@ -13,20 +13,39 @@ server_patch_schema = { "type": "object", "properties": { "server_name": {"type": "string", "minLength": 1}, - "path": {"type": "string", "minLength": 1}, "backup_path": {"type": "string"}, "executable": {"type": "string"}, "log_path": {"type": "string", "minLength": 1}, "execution_command": {"type": "string", "minLength": 1}, + "java_selection": {"type": "string"}, "auto_start": {"type": "boolean"}, - "auto_start_delay": {"type": "integer"}, + "auto_start_delay": {"type": "integer", "minimum": 0}, "crash_detection": {"type": "boolean"}, "stop_command": {"type": "string"}, - "executable_update_url": {"type": "string", "minLength": 1}, + "executable_update_url": {"type": "string"}, "server_ip": {"type": "string", "minLength": 1}, "server_port": {"type": "integer"}, - "logs_delete_after": {"type": "integer"}, - "type": {"type": "string", "minLength": 1}, + "shutdown_timeout": {"type": "integer", "minimum": 0}, + "logs_delete_after": {"type": "integer", "minimum": 0}, + "ignored_exits": {"type": "string"}, + "show_status": {"type": "boolean"}, + }, + "additionalProperties": False, + "minProperties": 1, +} +basic_server_patch_schema = { + "type": "object", + "properties": { + "server_name": {"type": "string", "minLength": 1}, + "executable": {"type": "string"}, + "java_selection": {"type": "string"}, + "auto_start": {"type": "boolean"}, + "auto_start_delay": {"type": "integer", "minimum": 0}, + "crash_detection": {"type": "boolean"}, + "stop_command": {"type": "string"}, + "shutdown_timeout": {"type": "integer"}, + "logs_delete_after": {"type": "integer", "minimum": 0}, + "ignored_exits": {"type": "string"}, }, "additionalProperties": False, "minProperties": 1, @@ -63,7 +82,11 @@ class ApiServersServerIndexHandler(BaseApiHandler): ) try: - validate(data, server_patch_schema) + # prevent general users from becoming bad actors + if auth_data[4]["superuser"]: + validate(data, server_patch_schema) + else: + validate(data, basic_server_patch_schema) except ValidationError as e: return self.finish_json( 400, @@ -88,9 +111,24 @@ class ApiServersServerIndexHandler(BaseApiHandler): return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) server_obj = self.controller.servers.get_server_obj(server_id) + java_flag = False for key in data: # If we don't validate the input there could be security issues + if key == "java_selection" and data[key] != "none": + try: + command = self.helper.get_execution_java( + data[key], server_obj.execution_command + ) + setattr(server_obj, "execution_command", command) + except ValueError: + return self.finish_json( + 400, {"status": "error", "error": "INVALID EXECUTION COMMAND"} + ) + java_flag = True + if key != "path": + if key == "execution_command" and java_flag: + continue setattr(server_obj, key, data[key]) self.controller.servers.update_server(server_obj) @@ -134,7 +172,16 @@ class ApiServersServerIndexHandler(BaseApiHandler): ) self.tasks_manager.remove_all_server_tasks(server_id) - self.controller.remove_server(server_id, remove_files) + failed = False + for item in self.controller.servers.failed_servers[:]: + if item["server_id"] == int(server_id): + self.controller.servers.failed_servers.remove(item) + failed = True + + if failed: + self.controller.remove_unloaded_server(server_id) + else: + self.controller.remove_server(server_id, remove_files) self.controller.management.add_to_audit_log( auth_data[4]["user_id"], diff --git a/app/classes/web/routes/api/servers/server/stdin.py b/app/classes/web/routes/api/servers/server/stdin.py index b6bd0c3c..117fe188 100644 --- a/app/classes/web/routes/api/servers/server/stdin.py +++ b/app/classes/web/routes/api/servers/server/stdin.py @@ -35,7 +35,13 @@ class ApiServersServerStdinHandler(BaseApiHandler): "Please report this to the devs" ) return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) - + decoded = self.request.body.decode("utf-8") + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Sent command ({decoded}) to terminal", + server_id=0, + source_ip=self.get_remote_ip(), + ) if svr.send_command(self.request.body.decode("utf-8")): return self.finish_json( 200, diff --git a/app/classes/web/routes/api/servers/server/tasks/index.py b/app/classes/web/routes/api/servers/server/tasks/index.py index 64ca3bef..72f8def4 100644 --- a/app/classes/web/routes/api/servers/server/tasks/index.py +++ b/app/classes/web/routes/api/servers/server/tasks/index.py @@ -1,16 +1,121 @@ # TODO: create and read +import json import logging +from croniter import croniter +from jsonschema import ValidationError, validate +from app.classes.models.server_permissions import EnumPermissionsServer from app.classes.web.base_api_handler import BaseApiHandler logger = logging.getLogger(__name__) +new_task_schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "enabled": { + "type": "boolean", + "default": True, + }, + "action": { + "type": "string", + }, + "interval": {"type": "integer"}, + "interval_type": { + "type": "string", + "enum": [ + # Basic tasks + "hours", + "minutes", + "days", + # Chain reaction tasks: + "reaction", + # CRON tasks: + "", + ], + }, + "start_time": {"type": "string", "pattern": r"\d{1,2}:\d{1,2}"}, + "command": {"type": ["string", "null"]}, + "one_time": {"type": "boolean", "default": False}, + "cron_string": {"type": "string", "default": ""}, + "parent": {"type": ["integer", "null"]}, + "delay": {"type": "integer", "default": 0}, + }, + "additionalProperties": False, + "minProperties": 1, +} class ApiServersServerTasksIndexHandler(BaseApiHandler): def get(self, server_id: str, task_id: str): pass - def post(self, server_id: str, task_id: str): - pass + def post(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, new_task_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.SCHEDULE + 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"}) + data["server_id"] = server_id + if not data.get("start_time"): + data["start_time"] = "00:00" + + # validate cron string + if data["cron_string"] != "" and not croniter.is_valid(data["cron_string"]): + return self.finish_json( + 405, + { + "status": "error", + "error": self.helper.translation.translate( + "error", + "cronFormat", + self.controller.users.get_user_lang_by_id( + auth_data[4]["user_id"] + ), + ), + }, + ) + if "parent" not in data: + data["parent"] = None + task_id = self.tasks_manager.schedule_job(data) + + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Edited server {server_id}: added schedule", + server_id, + self.get_remote_ip(), + ) + self.tasks_manager.reload_schedule_from_db() + + self.finish_json(200, {"status": "ok", "data": {"schedule_id": task_id}}) diff --git a/app/classes/web/routes/api/servers/server/tasks/task/index.py b/app/classes/web/routes/api/servers/server/tasks/task/index.py index 72bbf7b0..1db5ccf1 100644 --- a/app/classes/web/routes/api/servers/server/tasks/task/index.py +++ b/app/classes/web/routes/api/servers/server/tasks/task/index.py @@ -3,6 +3,7 @@ import json import logging +from croniter import croniter from jsonschema import ValidationError, validate from app.classes.models.server_permissions import EnumPermissionsServer @@ -35,6 +36,7 @@ task_patch_schema = { "", ], }, + "name": {"type": "string"}, "start_time": {"type": "string", "pattern": r"\d{1,2}:\d{1,2}"}, "command": {"type": ["string", "null"]}, "one_time": {"type": "boolean", "default": False}, @@ -49,10 +51,47 @@ task_patch_schema = { class ApiServersServerTasksTaskIndexHandler(BaseApiHandler): def get(self, server_id: str, task_id: str): - pass + auth_data = self.authenticate_user() + if not auth_data: + return + if ( + EnumPermissionsServer.SCHEDULE + 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_scheduled_task(task_id)) def delete(self, server_id: str, task_id: str): - pass + auth_data = self.authenticate_user() + if not auth_data: + return + if ( + EnumPermissionsServer.SCHEDULE + 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: + self.tasks_manager.remove_job(task_id) + except Exception: + return self.finish_json( + 400, {"status": "error", "error": "NO SCHEDULE FOUND"} + ) + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Edited server {server_id}: removed schedule", + server_id, + self.get_remote_ip(), + ) + self.tasks_manager.reload_schedule_from_db() + + return self.finish_json(200, {"status": "ok"}) def patch(self, server_id: str, task_id: str): auth_data = self.authenticate_user() @@ -96,6 +135,21 @@ class ApiServersServerTasksTaskIndexHandler(BaseApiHandler): if str(data.get("parent")) == str(task_id) and data.get("parent") is not None: data["parent"] = None + data["server_id"] = server_id + if data["cron_string"] != "" and not croniter.is_valid(data["cron_string"]): + return self.finish_json( + 405, + { + "status": "error", + "error": self.helper.translation.translate( + "error", + "cronFormat", + self.controller.users.get_user_lang_by_id( + auth_data[4]["user_id"] + ), + ), + }, + ) self.tasks_manager.update_job(task_id, data) self.controller.management.add_to_audit_log( diff --git a/app/config/version.json b/app/config/version.json index 0f9e23a2..2f25e35c 100644 --- a/app/config/version.json +++ b/app/config/version.json @@ -1,5 +1,5 @@ { "major": 4, - "minor": 0, - "sub": 23 + "minor": 1, + "sub": 0 } diff --git a/app/frontend/templates/panel/dashboard.html b/app/frontend/templates/panel/dashboard.html index a0927996..9aa207eb 100644 --- a/app/frontend/templates/panel/dashboard.html +++ b/app/frontend/templates/panel/dashboard.html @@ -647,10 +647,13 @@ $.ajax({ type: "POST", headers: { 'X-XSRFToken': token }, - url: '/server/command?command=' + command + '&id=' + server_id, + url: `/api/v2/servers/${server_id}/action/${command}`, success: function (data) { console.log("got response:"); console.log(data); + if (command === "clone_server" && data.status === "ok") { + window.location.reload(); + } /*setTimeout(function () { if (command != 'start_server') { location.reload(); @@ -705,24 +708,6 @@ document.querySelector('.dynamicMsg').appendChild(parentEl); } - function send_kill(server_id) { - /* this getCookie function is in base.html */ - const token = getCookie("_xsrf"); - - $.ajax({ - type: "POST", - headers: { 'X-XSRFToken': token }, - url: '/ajax/kill?id=' + server_id, - success: function (data) { - console.log("got response:"); - console.log(data); - /*setTimeout(function () { - location.reload(); - }, 10000);*/ - } - }); - } - function update_one_server_status(server) { /* Mobile view update */ server_cpu = document.getElementById('server_cpu_' + server.id); @@ -901,17 +886,11 @@ }, callback: function (result) { if (result) { - send_kill(server_id); + send_command(server_id, "kill_server"); let dialog = bootbox.dialog({ title: '{% raw translate("dashboard", "killing", data["lang"]) %}', message: '
Loading...
' }); - - dialog.init(function () { - setTimeout(function () { - location.reload(); - }, 15000); - }); } } }); @@ -1000,7 +979,13 @@ }, callback: function (result) { if (result) { - cloneServer(server_id); + send_command(server_id, 'clone_server'); + bootbox.dialog({ + backdrop: true, + title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}', + message: '