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 @@
+ +