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..e19211c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## --- [4.0.23] - 2023/TBD +## --- [4.1.1] - 2023/TBD ### New features TBD ### Bug fixes @@ -10,6 +10,26 @@ TBD TBD

+## --- [4.1.0] - 2023/05/15 +### New features +- Mobile PWA App (beta) | Ability to add a Crafty icon to your mobile's home screen ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/576)) +- [New Crafty Documentation release](https://docs.craftycontrol.com) +### 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)) +- MKDocs Release | Replace wiki names with docs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/583)) +### Bug fixes +- 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)) +- Fix Auth'd servers in roles | Refine server ordering ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/574)) +- Fix import loop detection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/575)) +- Fix Cargo errors on Ubuntu 23.04 installs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/579)) +- Fix project root error on first start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/580)) +### Tweaks +- Check for python version so we don't just fail out on unsupported python versions ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/577)) +- Show warning for serverjars API connection issues ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/581)) +- Retain pathing in execution command on backup restore ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/578)) +

+ ## --- [4.0.22] - 2023/04/08 ### Bug fixes - Fix dashboard crash for users without disks or if crafty doesn't have permission to access mount point ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/571)) diff --git a/Dockerfile b/Dockerfile index a379e9fb..c7eddc3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,7 +67,7 @@ LABEL \ org.opencontainers.image.title="Crafty Controller" \ org.opencontainers.image.description="A Game Server Control Panel / Launcher" \ org.opencontainers.image.url="https://craftycontrol.com/" \ - org.opencontainers.image.documentation="https://wiki.craftycontrol.com/" \ + org.opencontainers.image.documentation="https://docs.craftycontrol.com" \ org.opencontainers.image.source="https://gitlab.com/crafty-controller/crafty-4" \ org.opencontainers.image.vendor="Arcadia Technology, LLC." \ org.opencontainers.image.licenses="GPL-3.0" diff --git a/README.md b/README.md index a351e6c0..5306db38 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.1 > Python based Control Panel for your Minecraft Server ## What is Crafty Controller? @@ -9,7 +9,7 @@ a web interface for the server administrators to interact with their servers. Cr is compatible with Docker, Linux, Windows 7, Windows 8 and Windows 10. ## Documentation -Documentation available on [wiki.craftycontrol.com](https://craftycontrol.com) +Documentation available on [Crafty Docs](https://docs.craftycontrol.com) ## Meta Project Homepage - https://craftycontrol.com diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index 738d7493..b7c10ed8 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -253,6 +253,7 @@ class ServersController(metaclass=Singleton): @staticmethod def get_authorized_servers(user_id): + server_ids = [] server_data: t.List[t.Dict[str, t.Any]] = [] user_roles = HelperUsers.user_role_query(user_id) for user in user_roles: @@ -260,11 +261,13 @@ class ServersController(metaclass=Singleton): user.role_id ) for role in role_servers: - server_data.append( - ServersController().get_server_instance_by_id( - role.server_id.server_id + if role.server_id.server_id not in server_ids: + server_ids.append(role.server_id.server_id) + server_data.append( + ServersController().get_server_instance_by_id( + role.server_id.server_id + ) ) - ) return server_data @@ -275,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 b81d8c99..65bc853a 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 @@ -301,6 +325,16 @@ class Helpers: except Exception: return False + @staticmethod + def check_address_status(address): + try: + response = requests.get(address, timeout=2) + return ( + response.status_code // 100 == 2 + ) # Check if the status code starts with 2 + except requests.RequestException: + return False + @staticmethod def check_port(server_port): try: diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index dccaf4e3..acdc1cac 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 ( @@ -763,31 +763,37 @@ class TasksManager: def check_for_old_logs(self): # check for server logs first self.controller.servers.check_for_old_logs() - # check for crafty logs now - logs_path = os.path.join(self.controller.project_root, "logs") - logs_delete_after = int( - self.helper.get_setting("crafty_logs_delete_after_days") - ) - latest_log_files = [ - "session.log", - "schedule.log", - "tornado-access.log", - "session.log", - "commander.log", - ] - # we won't delete if delete logs after is set to 0 - if logs_delete_after != 0: - log_files = list( - filter( - lambda val: val not in latest_log_files, - os.listdir(logs_path), - ) + try: + # check for crafty logs now + logs_path = os.path.join(self.controller.project_root, "logs") + logs_delete_after = int( + self.helper.get_setting("crafty_logs_delete_after_days") + ) + latest_log_files = [ + "session.log", + "schedule.log", + "tornado-access.log", + "session.log", + "commander.log", + ] + # we won't delete if delete logs after is set to 0 + if logs_delete_after != 0: + log_files = list( + filter( + lambda val: val not in latest_log_files, + os.listdir(logs_path), + ) + ) + for log_file in log_files: + log_file_path = os.path.join(logs_path, log_file) + if Helpers.check_file_exists( + log_file_path + ) and Helpers.is_file_older_than_x_days( + log_file_path, logs_delete_after + ): + os.remove(log_file_path) + except: + logger.debug( + "Unable to find project root." + " If this issue persists please contact support." ) - for log_file in log_files: - log_file_path = os.path.join(logs_path, log_file) - if Helpers.check_file_exists( - log_file_path - ) and Helpers.is_file_older_than_x_days( - log_file_path, logs_delete_after - ): - os.remove(log_file_path) diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index cd1ccc04..e3da33a8 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -289,9 +289,9 @@ class AjaxHandler(BaseHandler): 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) + svr_obj = self.controller.servers.get_server_instance_by_id(server_id) - if command == srv_obj.settings["stop_command"]: + 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}" @@ -313,8 +313,8 @@ class AjaxHandler(BaseHandler): ) command = None if command: - if srv_obj.check_running(): - srv_obj.send_command(command) + if svr_obj.check_running(): + svr_obj.send_command(command) self.controller.management.add_to_audit_log( exec_user["user_id"], @@ -382,23 +382,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) @@ -445,6 +428,21 @@ class AjaxHandler(BaseHandler): 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 @@ -505,6 +503,21 @@ class AjaxHandler(BaseHandler): 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 @@ -624,12 +637,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 +675,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 c24582bf..df58263d 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 @@ -256,7 +254,12 @@ class PanelHandler(BaseHandler): user_order = user_order["server_order"].split(",") page_servers = [] server_ids = [] - + for server in defined_servers: + server_ids.append(str(server.server_id)) + if str(server.server_id) not in user_order: + # a little unorthodox, but this will cut out a loop. + # adding servers to the user order that don't already exist there. + user_order.append(str(server.server_id)) for server_id in user_order[:]: for server in defined_servers[:]: if str(server.server_id) == str(server_id): @@ -265,14 +268,7 @@ class PanelHandler(BaseHandler): ) user_order.remove(server_id) defined_servers.remove(server) - - for server in defined_servers: - server_ids.append(str(server.server_id)) - if server not in page_servers: - page_servers.append( - DatabaseShortcuts.get_data_obj(server.server_object) - ) - + break for server_id in user_order[:]: # remove IDs in list that user no longer has access to if str(server_id) not in server_ids: @@ -452,6 +448,7 @@ class PanelHandler(BaseHandler): page_servers.append(server) un_used_servers.remove(server) user_order.remove(server_id) + break # we only want to set these server stats values once. # We need to update the flag so it only hits that if once. flag += 1 @@ -1080,7 +1077,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" @@ -1583,156 +1580,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) @@ -1829,336 +1676,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/public_handler.py b/app/classes/web/public_handler.py index dad74881..76c6a8be 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -50,12 +50,15 @@ class PublicHandler(BaseHandler): if page == "login": template = "public/login.html" - elif page == 404: + elif page == "404": template = "public/404.html" elif page == "error": template = "public/error.html" + elif page == "offline": + template = "public/offline.html" + elif page == "logout": self.clear_cookie("token") # self.clear_cookie("user") 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/classes/web/server_handler.py b/app/classes/web/server_handler.py index 8b533bdf..408152da 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -143,7 +143,11 @@ class ServerHandler(BaseHandler): "not a server creator or server limit reached" ) return - + page_data["server_api"] = False + if page_data["online"]: + page_data["server_api"] = self.helper.check_address_status( + "https://serverjars.com/api/fetchTypes" + ) page_data["server_types"] = self.controller.server_jars.get_serverjar_data() page_data["js_server_types"] = json.dumps( self.controller.server_jars.get_serverjar_data() @@ -333,7 +337,7 @@ class ServerHandler(BaseHandler): if import_type == "import_jar": if self.helper.is_subdir( - import_server_path, self.controller.project_root + self.controller.project_root, import_server_path ): self.redirect( "/panel/error?error=Loop Error: The selected path will cause" @@ -499,7 +503,7 @@ class ServerHandler(BaseHandler): if import_type == "import_jar": if self.helper.is_subdir( - import_server_path, self.controller.project_root + self.controller.project_root, import_server_path ): self.redirect( "/panel/error?error=Loop Error: The selected path will cause" diff --git a/app/classes/web/static_handler.py b/app/classes/web/static_handler.py index 731b48f4..4f75b036 100644 --- a/app/classes/web/static_handler.py +++ b/app/classes/web/static_handler.py @@ -11,6 +11,9 @@ except ModuleNotFoundError as e: class CustomStaticHandler(tornado.web.StaticFileHandler): def validate_absolute_path(self, root: str, absolute_path: str) -> Optional[str]: + # This is for the mobile app service worker + if self.request.path.find("service-worker.js") != -1: + self.set_header("Service-Worker-Allowed", "/") try: return super().validate_absolute_path(root, absolute_path) except tornado.web.HTTPError as error: diff --git a/app/config/version.json b/app/config/version.json index 0f9e23a2..a567e501 100644 --- a/app/config/version.json +++ b/app/config/version.json @@ -1,5 +1,5 @@ { "major": 4, - "minor": 0, - "sub": 23 + "minor": 1, + "sub": 1 } diff --git a/app/frontend/static/assets/crafty.webmanifest b/app/frontend/static/assets/crafty.webmanifest new file mode 100644 index 00000000..6c9460e3 --- /dev/null +++ b/app/frontend/static/assets/crafty.webmanifest @@ -0,0 +1,40 @@ +{ + "background_color": "#222436", + "description": "Crafty Controller is a free and open-source Minecraft launcher and manager that allows users to start and administer Minecraft servers from a user-friendly interface.", + "dir": "ltr", + "display": "standalone", + "name": "Crafty Controller", + "orientation": "any", + "scope": "/", + "short_name": "Crafty", + "start_url": "/", + "theme_color": "#222436", + "categories": ["utilities"], + "icons": [ + { + "src": "/static/assets/images/Crafty_4-0_Logo_square.ico", + "type": "image/x-icon", + "sizes":"128x128" + }, + { + "src": "/static/assets/images/Crafty_4-0.png", + "type": "image/png", + "sizes": "144x144", + "purpose": "any" + }, + { + "src": "/static/assets/images/crafty-logo-square-1024.png", + "type": "image/png", + "sizes": "1024x1024", + "purpose": "any" + }, + { + "src": "/static/assets/images/crafty-logo-square-96.png", + "type": "image/png", + "sizes": "96x96", + "purpose": "any" + } + ], + "lang": "en", + "prefer_related_applications": false +} diff --git a/app/frontend/static/assets/css/crafty.css b/app/frontend/static/assets/css/crafty.css index 9d389619..c8edba8b 100644 --- a/app/frontend/static/assets/css/crafty.css +++ b/app/frontend/static/assets/css/crafty.css @@ -149,10 +149,6 @@ div>.input-group>.custom-file-input { border: 1px solid var(--outline); } -.input-group>.input-group-append>button { - height: calc(1.5em + 0.75rem + 2px); -} - div>.input-group>.form-control-file { position: relative !important; -webkit-box-flex: 1 !important; diff --git a/app/frontend/static/assets/images/Crafty_4-0.png b/app/frontend/static/assets/images/Crafty_4-0.png new file mode 100644 index 00000000..d873eedc Binary files /dev/null and b/app/frontend/static/assets/images/Crafty_4-0.png differ diff --git a/app/frontend/static/assets/images/crafty-logo-square-1024.png b/app/frontend/static/assets/images/crafty-logo-square-1024.png new file mode 100644 index 00000000..65dd0671 Binary files /dev/null and b/app/frontend/static/assets/images/crafty-logo-square-1024.png differ diff --git a/app/frontend/static/assets/images/crafty-logo-square-96.png b/app/frontend/static/assets/images/crafty-logo-square-96.png new file mode 100644 index 00000000..6928e039 Binary files /dev/null and b/app/frontend/static/assets/images/crafty-logo-square-96.png differ diff --git a/app/frontend/static/assets/js/shared/service-worker.js b/app/frontend/static/assets/js/shared/service-worker.js new file mode 100644 index 00000000..f8073c39 --- /dev/null +++ b/app/frontend/static/assets/js/shared/service-worker.js @@ -0,0 +1,46 @@ +// This is the "Offline page" service worker + +importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js'); + +const CACHE = "crafty-controller"; + +// TODO: replace the following with the correct offline fallback page i.e.: const offlineFallbackPage = "offline.html"; +const offlineFallbackPage = "/offline"; + +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SKIP_WAITING") { + self.skipWaiting(); + } +}); + +self.addEventListener('install', async (event) => { + event.waitUntil( + caches.open(CACHE) + .then((cache) => cache.add(offlineFallbackPage)) + ); +}); + +if (workbox.navigationPreload.isSupported()) { + workbox.navigationPreload.enable(); +} + +self.addEventListener('fetch', (event) => { + if (event.request.mode === 'navigate') { + event.respondWith((async () => { + try { + const preloadResp = await event.preloadResponse; + + if (preloadResp) { + return preloadResp; + } + const networkResp = await fetch(event.request); + return networkResp; + } catch (error) { + + const cache = await caches.open(CACHE); + const cachedResp = await cache.match(offlineFallbackPage); + return cachedResp; + } + })()); + } +}); \ No newline at end of file diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html index 13da580c..58d49c53 100755 --- a/app/frontend/templates/base.html +++ b/app/frontend/templates/base.html @@ -18,6 +18,16 @@ href="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.css" /> + + + + + + + + + + @@ -247,7 +257,7 @@ const sendWssError = () => wsOpen || warn( 'WebSockets are required for Crafty to work. This websocket connection has been closed. Are you using a reverse proxy?', - 'https://wiki.craftycontrol.com/en/4/docs/Reverse%20Proxy%20Examples', + 'https://docs.craftycontrol.com/pages/getting-started/proxies/', 'wssError' ) @@ -526,6 +536,14 @@ }); }); + $(document).ready(() => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', {scope: '/'}) + .then(function (registration) { + console.log('Service Worker Registered'); + }); + } + }); {% block js %} diff --git a/app/frontend/templates/blank_base.html b/app/frontend/templates/blank_base.html index 50f4e815..5171ec27 100644 --- a/app/frontend/templates/blank_base.html +++ b/app/frontend/templates/blank_base.html @@ -14,6 +14,13 @@ + + + + + + + diff --git a/app/frontend/templates/main_menu.html b/app/frontend/templates/main_menu.html index 09c2dc79..5bbcfbad 100644 --- a/app/frontend/templates/main_menu.html +++ b/app/frontend/templates/main_menu.html @@ -100,7 +100,7 @@ diff --git a/app/frontend/templates/panel/custom_login.html b/app/frontend/templates/panel/custom_login.html index 7be39405..22a0c48c 100644 --- a/app/frontend/templates/panel/custom_login.html +++ b/app/frontend/templates/panel/custom_login.html @@ -66,7 +66,7 @@
- +
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: '
  {% raw translate("dashboard", "bePatientClone", data["lang"]) %}
', + closeButton: false, + }); } } @@ -1008,16 +993,6 @@ }); }); - - function cloneServer(server_id) { - send_command(server_id, 'clone_server'); - bootbox.dialog({ - backdrop: true, - title: '{% raw translate("dashboard", "sendingCommand", data["lang"]) %}', - message: '
  {% raw translate("dashboard", "bePatientClone", data["lang"]) %}
', - closeButton: false, - }); - } @@ -1069,12 +1044,12 @@ const token = getCookie("_xsrf") $.ajax({ - type: "POST", + type: "PATCH", headers: { 'X-XSRFToken': token }, - url: '/ajax/send_order?order=' + id_string, - data: { - order: id_string, - }, + url: `/api/v2/users/@me`, + data: JSON.stringify({ + server_order: id_string, + }), success: function (data) { console.log("got response:"); console.log(data); diff --git a/app/frontend/templates/panel/denied.html b/app/frontend/templates/panel/denied.html index 717c0def..d4548c05 100644 --- a/app/frontend/templates/panel/denied.html +++ b/app/frontend/templates/panel/denied.html @@ -12,6 +12,15 @@ + + + + + + + + + @@ -24,7 +33,7 @@ @@ -77,6 +86,21 @@ + \ No newline at end of file diff --git a/app/frontend/templates/panel/panel_edit_role.html b/app/frontend/templates/panel/panel_edit_role.html index 73958777..273bddec 100644 --- a/app/frontend/templates/panel/panel_edit_role.html +++ b/app/frontend/templates/panel/panel_edit_role.html @@ -321,9 +321,60 @@ return r ? r[1] : undefined; } + function gather_server_json() { + servers = []; + for (s = 0; s < page_servers.length; s++){ + mask = "" + for (i = 0; i < permissions.length; i++){ + if ($(`#permission_${page_servers[s].id}_${permissions[i]}`).prop('checked')){ + mask += "1" + }else{ + mask += "0" + } + } + servers.push(JSON.stringify({"id": page_servers[s].id, "permissions": mask})); + } + return servers; + } + $( document ).ready(function() { console.log( "ready!" ); }); + const roleId = new URLSearchParams(document.location.search).get('id'); + + $("#config_form").on("submit", async function (e) { + e.preventDefault(); + var token = getCookie("_xsrf") + let configForm = document.getElementById("config_form"); + + let formData = new FormData(configForm); + //Create an object from the form data entries + let formDataObject = Object.fromEntries(formData.entries()); + let send_object = Object() + send_object.servers = [] + send_object.name = formDataObject.role_name + + // Format the plain form data as JSON + let formDataJsonString = JSON.stringify(formDataObject, replacer); + + let res = await fetch(`/api/v2/roles/${roleId}`, { + method: 'PATCH', + headers: { + 'X-XSRFToken': token + }, + body: formDataJsonString, + }); + let responseData = await res.json(); + if (responseData.status === "ok") { + window.location.reload(); + } else { + + bootbox.alert({ + title: responseData.error, + message: responseData.error_data + }); + } + }); diff --git a/app/frontend/templates/panel/server_admin_controls.html b/app/frontend/templates/panel/server_admin_controls.html index ed255e8b..486139c5 100644 --- a/app/frontend/templates/panel/server_admin_controls.html +++ b/app/frontend/templates/panel/server_admin_controls.html @@ -14,7 +14,8 @@
- @@ -372,10 +372,6 @@ border: 1px solid var(--outline); } - .input-group>.input-group-append>button { - height: calc(1.5em + 0.75rem + 2px); - } - .scroll { max-height: 12em; overflow-y: auto; diff --git a/app/frontend/templates/server/wizard.html b/app/frontend/templates/server/wizard.html index ee1eb3e0..8e2b74fe 100644 --- a/app/frontend/templates/server/wizard.html +++ b/app/frontend/templates/server/wizard.html @@ -20,7 +20,7 @@ {% if data['online'] %}
-
+

{{ translate('serverWizard', 'newServer', data['lang']) }}

@@ -28,122 +28,157 @@

- {% raw xsrf_form_html() %} -

-
-
- - {% if data['super_user'] %} -
- + {% if data["server_api"] %} +
+ {% else %} +
+ + {% end %} + {% raw xsrf_form_html() %} +
+
+
+ +
+ + {% if data['super_user'] %} +
+ +
{% end %} - - {% for s in data['server_types'] %} - - {% end %} - - {% if data['super_user'] %} -
-
+
- {% end %} -
-
-
-
- - -
-
+
+
+ + +
+
-
-
- - -
-
+
+
+ + +
+
-
-
- - -
-
+
+
+ + +
+
-
-
-

{{ translate('serverWizard', 'quickSettings', data['lang']) }} - {{ translate('serverWizard', 'quickSettingsDescription', - data['lang']) }}

-
-
- -
-
- -
-
+
+

{{ translate('serverWizard', 'quickSettings', data['lang']) }} - {{ translate('serverWizard', 'quickSettingsDescription', + data['lang']) }}

+
+
-
-
- - -
-
+
+
+ + +
+
-
-
- - -
-
-
-
-
-
-
-

- {{ translate('serverWizard', 'addRole', data['lang']) }} - - {{ translate('serverWizard', 'autoCreate', - data['lang']) }} -

-
-
-
-
- {% for r in data['roles'] %} - - {% end %} +
+
+ + +
+
+ +
+
+ + +
+
+
+
+
+
+
+

+ {{ translate('serverWizard', 'addRole', data['lang']) }} + - {{ translate('serverWizard', 'autoCreate', + data['lang']) }} +

+
+
+
+
+ {% for r in data['roles'] %} + + {% end %} +
+
+
+ + + + {% if not data["server_api"] %} +
+

 {{ translate('error', 'serverJars1', data['lang']) }} {{ translate('error', 'craftyStatus', data['lang']) }} +  {{ translate('error', 'serverJars2', data['lang']) }}

- -
- - - - -

+ {% end %}
+ {% end %}
diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index 6789b250..f30bc6c8 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -186,7 +186,11 @@ "terribleFailure": "What a Terrible Failure!", "superError": "You must be a super user to complete this action.", "fileError": "File type must be an image.", - "migration": "Crafty's main server storage is being mirgated to a new location. All server starts have been suspended during this time. Please wait while we finish this migration" + "migration": "Crafty's main server storage is being mirgated to a new location. All server starts have been suspended during this time. Please wait while we finish this migration", + "serverJars1": "Server JARs API unreachable. Please check", + "craftyStatus": "Crafty's status page", + "serverJars2": "for the most up to date information.", + "cronFormat": "Invalid Cron format detected" }, "footer": { "allRightsReserved": "All rights reserved", @@ -209,6 +213,10 @@ "preparingLogs": " Please wait while we prepare your logs... We`ll send a notification when they`re ready. This may take a while for large deployments.", "supportLogs": "Support Logs" }, + "offline": { + "offline": "Offline", + "pleaseConnect": "Please connect to the internet to use Crafty." + }, "panelConfig": { "adminControls": "Admin Controls", "allowedServers": "Allowed Servers", @@ -574,7 +582,8 @@ "documentation": "Documentation", "navigation": "Navigation", "newServer": "Create New Server", - "servers": "Servers" + "servers": "Servers", + "inApp": "In App Docs" }, "userConfig": { "apiKey": "API Keys", diff --git a/main.py b/main.py index 97a8229f..2338517b 100644 --- a/main.py +++ b/main.py @@ -27,6 +27,17 @@ if helper.check_root(): time.sleep(5) Console.critical("Crafty shutting down. Root/Admin access denied.") sys.exit(0) +if not (sys.version_info.major == 3 and sys.version_info.minor >= 9): + Console.critical( + "Python version mismatch. Python " + f"{sys.version_info.major}.{sys.version_info.minor} detected." + ) + Console.critical("Crafty requires Python 3.9 or above. Please upgrade python.") + time.sleep(5) + Console.critical("Crafty shutting down.") + time.sleep(3) + Console.info("Crafty stopped. Exiting...") + sys.exit(0) # pylint: disable=wrong-import-position try: from app.classes.models.base_model import database_proxy diff --git a/requirements.txt b/requirements.txt index 4cbb29e9..3a1bd5e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,4 @@ termcolor==1.1 tornado==6.0 tzlocal==4.0 jsonschema==4.5.1 -orjson==3.6.7 +orjson==3.8.12