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 @@
[](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 @@
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: '