Merge branch 'dev' into bugfix/loop-detection

This commit is contained in:
Zedifus 2023-05-14 20:37:09 +01:00
commit d00b9fb4dd
25 changed files with 627 additions and 796 deletions

View File

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

View File

@ -1,9 +1,13 @@
# Changelog
## --- [4.0.23] - 2023/TBD
## --- [4.1.0] - 2023/TBD
### New features
TBD
### Refactor
- Frontend Ajax Refactor | Start using API to send Remote Comms to Server ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/565))
### Bug fixes
TBD
- Fix pipelines failing to build from gitlab pre-defined variable deprecation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/582))
- Fix incompatible buildx provenance meta, causing digest issues on GL/DH container registries ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/582))
- Fix Auth'd servers in roles | Refine server ordering ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/574))
### Tweaks
TBD
### Lang

View File

@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.0.23
# Crafty Controller 4.1.0
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?

View File

@ -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,6 +261,8 @@ class ServersController(metaclass=Singleton):
user.role_id
)
for role in role_servers:
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
@ -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):

View File

@ -89,6 +89,7 @@ class UsersController:
},
},
"hints": {"type": "boolean"},
"server_order": {"type": "string"},
}
# **********************************************************************************

View File

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

View File

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

View File

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

View File

@ -281,74 +281,7 @@ class AjaxHandler(BaseHandler):
exec_user["user_id"], server_id
)
if page == "send_command":
command = self.get_body_argument("command", default=None, strip=True)
server_id = self.get_argument("id", None)
if server_id is None:
logger.warning("Server ID not found in send_command ajax call")
Console.warning("Server ID not found in send_command ajax call")
srv_obj = self.controller.servers.get_server_instance_by_id(server_id)
if command == srv_obj.settings["stop_command"]:
logger.info(
"Stop command detected as terminal input - intercepting."
+ f"Starting Crafty's stop process for server with id: {server_id}"
)
self.controller.management.send_command(
exec_user["user_id"], server_id, self.get_remote_ip(), "stop_server"
)
command = None
elif command == "restart":
logger.info(
"Restart command detected as terminal input - intercepting."
+ f"Starting Crafty's stop process for server with id: {server_id}"
)
self.controller.management.send_command(
exec_user["user_id"],
server_id,
self.get_remote_ip(),
"restart_server",
)
command = None
if command:
if srv_obj.check_running():
srv_obj.send_command(command)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Sent command to "
f"{self.controller.servers.get_server_friendly_name(server_id)} "
f"terminal: {command}",
server_id,
self.get_remote_ip(),
)
elif page == "send_order":
self.controller.users.update_server_order(
exec_user["user_id"], bleach.clean(self.get_argument("order"))
)
return
elif page == "backup_now":
server_id = self.get_argument("id", None)
if server_id is None:
logger.error("Server ID is none. Canceling backup!")
return
server = self.controller.servers.get_server_instance_by_id(server_id)
self.controller.management.add_to_audit_log_raw(
self.controller.users.get_user_by_id(exec_user["user_id"])["username"],
exec_user["user_id"],
server_id,
f"Backup now executed for server {server_id} ",
source_ip=self.get_remote_ip(),
)
server.backup_server()
elif page == "select_photo":
if page == "select_photo":
if exec_user["superuser"]:
photo = urllib.parse.unquote(self.get_argument("photo", ""))
opacity = self.get_argument("opacity", 100)
@ -382,23 +315,6 @@ class AjaxHandler(BaseHandler):
self.controller.cached_login = "login_1.jpg"
return
elif page == "kill":
if not permissions["Commands"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Commands")
return
server_id = self.get_argument("id", None)
svr = self.controller.servers.get_server_instance_by_id(server_id)
try:
svr.kill()
time.sleep(5)
svr.cleanup_server_object()
svr.record_server_stats()
except Exception as e:
logger.error(
f"Could not find PID for requested termsig. Full error: {e}"
)
return
elif page == "eula":
server_id = self.get_argument("id", None)
svr = self.controller.servers.get_server_instance_by_id(server_id)
@ -624,12 +540,6 @@ class AjaxHandler(BaseHandler):
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "del_task":
if not permissions["Schedule"] in user_perms:
self.redirect("/panel/error?error=Unauthorized access to Tasks")
else:
sch_id = self.get_argument("schedule_id", "-404")
self.tasks_manager.remove_job(sch_id)
if page == "del_backup":
if not permissions["Backup"] in user_perms:
@ -668,84 +578,6 @@ class AjaxHandler(BaseHandler):
):
os.remove(file_path)
elif page == "delete_server":
if not permissions["Config"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument("id", None)
logger.info(
f"Removing server from panel for server: "
f"{self.controller.servers.get_server_friendly_name(server_id)}"
)
server_data = self.controller.servers.get_server_data(server_id)
server_name = server_data["server_name"]
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, False)
elif page == "delete_server_files":
if not permissions["Config"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument("id", None)
logger.info(
f"Removing server and all associated files for server: "
f"{self.controller.servers.get_server_friendly_name(server_id)}"
)
server_data = self.controller.servers.get_server_data(server_id)
server_name = server_data["server_name"]
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip(),
)
for server in self.controller.servers.failed_servers:
if server["server_id"] == int(server_id):
return
self.tasks_manager.remove_all_server_tasks(server_id)
self.controller.remove_server(server_id, True)
elif page == "delete_unloaded_server":
if not permissions["Config"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_id = self.get_argument("id", None)
logger.info(
f"Removing server and all associated files for server: "
f"{self.controller.servers.get_server_friendly_name(server_id)}"
)
server_data = self.controller.servers.get_server_data_by_id(server_id)
server_name = server_data["server_name"]
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Deleted server {server_id} named {server_name}",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.remove_all_server_tasks(server_id)
for item in self.controller.servers.failed_servers[:]:
if item["server_id"] == int(server_id):
self.controller.servers.failed_servers.remove(item)
self.controller.remove_unloaded_server(server_id)
def check_server_id(self, server_id, page_name):
if server_id is None:
logger.warning(

View File

@ -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
@ -1053,7 +1050,7 @@ class PanelHandler(BaseHandler):
page_data["schedule"]["cron_string"] = ""
page_data["schedule"]["delay"] = 0
page_data["schedule"]["time"] = ""
page_data["schedule"]["interval"] = ""
page_data["schedule"]["interval"] = 1
# we don't need to check difficulty here.
# We'll just default to basic for new schedules
page_data["schedule"]["difficulty"] = "basic"
@ -1556,156 +1553,6 @@ class PanelHandler(BaseHandler):
role = self.controller.roles.get_role(r)
exec_user_role.add(role["role_name"])
if page == "server_detail":
if not permissions[
"Config"
] in self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
):
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Config")
return
server_name = self.get_argument("server_name", None)
server_obj = self.controller.servers.get_server_obj(server_id)
shutdown_timeout = self.get_argument("shutdown_timeout", 60)
if superuser:
log_path = self.get_argument("log_path", "")
if log_path:
if Helpers.is_os_windows():
log_path.replace(" ", "^ ")
log_path = Helpers.wtol_path(log_path)
if not self.helper.validate_traversal(server_obj.path, log_path):
log_path = ""
executable = self.get_argument("executable", None)
execution_command = self.get_argument("execution_command", None)
server_ip = self.get_argument("server_ip", None)
server_port = self.get_argument("server_port", None)
if int(server_port) < 1 or int(server_port) > 65535:
self.redirect(
"/panel/error?error=Constraint Error: "
"Port must be greater than 0 and less than 65535"
)
return
executable_update_url = self.get_argument("executable_update_url", "")
show_status = int(float(self.get_argument("show_status", "0")))
else:
execution_command = server_obj.execution_command
executable = server_obj.executable
stop_command = self.get_argument("stop_command", None)
auto_start_delay = self.get_argument("auto_start_delay", "10")
auto_start = int(float(self.get_argument("auto_start", "0")))
crash_detection = int(float(self.get_argument("crash_detection", "0")))
logs_delete_after = int(float(self.get_argument("logs_delete_after", "0")))
java_selection = self.get_argument("java_selection", None)
# make sure there is no whitespace
ignored_exits = self.get_argument("ignored_exits", "").replace(" ", "")
# subpage = self.get_argument('subpage', None)
server_id = self.check_server_id()
if server_id is None:
return
if java_selection:
try:
if self.helper.is_os_windows():
execution_list = shlex.split(execution_command, posix=False)
else:
execution_list = shlex.split(execution_command, posix=True)
except ValueError:
self.redirect(
"/panel/error?error=Invalid execution command. Java path"
" must be surrounded by quotes."
" (Are you missing a closing quote?)"
)
if (
not any(
java_selection in path for path in Helpers.find_java_installs()
)
and java_selection != "java"
):
self.redirect(
"/panel/error?error=Attack attempted."
+ " A copy of this report is being sent to server owner."
)
self.controller.management.add_to_audit_log_raw(
exec_user["username"],
exec_user["user_id"],
server_id,
f"Attempted to send bad java path for {server_id}."
+ " Possible attack. Act accordingly.",
self.get_remote_ip(),
)
return
if java_selection != "java":
if self.helper.is_os_windows():
execution_list[0] = '"' + java_selection + '/bin/java"'
else:
execution_list[0] = '"' + java_selection + '"'
else:
execution_list[0] = "java"
execution_command = ""
for item in execution_list:
execution_command += item + " "
server_obj: Servers = self.controller.servers.get_server_obj(server_id)
stale_executable = server_obj.executable
# Compares old jar name to page data being passed.
# If they are different we replace the executable name in the
if str(stale_executable) != str(executable):
execution_command = execution_command.replace(
str(stale_executable), str(executable)
)
server_obj.server_name = server_name
server_obj.shutdown_timeout = shutdown_timeout
if superuser:
if Helpers.validate_traversal(
self.helper.get_servers_root_dir(), server_obj.path
):
server_obj.log_path = log_path
if Helpers.validate_traversal(
self.helper.get_servers_root_dir(), executable
):
server_obj.executable = executable
server_obj.execution_command = execution_command
server_obj.server_ip = server_ip
server_obj.server_port = server_port
server_obj.executable_update_url = executable_update_url
server_obj.show_status = show_status
else:
server_obj.log_path = server_obj.log_path
server_obj.executable = server_obj.executable
server_obj.execution_command = execution_command
server_obj.server_ip = server_obj.server_ip
server_obj.server_port = server_obj.server_port
server_obj.executable_update_url = server_obj.executable_update_url
server_obj.stop_command = stop_command
server_obj.auto_start_delay = auto_start_delay
server_obj.auto_start = auto_start
server_obj.crash_detection = crash_detection
server_obj.logs_delete_after = logs_delete_after
server_obj.ignored_exits = ignored_exits
failed = False
for servers in self.controller.servers.failed_servers:
if servers["server_id"] == int(server_id):
failed = True
if not failed:
self.controller.servers.update_server(server_obj)
else:
self.controller.servers.update_unloaded_server(server_obj)
self.controller.servers.init_all_servers()
self.controller.servers.crash_detection(server_obj)
self.controller.servers.refresh_server_settings(server_id)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Edited server {server_id} named {server_name}",
server_id,
self.get_remote_ip(),
)
self.redirect(f"/panel/server_detail?id={server_id}&subpage=config")
if page == "server_backup":
logger.debug(self.request.arguments)
@ -1802,336 +1649,6 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/config_json")
if page == "new_schedule":
server_id = self.check_server_id()
if not server_id:
return
if (
not permissions["Schedule"]
in self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
and not superuser
):
self.redirect(
"/panel/error?error=Unauthorized access: User not authorized"
)
return
difficulty = bleach.clean(self.get_argument("difficulty", None))
server_obj = self.controller.servers.get_server_obj(server_id)
enabled = bleach.clean(self.get_argument("enabled", "0"))
name = bleach.clean(self.get_argument("name", ""))
if difficulty == "basic":
action = bleach.clean(self.get_argument("action", None))
interval = bleach.clean(self.get_argument("interval", None))
interval_type = bleach.clean(self.get_argument("interval_type", None))
# only check for time if it's number of days
if interval_type == "days":
sch_time = bleach.clean(self.get_argument("time", None))
if int(interval) > 30:
self.redirect(
"/panel/error?error=Invalid argument."
" Days must be 30 or fewer."
)
return
if action == "command":
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
command = "stop_server"
elif action == "restart":
command = "restart_server"
elif action == "backup":
command = "backup_server"
elif difficulty == "reaction":
interval_type = "reaction"
action = bleach.clean(self.get_argument("action", None))
delay = bleach.clean(self.get_argument("delay", None))
parent = bleach.clean(self.get_argument("parent", None))
if action == "command":
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
command = "stop_server"
elif action == "restart":
command = "restart_server"
elif action == "backup":
command = "backup_server"
else:
interval_type = ""
cron_string = bleach.clean(self.get_argument("cron", ""))
if not croniter.is_valid(cron_string):
self.redirect(
"/panel/error?error=INVALID FORMAT: Invalid Cron Format."
)
return
action = bleach.clean(self.get_argument("action", None))
if action == "command":
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
command = "stop_server"
elif action == "restart":
command = "restart_server"
elif action == "backup":
command = "backup_server"
if bleach.clean(self.get_argument("enabled", "0")) == "1":
enabled = True
else:
enabled = False
if bleach.clean(self.get_argument("one_time", "0")) == "1":
one_time = True
else:
one_time = False
if interval_type == "days":
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": interval_type,
"interval": interval,
"command": command,
"start_time": sch_time,
"enabled": enabled,
"one_time": one_time,
"cron_string": "",
"parent": None,
"delay": 0,
}
elif difficulty == "reaction":
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": interval_type,
"interval": "",
# We'll base every interval off of a midnight start time.
"start_time": "",
"command": command,
"cron_string": "",
"enabled": enabled,
"one_time": one_time,
"parent": parent,
"delay": delay,
}
elif difficulty == "advanced":
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": "",
"interval": "",
# We'll base every interval off of a midnight start time.
"start_time": "",
"command": command,
"cron_string": cron_string,
"enabled": enabled,
"one_time": one_time,
"parent": None,
"delay": 0,
}
else:
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": interval_type,
"interval": interval,
"command": command,
"enabled": enabled,
# We'll base every interval off of a midnight start time.
"start_time": "00:00",
"one_time": one_time,
"cron_string": "",
"parent": None,
"delay": 0,
}
self.tasks_manager.schedule_job(job_data)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Edited server {server_id}: added scheduled job",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.reload_schedule_from_db()
self.redirect(f"/panel/server_detail?id={server_id}&subpage=schedules")
if page == "edit_schedule":
server_id = self.check_server_id()
if not server_id:
return
if (
not permissions["Schedule"]
in self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
and not superuser
):
self.redirect(
"/panel/error?error=Unauthorized access: User not authorized"
)
return
sch_id = self.get_argument("sch_id", None)
if sch_id is None:
self.redirect("/panel/error?error=Invalid Schedule ID")
difficulty = bleach.clean(self.get_argument("difficulty", None))
server_obj = self.controller.servers.get_server_obj(server_id)
enabled = bleach.clean(self.get_argument("enabled", "0"))
name = bleach.clean(self.get_argument("name", ""))
if difficulty == "basic":
action = bleach.clean(self.get_argument("action", None))
interval = bleach.clean(self.get_argument("interval", None))
interval_type = bleach.clean(self.get_argument("interval_type", None))
# only check for time if it's number of days
if interval_type == "days":
sch_time = bleach.clean(self.get_argument("time", None))
if int(interval) > 30:
self.redirect(
"/panel/error?error=Invalid argument."
" Days must be 30 or fewer."
)
return
if action == "command":
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
command = "stop_server"
elif action == "restart":
command = "restart_server"
elif action == "backup":
command = "backup_server"
elif difficulty == "reaction":
interval_type = "reaction"
action = bleach.clean(self.get_argument("action", None))
delay = bleach.clean(self.get_argument("delay", None))
parent = bleach.clean(self.get_argument("parent", None))
if action == "command":
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
command = "stop_server"
elif action == "restart":
command = "restart_server"
elif action == "backup":
command = "backup_server"
parent = bleach.clean(self.get_argument("parent", None))
else:
interval_type = ""
cron_string = bleach.clean(self.get_argument("cron", ""))
if not croniter.is_valid(cron_string):
self.redirect(
"/panel/error?error=INVALID FORMAT: Invalid Cron Format."
)
return
action = bleach.clean(self.get_argument("action", None))
if action == "command":
command = self.get_argument("command", None)
elif action == "start":
command = "start_server"
elif action == "stop":
command = "stop_server"
elif action == "restart":
command = "restart_server"
elif action == "backup":
command = "backup_server"
if bleach.clean(self.get_argument("enabled", "0")) == "1":
enabled = True
else:
enabled = False
if bleach.clean(self.get_argument("one_time", "0")) == "1":
one_time = True
else:
one_time = False
if interval_type == "days":
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": interval_type,
"interval": interval,
"command": command,
"start_time": sch_time,
"enabled": enabled,
"one_time": one_time,
"cron_string": "",
"parent": None,
"delay": 0,
}
elif difficulty == "advanced":
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": "",
"interval": "",
# We'll base every interval off of a midnight start time.
"start_time": "",
"command": command,
"cron_string": cron_string,
"delay": "",
"parent": "",
"enabled": enabled,
"one_time": one_time,
}
elif difficulty == "reaction":
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": interval_type,
"interval": "",
# We'll base every interval off of a midnight start time.
"start_time": "",
"command": command,
"cron_string": "",
"enabled": enabled,
"one_time": one_time,
"parent": parent,
"delay": delay,
}
else:
job_data = {
"name": name,
"server_id": server_id,
"action": action,
"interval_type": interval_type,
"interval": interval,
"command": command,
"enabled": enabled,
# We'll base every interval off of a midnight start time.
"start_time": "00:00",
"delay": "",
"parent": "",
"one_time": one_time,
"cron_string": "",
}
self.tasks_manager.update_job(sch_id, job_data)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Edited server {server_id}: updated schedule",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.reload_schedule_from_db()
self.redirect(f"/panel/server_detail?id={server_id}&subpage=schedules")
elif page == "edit_user":
if bleach.clean(self.get_argument("username", None)).lower() == "system":
self.redirect(

View File

@ -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:
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,

View File

@ -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:
# 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,6 +172,15 @@ class ApiServersServerIndexHandler(BaseApiHandler):
)
self.tasks_manager.remove_all_server_tasks(server_id)
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(

View File

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

View File

@ -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}})

View File

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

View File

@ -1,5 +1,5 @@
{
"major": 4,
"minor": 0,
"sub": 23
"minor": 1,
"sub": 0
}

View File

@ -647,10 +647,13 @@
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + server_id,
url: `/api/v2/servers/${server_id}/action/${command}`,
success: function (data) {
console.log("got response:");
console.log(data);
if (command === "clone_server" && data.status === "ok") {
window.location.reload();
}
/*setTimeout(function () {
if (command != 'start_server') {
location.reload();
@ -705,24 +708,6 @@
document.querySelector('.dynamicMsg').appendChild(parentEl);
}
function send_kill(server_id) {
/* this getCookie function is in base.html */
const token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/kill?id=' + server_id,
success: function (data) {
console.log("got response:");
console.log(data);
/*setTimeout(function () {
location.reload();
}, 10000);*/
}
});
}
function update_one_server_status(server) {
/* Mobile view update */
server_cpu = document.getElementById('server_cpu_' + server.id);
@ -901,17 +886,11 @@
},
callback: function (result) {
if (result) {
send_kill(server_id);
send_command(server_id, "kill_server");
let dialog = bootbox.dialog({
title: '{% raw translate("dashboard", "killing", data["lang"]) %}',
message: '<p><i class="fa fa-spin fa-spinner"></i> Loading...</p>'
});
dialog.init(function () {
setTimeout(function () {
location.reload();
}, 15000);
});
}
}
});
@ -1000,16 +979,6 @@
},
callback: function (result) {
if (result) {
cloneServer(server_id);
}
}
});
});
});
function cloneServer(server_id) {
send_command(server_id, 'clone_server');
bootbox.dialog({
backdrop: true,
@ -1018,6 +987,12 @@
closeButton: false,
});
}
}
});
});
});
</script>
<script src="/static/assets/vendors/js/jquery-ui.js"></script>
<link rel="stylesheet" href="/static/assets/vendors/css/jquery-ui.css">
@ -1069,12 +1044,12 @@
const token = getCookie("_xsrf")
$.ajax({
type: "POST",
type: "PATCH",
headers: { 'X-XSRFToken': token },
url: '/ajax/send_order?order=' + id_string,
data: {
order: id_string,
},
url: `/api/v2/users/@me`,
data: JSON.stringify({
server_order: id_string,
}),
success: function (data) {
console.log("got response:");
console.log(data);

View File

@ -321,9 +321,60 @@
return r ? r[1] : undefined;
}
function gather_server_json() {
servers = [];
for (s = 0; s < page_servers.length; s++){
mask = ""
for (i = 0; i < permissions.length; i++){
if ($(`#permission_${page_servers[s].id}_${permissions[i]}`).prop('checked')){
mask += "1"
}else{
mask += "0"
}
}
servers.push(JSON.stringify({"id": page_servers[s].id, "permissions": mask}));
}
return servers;
}
$( document ).ready(function() {
console.log( "ready!" );
});
const roleId = new URLSearchParams(document.location.search).get('id');
$("#config_form").on("submit", async function (e) {
e.preventDefault();
var token = getCookie("_xsrf")
let configForm = document.getElementById("config_form");
let formData = new FormData(configForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
let send_object = Object()
send_object.servers = []
send_object.name = formDataObject.role_name
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
let res = await fetch(`/api/v2/roles/${roleId}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
</script>

View File

@ -14,7 +14,8 @@
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }}
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
@ -76,10 +77,14 @@
<li class="playerItem">
<h3>{{ player }}</h3>
<div class="buttons">
<button onclick="send_command_to_server('ban {{ player }}')" type="button" class="btn btn-danger">Ban</button>
<button onclick="send_command_to_server('kick {{ player }}')" type="button" class="btn btn-outline-danger">Kick</button>
<button onclick="send_command_to_server('op {{ player }}')" type="button" class="btn btn-warning">OP</button>
<button onclick="send_command_to_server('deop {{ player }}')" type="button" class="btn btn-outline-warning">De-OP</button>
<button onclick="send_command_to_server('ban {{ player }}')" type="button"
class="btn btn-danger">Ban</button>
<button onclick="send_command_to_server('kick {{ player }}')" type="button"
class="btn btn-outline-danger">Kick</button>
<button onclick="send_command_to_server('op {{ player }}')" type="button"
class="btn btn-warning">OP</button>
<button onclick="send_command_to_server('deop {{ player }}')" type="button"
class="btn btn-outline-warning">De-OP</button>
</div>
</li>
{% end %}
@ -136,21 +141,22 @@
});
function send_command_to_server(command) {
async function send_command_to_server(command) {
console.log(command)
var token = getCookie("_xsrf")
console.log('sending command: ' + command)
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/send_command?id=' + serverId,
data: { command },
success: function (data) {
console.log("got response:");
console.log(data);
let res = await fetch(`/api/v2/servers/${serverId}/stdin`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: command,
});
let responseData = await res.text();
console.log("got response:");
console.log(responseData);
}

View File

@ -326,7 +326,7 @@
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/backup_now?id=' + server_id,
url: `/api/v2/servers/${server_id}/action/backup_server`,
success: function (data) {
return;
},

View File

@ -43,10 +43,7 @@
<div class="row">
<div class="col-md-6 col-sm-12">
<form class="forms-sample" method="post" id="config_form" action="/panel/server_detail">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="config">
<form class="forms-sample" method="post" id="config_form">
<div class="form-group">
<label for="server_name">{{ translate('serverConfig', 'serverName', data['lang']) }} <small
@ -96,7 +93,7 @@
</label>
<select class="form-select form-control form-control-lg select-css" id="java_selection"
name="java_selection" form="config_form">
<option value="">{{ translate('serverConfig',
<option value="none">{{ translate('serverConfig',
'javaNoChange', data['lang'])}}</option>
{% for path in data['java_versions'] %}
<option value="{{path}}">{{path}}</option>
@ -359,7 +356,7 @@
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server?id=' + serverId,
url: `/api/v2/servers/${serverId}`,
data: {
},
success: function (data) {
@ -373,7 +370,7 @@
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_server_files?id=' + serverId,
url: `/api/v2/servers/${serverId}?files=true`,
data: {
},
success: function (data) {
@ -393,7 +390,7 @@
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + serverId,
url: `/api/v2/servers/${serverId}/action/${command}`,
success: function (data) {
console.log("got response:");
console.log(data);
@ -522,7 +519,7 @@
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/delete_unloaded_server?id=' + serverId,
url: `/api/v2/servers/${serverId}`,
data: {
},
success: function (data) {
@ -550,11 +547,92 @@
$('.port-hint').popover("hide");
});
async function postFormFieldsAsJson({ url, formData }) {
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject);
//Set the fetch options (headers, body)
let fetchOptions = {
//HTTP method set to POST.
method: "PATCH",
//Set the headers that specify you're sending a JSON body request and accepting JSON response
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
// POST request body as JSON string.
body: formDataJsonString,
};
//Get the response body as JSON.
//If the response was not OK, throw an error.
let res = await fetch(url, fetchOptions);
//If the response is not ok throw an error (for debugging)
if (!res.ok) {
let error = await res.text();
throw new Error(error);
}
//If the response was OK, return the response body.
return res.json();
}
function replacer(key, value) {
if (key != "ignored_exits") {
if (typeof value == "boolean" || key === "executable_update_url") {
return value
} else {
return (isNaN(value) ? value : +value);
}
} else {
return value;
}
}
$(document).ready(function () {
let token = getCookie("_xsrf")
webSocket.on('remove_spinner', function () {
document.getElementById("update-spinner").style.visibility = "hidden";
});
$("#config_form").on("submit", async function (e) {
e.preventDefault();
var token = getCookie("_xsrf")
let configForm = document.getElementById("config_form");
let formData = new FormData(configForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.show_status = $("#show_status").prop('checked');
formDataObject.crash_detection = $("#crash_detection").prop('checked');
formDataObject.auto_start = $("#auto_start").prop('checked');
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
formDataJsonString["ignored_exits"] = toString(formDataJsonString["ignored_exits"]);
console.log(formDataJsonString.ignored_exits)
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${serverId}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
</script>

View File

@ -37,15 +37,12 @@
<div class="row">
<div class="col-md-8 col-sm-8">
{% if data['new_schedule'] == True %}
<form class="forms-sample" method="post"
<form class="forms-sample" method="post" id="new_schedule_form"
action="/panel/new_schedule?id={{ data['server_stats']['server_id']['server_id'] }}">
{% else %}
<form class="forms-sample" method="post"
<form class="forms-sample" method="post" id="schedule_form"
action="/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['schedule']['schedule_id'] }}">
{% end %}
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="config">
<div class="form-group">
<label for="name">{{ translate('serverSchedules', 'name' , data['lang']) }}</label>
@ -89,7 +86,7 @@
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'interval-explain' ,
data['lang']) }}</small> </label>
<input type="number" class="form-control" name="interval" id="interval"
value="{{ data['schedule']['interval'] }}" placeholder="Interval" required>
value="{{ data['schedule']['interval'] }}" placeholder="Interval" required min="1">
<br>
<br>
<select id="interval_type" onchange="ifDays(this);" name="interval_type"
@ -108,7 +105,7 @@
<label for="time">{{ translate('serverScheduleConfig', 'time' , data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'time-explain' ,
data['lang']) }}</small> </label>
<input type="time" class="form-control" name="time" id="time"
<input type="time" class="form-control" name="start_time" id="time"
value="{{ data['schedule']['time'] }}" placeholder="Time" required>
</div>
</div>
@ -127,7 +124,7 @@
<label for="cron">{{ translate('serverScheduleConfig', 'cron' , data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverScheduleConfig', 'cron-explain' , data['lang'])
}}</small> </label>
<input type="input" class="form-control" name="cron" id="cron"
<input type="input" class="form-control" name="cron_string" id="cron"
value="{{ data['schedule']['cron_string'] }}" placeholder="* * * * *">
</div>
</div>
@ -234,8 +231,120 @@
return r ? r[1] : undefined;
}
function replacer(key, value) {
if (key != "start_time" && key != "cron_string" && key != "interval_type") {
if (typeof value == "boolean") {
return value
}
console.log(key)
if (key === "interval" && value === ""){
return 0;
}
if (key === "command" && typeof(value === "integer")){
return value.toString();
}else {
return (isNaN(value) ? value : +value);
}
} else {
if (value === "" && key == "start_time"){
return "00:00";
}else{
return value;
}
}
}
const serverId = new URLSearchParams(document.location.search).get('id');
const schId = new URLSearchParams(document.location.search).get('sch_id');
$(document).ready(function () {
console.log("ready!");
$("#new_schedule_form").on("submit", async function (e) {
e.preventDefault();
var token = getCookie("_xsrf")
let schForm = document.getElementById("new_schedule_form");
let formData = new FormData(schForm);
formData.delete("difficulty");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.enabled = $("#enabled").prop('checked');
formDataObject.one_time = $("#one_time").prop('checked');
if ($("#difficulty").val() == "reaction"){
formDataObject.interval_type = "reaction";
}
if (formDataObject.cron_string != ""){
formDataObject.interval_type = '';
}
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${serverId}/tasks/`, {
method: 'POST',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = `/panel/server_detail?id=${serverId}&subpage=schedules`;
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
});
$("#schedule_form").on("submit", async function (e) {
e.preventDefault();
var token = getCookie("_xsrf")
let schForm = document.getElementById("schedule_form");
let formData = new FormData(schForm);
formData.delete("difficulty");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.enabled = $("#enabled").prop('checked');
formDataObject.one_time = $("#one_time").prop('checked');
if ($("#difficulty").val() == "reaction"){
formDataObject.interval_type = "reaction";
}
if (formDataObject.cron_string != ""){
formDataObject.interval_type = '';
}
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${serverId}/tasks/${schId}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = `/panel/server_detail?id=${serverId}&subpage=schedules`;
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
@ -265,6 +374,7 @@
document.getElementById("parent").required = true;
document.getElementById("interval").required = false;
document.getElementById("time").required = false;
$("#cron").val("");
}
else {
document.getElementById("ifAdvanced").style.display = "none";
@ -274,6 +384,7 @@
document.getElementById("parent").required = false;
document.getElementById("interval").required = true;
document.getElementById("time").required = true;
$("#cron").val("");
}
}
function ifDays() {
@ -286,22 +397,6 @@
}
}
function del_task(sch_id, id) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/del_task?server_id=' + id + '&schedule_id=' + sch_id,
data: {
schedule_id: sch_id,
id: id
},
success: function (data) {
location.reload();
},
});
}
function startup() {
try {
document.getElementById("{{ data['schedule']['interval_type'] }}").setAttribute('selected', true);

View File

@ -90,7 +90,7 @@
<p>{{schedule.command}}</p>
</td>
<td id="{{schedule.interval}}" class="action">
{% if schedule.interval != '' %}
{% if schedule.interval_type != '' and schedule.interval_type != 'reaction' %}
<p>{{ translate('serverSchedules', 'every', data['lang']) }}</p>
<p>{{schedule.interval}} {{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
@ -440,21 +440,19 @@
});
});
function del_task(sch_id, id) {
async function del_task(sch_id, id) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/del_task?server_id=' + id + '&schedule_id=' + sch_id,
data: {
schedule_id: sch_id,
id: id
},
success: function (data) {
location.reload();
let res = await fetch(`/api/v2/servers/${id}/tasks/${sch_id}`, {
method: 'DELETE',
headers: {
'token': token,
},
});
let responseData = await res;
if (responseData.statusText === "OK") {
window.location.reload();
}
}
</script>

View File

@ -179,7 +179,7 @@
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/server/command?command=' + command + '&id=' + serverId,
url: `/api/v2/servers/${serverId}/action/${command}`,
success: function (data) {
console.log("got response:");
console.log(data);
@ -311,12 +311,12 @@
formdata.append('command', serverCommand)
console.log('sending command: ' + serverCommand)
let res = await fetch("/ajax/send_command?id=" + serverId, {
let res = await fetch(`/api/v2/servers/${serverId}/stdin`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: formdata,
body: serverCommand,
});
let responseData = await res.text();

View File

@ -186,7 +186,8 @@
"terribleFailure": "What a Terrible Failure!",
"superError": "You must be a super user to complete this action.",
"fileError": "File type must be an image.",
"migration": "Crafty's main server storage is being mirgated to a new location. All server starts have been suspended during this time. Please wait while we finish this migration"
"migration": "Crafty's main server storage is being mirgated to a new location. All server starts have been suspended during this time. Please wait while we finish this migration",
"cronFormat": "Invalid Cron format detected"
},
"footer": {
"allRightsReserved": "All rights reserved",