diff --git a/.gitignore b/.gitignore index b7f2d2b3..7f3ad858 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ venv.bak/ .idea/ /imports/ /servers/ +/app/frontend/static/assets/images/auth/custom/ /backups/ /temp/ /docker/servers/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cab7514..f90dd136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ # Changelog +## --- [4.0.17] - 2022/11/30 +### New features +- Automate forge install process through Crafty server creation for Forge server version 1.16 and greater. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/495)) +- Tooltip for server port on dashboard. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/496)) +- Custom login image backgrounds. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/494)) +### Bug fixes +- Fix no port on bedrock server creation. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/493)) +### Tweaks +- Docker🐋 | Update image base to Ubuntu 22.04 Jammy ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/497))
+*(OpenJDK16 Removed, no jammy backport)* +

+ ## --- [4.0.16] - 2022/10/23 ### New features - Automatically set update url for (new) server creation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/487)) diff --git a/Dockerfile b/Dockerfile index 4bd83bba..a379e9fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu:22.04 ENV DEBIAN_FRONTEND="noninteractive" @@ -24,7 +24,6 @@ RUN apt-get update \ default-jre \ openjdk-8-jre-headless \ openjdk-11-jre-headless \ - openjdk-16-jre-headless \ openjdk-17-jre-headless \ && apt-get autoremove \ && apt-get clean diff --git a/README.md b/README.md index babfc564..5ae83d5f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,5 @@ [![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com) - -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![Supported Python Versions](https://shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20-blue)](https://www.python.org) -[![Version](https://img.shields.io/badge/release-v4.0.16-blue)](https://gitlab.com/crafty-controller/crafty-4/-/releases) -[![Code Quality](https://img.shields.io/badge/code%20quality-10-brightgreen)](https://gitlab.com/crafty-controller/crafty-4) -[![Build Status](https://gitlab.com/crafty-controller/crafty-4/badges/master/pipeline.svg)](https://gitlab.com/crafty-controller/crafty-4/-/commits/master) -[![Licence](https://img.shields.io/gitlab/license/20430749)](https://gitlab.com/crafty-controller/crafty-4/-/blob/master/LICENSE) -# Crafty Controller 4.0.16 +# Crafty Controller 4.0.17 > Python based Control Panel for your Minecraft Server ## What is Crafty Controller? diff --git a/app/classes/controllers/management_controller.py b/app/classes/controllers/management_controller.py index 547a8ebd..47860fe1 100644 --- a/app/classes/controllers/management_controller.py +++ b/app/classes/controllers/management_controller.py @@ -94,6 +94,14 @@ class ManagementController: def delete_scheduled_task(schedule_id): return HelpersManagement.delete_scheduled_task(schedule_id) + @staticmethod + def set_login_image(path): + HelpersManagement.set_login_image(path) + + @staticmethod + def get_login_image(): + return HelpersManagement.get_login_image() + @staticmethod def update_scheduled_task(schedule_id, updates): return HelpersManagement.update_scheduled_task(schedule_id, updates) diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index b0a9044a..4c97a6c7 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -119,9 +119,15 @@ class ServersController(metaclass=Singleton): return srv.stats_helper.set_import() @staticmethod - def finish_import(server_id): + def finish_import(server_id, forge=False): srv = ServersController().get_server_instance_by_id(server_id) - return srv.stats_helper.finish_import() + # This is where we start the forge installerr + if forge: + srv.run_threaded_server( + HelperUsers.get_user_id_by_name("system"), forge_install=True + ) + else: + srv.stats_helper.finish_import() @staticmethod def get_import_status(server_id): diff --git a/app/classes/minecraft/serverjars.py b/app/classes/minecraft/serverjars.py index 1ecfc0f1..a656944b 100644 --- a/app/classes/minecraft/serverjars.py +++ b/app/classes/minecraft/serverjars.py @@ -187,31 +187,28 @@ class ServerJars: # open a file stream with requests.get(fetch_url, timeout=2, stream=True) as r: + success = False try: with open(path, "wb") as output: shutil.copyfileobj(r.raw, output) - ServersController.finish_import(server_id) + # If this is the newer forge version we will run the installer + if server == "forge" and int(version.split(".")[1]) > 15: + ServersController.finish_import(server_id, True) + else: + ServersController.finish_import(server_id) - for user in server_users: - self.helper.websocket_helper.broadcast_user( - user, "notification", "Executable download finished" - ) - time.sleep(3) - self.helper.websocket_helper.broadcast_user( - user, "send_start_reload", {} - ) - return True + success = True except Exception as e: logger.error(f"Unable to save jar to {path} due to error:{e}") ServersController.finish_import(server_id) server_users = PermissionsServers.get_server_user_list(server_id) - for user in server_users: - self.helper.websocket_helper.broadcast_user( - user, "notification", "Executable download finished" - ) - time.sleep(3) - self.helper.websocket_helper.broadcast_user( - user, "send_start_reload", {} - ) - return False + for user in server_users: + self.helper.websocket_helper.broadcast_user( + user, "notification", "Executable download finished" + ) + time.sleep(3) + self.helper.websocket_helper.broadcast_user( + user, "send_start_reload", {} + ) + return success diff --git a/app/classes/models/management.py b/app/classes/models/management.py index c961f002..55c86bb7 100644 --- a/app/classes/models/management.py +++ b/app/classes/models/management.py @@ -43,6 +43,7 @@ class AuditLog(BaseModel): # ********************************************************************************** class CraftySettings(BaseModel): secret_api_key = CharField(default="") + login_photo = CharField(default="login_1.jpg") class Meta: table_name = "crafty_settings" @@ -254,6 +255,19 @@ class HelpersManagement: ) return settings[0].secret_api_key + @staticmethod + def get_login_image(): + settings = CraftySettings.select(CraftySettings.login_photo).where( + CraftySettings.id == 1 + ) + return settings[0].login_photo + + @staticmethod + def set_login_image(photo): + CraftySettings.update({CraftySettings.login_photo: photo}).where( + CraftySettings.id == 1 + ).execute() + # ********************************************************************************** # Schedules Methods # ********************************************************************************** diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index eb8c3219..e6263cdf 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -73,6 +73,7 @@ class Controller: timezone=str(tz) ) self.first_login = False + self.cached_login = self.management.get_login_image() self.support_scheduler.start() @staticmethod @@ -484,17 +485,32 @@ class Controller: return False if Helpers.is_os_windows(): - server_command = ( - f"java -Xms{Helpers.float_to_string(min_mem)}M " - f"-Xmx{Helpers.float_to_string(max_mem)}M " - f'-jar "{server_file}" nogui' - ) + # Let's check for and setup for install server commands + if server == "forge": + server_command = ( + f"java -Xms{Helpers.float_to_string(min_mem)}M " + f"-Xmx{Helpers.float_to_string(max_mem)}M " + f'-jar "{server_file}" --installServer' + ) + else: + server_command = ( + f"java -Xms{Helpers.float_to_string(min_mem)}M " + f"-Xmx{Helpers.float_to_string(max_mem)}M " + f'-jar "{server_file}" nogui' + ) else: - server_command = ( - f"java -Xms{Helpers.float_to_string(min_mem)}M " - f"-Xmx{Helpers.float_to_string(max_mem)}M " - f"-jar {server_file} nogui" - ) + if server == "forge": + server_command = ( + f"java -Xms{Helpers.float_to_string(min_mem)}M " + f"-Xmx{Helpers.float_to_string(max_mem)}M " + f"-jar {server_file} --installServer" + ) + else: + server_command = ( + f"java -Xms{Helpers.float_to_string(min_mem)}M " + f"-Xmx{Helpers.float_to_string(max_mem)}M " + f"-jar {server_file} nogui" + ) server_log_file = "./logs/latest.log" server_stop = "stop" diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index 257bf64f..ccc50f70 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -202,12 +202,15 @@ class ServerInstance: # remove the scheduled job since it's ran return self.server_scheduler.remove_job(str(self.server_id)) - def run_threaded_server(self, user_id): + def run_threaded_server(self, user_id, forge_install=False): # start the server self.server_thread = threading.Thread( target=self.start_server, daemon=True, - args=(user_id,), + args=( + user_id, + forge_install, + ), name=f"{self.server_id}_server_thread", ) self.server_thread.start() @@ -292,13 +295,13 @@ class ServerInstance: logger.critical(f"Unable to write/access {self.server_path}") Console.critical(f"Unable to write/access {self.server_path}") - def start_server(self, user_id): + def start_server(self, user_id, forge_install=False): if not user_id: user_lang = self.helper.get_setting("language") else: user_lang = HelperUsers.get_user_lang_by_id(user_id) - if self.stats_helper.get_import_status(): + if self.stats_helper.get_import_status() and not forge_install: if user_id: self.helper.websocket_helper.broadcast_user( user_id, @@ -341,7 +344,9 @@ class ServerInstance: "eula= true", "eula =true", ] - + # If this is a forge installer we're running we can bypass the eula checks. + if forge_install is True: + e_flag = True if not e_flag and self.settings["type"] == "minecraft-java": if user_id: self.helper.websocket_helper.broadcast_user( @@ -422,6 +427,9 @@ class ServerInstance: ).format(self.name, ex) }, ) + if forge_install: + # Reset import status if failed while forge installing + self.stats_helper.finish_import() return False else: @@ -460,6 +468,9 @@ class ServerInstance: ).format(self.name, ex) }, ) + if forge_install: + # Reset import status if failed while forge installing + self.stats_helper.finish_import() return False out_buf = ServerOutBuf(self.helper, self.process, self.server_id) @@ -540,6 +551,10 @@ class ServerInstance: self.detect_crash, "interval", seconds=30, id=f"c_{self.server_id}" ) + # If this is a forge install we'll call the watcher to do the things + if forge_install: + self.forge_install_watcher() + def check_internet_thread(self, user_id, user_lang): if user_id: if not Helpers.check_internet(): @@ -553,6 +568,78 @@ class ServerInstance: }, ) + def forge_install_watcher(self): + # Enter for install if that parameter is true + while True: + # We'll watch the process + if self.process.poll() is None: + # IF process still has not exited we'll keep looping + time.sleep(5) + Console.debug("Installing Forge...") + else: + # Process has exited. Lets do some work to setup the new + # run command. + # Let's grab the server object we're going to update. + server_obj = HelperServers.get_server_obj(self.server_id) + + # The forge install is done so we can delete that install file. + os.remove(os.path.join(server_obj.path, server_obj.executable)) + + # We need to grab the exact forge version number. + # We know we can find it here in the run.sh/bat script. + run_file_path = "" + if self.helper.is_os_windows(): + run_file_path = os.path.join(server_obj.path, "run.bat") + else: + run_file_path = os.path.join(server_obj.path, "run.sh") + + if Helpers.check_file_perms(run_file_path) and os.path.isfile( + run_file_path + ): + run_file = open(run_file_path, "r", encoding="utf-8") + run_file_text = run_file.read() + else: + Console.error( + "ERROR ! Forge install can't read the scripts files." + " Aborting ..." + ) + return + + # We get the server command parameters from forge script + server_command = re.findall( + r"java @([a-zA-Z0-9_\.]+)" + r" @([a-z.\/\-]+)([0-9.\-]+)\/\b([a-z_0-9]+\.txt)\b( .{2,4})?", + run_file_text, + )[0] + + version = server_command[2] + executable_path = f"{server_command[1]}{server_command[2]}/" + + # Let's set the proper server executable + server_obj.executable = os.path.join( + f"{executable_path}forge-{version}-server.jar" + ) + # Now lets set up the new run command. + # This is based off the run.sh/bat that + # Forge uses in 1.16 and < + execution_command = ( + f"java @{server_command[0]}" + f" @{executable_path}{server_command[3]} nogui {server_command[4]}" + ) + server_obj.execution_command = execution_command + Console.debug("SUCCESS! Forge install completed") + + # We'll update the server with the new information now. + HelperServers.update_server(server_obj) + self.stats_helper.finish_import() + server_users = PermissionsServers.get_server_user_list(self.server_id) + + for user in server_users: + self.helper.websocket_helper.broadcast_user( + user, "send_start_reload", {} + ) + break + def stop_crash_detection(self): # This is only used if the crash detection settings change # while the server is running. diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index a90b4141..fe3fb14f 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -352,6 +352,38 @@ class AjaxHandler(BaseHandler): self.controller.clear_unexecuted_commands() return + elif page == "select_photo": + if exec_user["superuser"]: + photo = self.get_argument("photo", None) + if photo == "login_1.jpg": + self.controller.management.set_login_image("login_1.jpg") + self.controller.cached_login = f"{photo}" + else: + self.controller.management.set_login_image(f"custom/{photo}") + self.controller.cached_login = f"custom/{photo}" + return + + elif page == "delete_photo": + if exec_user["superuser"]: + photo = self.get_argument("photo", None) + if photo and photo != "login_1.jpg": + os.remove( + os.path.join( + self.controller.project_root, + f"app/frontend/static/assets/images/auth/custom/{photo}", + ) + ) + current = self.controller.cached_login + split = current.split("/") + if len(split) == 1: + current_photo = current + else: + current_photo = split[1] + if current_photo == photo: + self.controller.management.set_login_image("login_1.jpg") + self.controller.cached_login = "login_1.jpg" + return + elif page == "kill": if not permissions["Commands"] in user_perms: if not superuser: diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 9111d358..d64774bd 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -849,6 +849,32 @@ class PanelHandler(BaseHandler): page_data["roles"] = self.controller.roles.get_all_roles() page_data["auth-servers"][user.user_id] = super_auth_servers page_data["managed_users"] = [] + page_data["backgrounds"] = [] + cached_split = self.controller.cached_login.split("/") + + if len(cached_split) == 1: + page_data["backgrounds"].append( + self.controller.cached_login + ) + else: + page_data["backgrounds"].append(cached_split[1]) + if "login_1.jpg" not in page_data["backgrounds"]: + page_data["backgrounds"].append("login_1.jpg") + self.helper.ensure_dir_exists( + os.path.join( + self.controller.project_root, + "app/frontend/static/assets/images/auth/custom", + ) + ) + for item in os.listdir( + os.path.join( + self.controller.project_root, + "app/frontend/static/assets/images/auth/custom", + ) + ): + if item not in page_data["backgrounds"]: + page_data["backgrounds"].append(item) + page_data["background"] = self.controller.cached_login else: page_data["managed_users"] = self.controller.users.get_managed_users( exec_user["user_id"] diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py index cf50d535..bb31248f 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -48,6 +48,7 @@ class PublicHandler(BaseHandler): template = "public/404.html" if page == "login": + page_data["background"] = self.controller.cached_login template = "public/login.html" elif page == 404: diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index d60ce2a2..08162365 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -457,6 +457,9 @@ class ServerHandler(BaseHandler): server = bleach.clean(self.get_argument("server", "")) server_name = bleach.clean(self.get_argument("server_name", "")) port = bleach.clean(self.get_argument("port", "")) + + if not port: + port = 19132 if int(port) < 1 or int(port) > 65535: self.redirect( "/panel/error?error=Constraint Error: " diff --git a/app/classes/web/upload_handler.py b/app/classes/web/upload_handler.py index 077128e4..2de4fe1f 100644 --- a/app/classes/web/upload_handler.py +++ b/app/classes/web/upload_handler.py @@ -152,65 +152,46 @@ class UploadHandler(BaseHandler): return self.do_upload = True - if superuser: - exec_user_server_permissions = ( - self.controller.server_perms.list_defined_permissions() + if not superuser: + self.helper.websocket_helper.broadcast_user( + user_id, + "send_start_error", + { + "error": self.helper.translation.translate( + "error", + "superError", + self.controller.users.get_user_lang_by_id(user_id), + ), + }, ) - elif api_key is not None: - exec_user_server_permissions = ( - self.controller.server_perms.get_api_key_permissions_list( - api_key, server_id - ) + return + if not self.request.headers.get("X-Content-Type", None).startswith( + "image/" + ): + self.helper.websocket_helper.broadcast_user( + user_id, + "send_start_error", + { + "error": self.helper.translation.translate( + "error", + "fileError", + self.controller.users.get_user_lang_by_id(user_id), + ), + }, ) - else: - exec_user_server_permissions = ( - self.controller.server_perms.get_user_id_permissions_list( - exec_user["user_id"], server_id - ) - ) - - server_id = self.request.headers.get("X-ServerId", None) - if server_id is None: - logger.warning("Server ID not found in upload handler call") - Console.warning("Server ID not found in upload handler call") - self.do_upload = False - + return if user_id is None: logger.warning("User ID not found in upload handler call") Console.warning("User ID not found in upload handler call") self.do_upload = False - if EnumPermissionsServer.FILES not in exec_user_server_permissions: - logger.warning( - f"User {user_id} tried to upload a file to " - f"{server_id} without permissions!" - ) - Console.warning( - f"User {user_id} tried to upload a file to " - f"{server_id} without permissions!" - ) - self.do_upload = False - - path = self.request.headers.get("X-Path", None) + path = os.path.join( + self.controller.project_root, + "app/frontend/static/assets/images/auth/custom", + ) filename = self.request.headers.get("X-FileName", None) full_path = os.path.join(path, filename) - if not Helpers.in_path( - Helpers.get_os_understandable_path( - self.controller.servers.get_server_data_by_id(server_id)["path"] - ), - full_path, - ): - logger.warning( - f"User {user_id} tried to upload a file to {server_id} " - f"but the path is not inside of the server!" - ) - Console.warning( - f"User {user_id} tried to upload a file to {server_id} " - f"but the path is not inside of the server!" - ) - self.do_upload = False - if self.do_upload: try: self.f = open(full_path, "wb") diff --git a/app/config/version.json b/app/config/version.json index 2dece081..f97d9f2b 100644 --- a/app/config/version.json +++ b/app/config/version.json @@ -1,5 +1,5 @@ { "major": 4, "minor": 0, - "sub": 16 + "sub": 17 } diff --git a/app/frontend/static/assets/css/dark/style.css b/app/frontend/static/assets/css/dark/style.css index d4b32181..dbd6ea8b 100755 --- a/app/frontend/static/assets/css/dark/style.css +++ b/app/frontend/static/assets/css/dark/style.css @@ -28362,11 +28362,6 @@ div.tagsinput span.tag a { min-height: 100vh; } -.auth.auth-bg-1 { - background: url("../../images/auth/login_1.jpg"); - background-size: cover; -} - .auth.register-bg-1 { background: url("../../images/auth/register.jpg") center center no-repeat; background-size: cover; diff --git a/app/frontend/static/assets/css/shared/style.css b/app/frontend/static/assets/css/shared/style.css index 5b223d6d..7352d637 100755 --- a/app/frontend/static/assets/css/shared/style.css +++ b/app/frontend/static/assets/css/shared/style.css @@ -26992,11 +26992,6 @@ div.tagsinput span.tag a { min-height: 100vh; } -.auth.auth-bg-1 { - background: url("../../images/auth/login_1.jpg"); - background-size: cover; -} - .auth.register-bg-1 { background: url("../../images/auth/register.jpg") center center no-repeat; background-size: cover; diff --git a/app/frontend/templates/panel/dashboard.html b/app/frontend/templates/panel/dashboard.html index c89d00b2..1fac58cf 100644 --- a/app/frontend/templates/panel/dashboard.html +++ b/app/frontend/templates/panel/dashboard.html @@ -168,22 +168,11 @@ {% if server['user_command_permission'] %} - {% if server['stats']['running'] %} - - -   - - - -   - - - -   - + {% if server['stats']['importing'] and server['stats']['running'] %} + +  {{ translate('serverTerm', 'installing', + data['lang']) }} {% elif server['stats']['updating']%} {{ translate('serverTerm', 'importing', data['lang']) }} + {% elif server['stats']['running'] %} + + +   + + + +   + + + +   {% else %} @@ -275,22 +279,27 @@ - {% if server['stats']['running'] %} - {{ translate('dashboard', 'online', - data['lang']) }} - {% elif server['stats']['crashed'] %} - {{ translate('dashboard', - 'crashed', - data['lang']) }} - {% else %} - {{ translate('dashboard', 'offline', - data['lang']) }} - {% end %} + + {% if server['stats']['running'] %} + {{ translate('dashboard', 'online', + data['lang']) }} + {% elif server['stats']['crashed'] %} + {{ translate('dashboard', + 'crashed', + data['lang']) }} + {% else %} + {{ translate('dashboard', 'offline', + data['lang']) }} + {% end %} +
+
{% end %} +
{% for server in data['failed_servers'] %}  
+
+
+
+
+ +

{{ translate('panelConfig', 'loginImage', data['lang']) }}

+
+

+ +

+ {% raw xsrf_form_html() %} + + +
+
+ +
+
+
+ + + + +
+
+
+
+
+ +

+
+
+
+
+
+

{{ translate('panelConfig', 'loginBackground', data['lang']) }}




+
+ +
+
+
{{ translate('panelConfig', 'preview', data['lang']) }}:
+ +
+
+
+ + +
+
+
+
+
{% end %} @@ -279,6 +342,13 @@ $('.too_small2').popover("hide"); } // New width }); + $('#file').change(function () { + console.log("File changed"); + if ($('#file').val()) { + $('#upload-button').prop("disabled", false); + console.log("File changed good"); + } + }); {% end %} \ No newline at end of file diff --git a/app/frontend/templates/panel/panel_edit_user.html b/app/frontend/templates/panel/panel_edit_user.html index de60388e..a40d2903 100644 --- a/app/frontend/templates/panel/panel_edit_user.html +++ b/app/frontend/templates/panel/panel_edit_user.html @@ -124,7 +124,7 @@ data['lang']) }}{% end %}
-