diff --git a/.gitignore b/.gitignore
index 132a2a26..d3b153ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@ app/config/
docker/*
!docker/docker-compose.yml
lang_sort_log.txt
+lang_sort.txt
diff --git a/.gitlab/scripts/sort.py b/.gitlab/scripts/sort.py
new file mode 100644
index 00000000..c78885a6
--- /dev/null
+++ b/.gitlab/scripts/sort.py
@@ -0,0 +1,72 @@
+import json
+import os
+
+
+def get_missing_keys_and_values(obj1, obj2, path=None):
+ if path is None:
+ path = []
+
+ missing_keys_and_values = {}
+
+ if isinstance(obj1, dict) and isinstance(obj2, dict):
+ for key in obj1:
+ if key not in obj2:
+ missing_keys_and_values[key] = obj1[key]
+ elif isinstance(obj1[key], (dict, list)) and isinstance(
+ obj2[key], (dict, list)
+ ):
+ sub_missing = get_missing_keys_and_values(
+ obj1[key], obj2[key], path + [key]
+ )
+ if sub_missing:
+ missing_keys_and_values[key] = sub_missing
+
+ return missing_keys_and_values
+
+
+def main():
+ project_dir = os.getcwd()
+ os.chdir("../../app/translations") # Change the working directory
+ dir_path = os.getcwd() # Get the current working directory
+
+ en_en_path = os.path.join(dir_path, "en_EN.json")
+
+ if not os.path.isfile(en_en_path):
+ print(
+ f"The file en_EN.json does not exist in {dir_path}. Ensure you have the right directory, Exiting."
+ )
+ return
+
+ result = {} # JSON object to store missing keys and values
+
+ for root, _, files in os.walk(dir_path):
+ for file in files:
+ if (
+ "_incomplete" not in file
+ and file != "en_EN.json"
+ and file.endswith(".json")
+ ):
+ file_path = os.path.join(root, file)
+
+ with open(file_path, "r", encoding="utf-8") as current_file:
+ current_data = json.load(current_file)
+
+ with open(en_en_path, "r", encoding="utf-8") as en_en_file:
+ en_en_data = json.load(en_en_file)
+
+ missing_keys_and_values = get_missing_keys_and_values(
+ en_en_data, current_data
+ )
+ if missing_keys_and_values:
+ result[file] = missing_keys_and_values
+
+ # Write the JSON object to lang_sort.txt
+ with open(
+ os.path.join(project_dir, "lang_sort.txt"),
+ "w",
+ encoding="utf-8",
+ ) as output_file:
+ json.dump(result, output_file, indent=4)
+
+
+main()
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7054c1b1..1c0dab4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,36 @@
# Changelog
+## --- [4.2.2] - 2023/12/13
+### New features
+- Loading Screen for Crafty during startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
+### Refactor
+- Remove deprecated API V1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/670))
+- Tidy up main.py to be more comprehensive ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
+- Force random password on first run. Stop using common default password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/672) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/673))
+### Bug fixes
+- Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664))
+- Bump cryptography for CVE-2023-49083 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/680))
+- Fix bug where su cannot edit general user password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/676))
+- Fix bug where no file error on import root dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/677))
+- Fix Unban button failing to pardon users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/671))
+- Fix stack in API error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/674))
+- Fix bug where you cannot select "do not monitor mounts" from `config.json` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/678))
+- Fix support log 'x' button still downloading logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/679))
+- Fix bug where servers are created without bu dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/682))
+### Tweaks
+- Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666))
+- Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
+- Add server name tag in panel header (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
+- Setup logging for panel authentication attempts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
+- Update minimum password length from 6 to 8, and unrestrict maximum password length ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
+- Give better feedback when backup delete fails ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/681))
+- Add user queue debug logging ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/683))
+### Lang
+- Update `de_DE, en_EN, fr_FR, lol_EN, lv_LV, nl_BE, pl_PL, zh_CN` translations for `4.2.2` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684))
+- Mark `es_ES` as incomplete ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684))
+- Mark `he_IL` as active ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684))
+- pl_PL Minor fixes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/675))
+
+
## --- [4.2.1] - 2023/11/01
### Bug fixes
- Fix logic issue with `get_files` API permissions check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/654))
diff --git a/README.md b/README.md
index a805eca7..0a94b6cb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
-# Crafty Controller 4.2.1
+# Crafty Controller 4.2.2
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py
index c0bae7b0..88923194 100644
--- a/app/classes/controllers/servers_controller.py
+++ b/app/classes/controllers/servers_controller.py
@@ -22,6 +22,7 @@ from app.classes.models.server_permissions import (
PermissionsServers,
EnumPermissionsServer,
)
+from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@@ -36,6 +37,8 @@ class ServersController(metaclass=Singleton):
self.management_helper = management_helper
self.servers_list = []
self.stats = Stats(self.helper, self)
+ self.web_sock = WebSocketManager()
+ self.server_subpage = {}
# **********************************************************************************
# Generic Servers Methods
@@ -169,8 +172,15 @@ class ServersController(metaclass=Singleton):
def init_all_servers(self):
servers = self.get_all_defined_servers()
self.failed_servers = []
-
for server in servers:
+ self.web_sock.broadcast_to_admins(
+ "update",
+ {"section": "server", "server": server["server_name"]},
+ )
+ self.web_sock.broadcast_to_non_admins(
+ "update",
+ {"section": "init"},
+ )
server_id = server.get("server_id")
# if we have already initialized this server, let's skip it.
diff --git a/app/classes/controllers/users_controller.py b/app/classes/controllers/users_controller.py
index ed53ad61..87cc513c 100644
--- a/app/classes/controllers/users_controller.py
+++ b/app/classes/controllers/users_controller.py
@@ -45,8 +45,7 @@ class UsersController:
},
"password": {
"type": "string",
- "maxLength": 20,
- "minLength": 6,
+ "minLength": 8,
"examples": ["crafty"],
"title": "Password",
},
diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py
index 62ce8819..9c63a323 100644
--- a/app/classes/shared/helpers.py
+++ b/app/classes/shared/helpers.py
@@ -80,6 +80,7 @@ class Helpers:
self.translation = Translation(self)
self.update_available = False
self.ignored_names = ["crafty_managed.txt", "db_stats"]
+ self.crafty_starting = False
@staticmethod
def auto_installer_fix(ex):
@@ -361,6 +362,42 @@ class Helpers:
return result_of_check == 0
+ def create_pass(self):
+ # Maximum length of password needed
+ max_len = 64
+
+ # Declare string of the character that we need in our password
+ digits = string.digits
+ locase = string.ascii_lowercase
+ upcase = string.ascii_uppercase
+ symbols = "!@#$%^&*" # Reducing to avoid issues with ([]{}<>,'`) etc
+
+ # Combine all the character strings above to form one string
+ combo = digits + upcase + locase + symbols
+
+ # Randomly select at least one character from each character set above
+ rand_digit = secrets.choice(digits)
+ rand_upper = secrets.choice(upcase)
+ rand_lower = secrets.choice(locase)
+ rand_symbol = secrets.choice(symbols)
+
+ # Combine the character randomly selected above
+ temp_pass = rand_digit + rand_upper + rand_lower + rand_symbol
+
+ # Fill the rest of the password length by selecting randomly char list
+ for _ in range(max_len - 4):
+ temp_pass += secrets.choice(combo)
+
+ # Shuffle the temporary password to prevent predictable patterns
+ temp_pass_list = list(temp_pass)
+ secrets.SystemRandom().shuffle(temp_pass_list)
+
+ # Form the password by concatenating the characters
+ password = "".join(temp_pass_list)
+
+ # Return completed password
+ return password
+
@staticmethod
def cmdparse(cmd_in):
# Parse a string into arguments
diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py
index 23586696..ae58d988 100644
--- a/app/classes/shared/main_controller.py
+++ b/app/classes/shared/main_controller.py
@@ -78,6 +78,37 @@ class Controller:
self.first_login = False
self.cached_login = self.management.get_login_image()
self.support_scheduler.start()
+ try:
+ with open(
+ os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
+ "r",
+ encoding="utf-8",
+ ) as f:
+ self.auth_tracker = json.load(f)
+ except (FileNotFoundError, json.JSONDecodeError):
+ self.auth_tracker = {}
+
+ def log_attempt(self, remote_ip, username):
+ remote = self.auth_tracker.get(str(remote_ip), None)
+ if remote:
+ remote["names"].append(username)
+ remote["attempts"] += 1
+ remote["times"].append(datetime.now().strftime("%d/%m/%Y %H:%M:%S"))
+ self.auth_tracker[str(remote_ip)] = remote
+ else:
+ self.auth_tracker[str(remote_ip)] = {
+ "names": [username],
+ "attempts": 1,
+ "times": [datetime.now().strftime("%d/%m/%Y %H:%M:%S")],
+ }
+
+ def write_auth_tracker(self):
+ with open(
+ os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
+ "w",
+ encoding="utf-8",
+ ) as f:
+ json.dump(self.auth_tracker, f, indent=4)
@staticmethod
def check_system_user():
@@ -498,6 +529,10 @@ class Controller:
server_host=monitoring_host,
server_type=monitoring_type,
)
+ self.management.set_backup_config(
+ new_server_id,
+ backup_path,
+ )
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
# modded update urls from server jars will only update the installer
diff --git a/app/classes/shared/main_models.py b/app/classes/shared/main_models.py
index 4bfca52c..c166b7fb 100644
--- a/app/classes/shared/main_models.py
+++ b/app/classes/shared/main_models.py
@@ -14,13 +14,17 @@ class DatabaseBuilder:
self.management_helper = management_helper
self.users_helper = users_helper
- def default_settings(self):
+ def default_settings(self, password="crafty"):
logger.info("Fresh Install Detected - Creating Default Settings")
Console.info("Fresh Install Detected - Creating Default Settings")
default_data = self.helper.find_default_password()
-
+ if password not in default_data:
+ Console.help(
+ "No default password found. Using password created "
+ "by Crafty. Find it in app/config/default-creds.txt"
+ )
username = default_data.get("username", "admin")
- password = default_data.get("password", "crafty")
+ password = default_data.get("password", password)
self.users_helper.add_user(
username=username,
diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py
index 0402c587..a5ea32ac 100644
--- a/app/classes/shared/tasks.py
+++ b/app/classes/shared/tasks.py
@@ -23,6 +23,7 @@ from app.classes.web.tornado_handler import Webserver
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger("apscheduler")
+command_log = logging.getLogger("cmd_queue")
scheduler_intervals = {
"seconds",
"minutes",
@@ -94,7 +95,15 @@ class TasksManager:
def command_watcher(self):
while True:
# select any commands waiting to be processed
+ command_log.debug(
+ "Queue currently has "
+ f"{self.controller.management.command_queue.qsize()} queued commands."
+ )
if not self.controller.management.command_queue.empty():
+ command_log.info(
+ "Current queued commands: "
+ f"{list(self.controller.management.command_queue.queue)}"
+ )
cmd = self.controller.management.command_queue.get()
try:
svr = self.controller.servers.get_server_instance_by_id(
@@ -201,6 +210,13 @@ class TasksManager:
id="update_watcher",
start_date=datetime.datetime.now(),
)
+ self.scheduler.add_job(
+ self.controller.write_auth_tracker,
+ "interval",
+ minutes=5,
+ id="auth_tracker_write",
+ start_date=datetime.datetime.now(),
+ )
# self.scheduler.add_job(
# self.scheduler.print_jobs, "interval", seconds=10, id="-1"
# )
diff --git a/app/classes/shared/websocket_manager.py b/app/classes/shared/websocket_manager.py
index f48adef8..7cda296d 100644
--- a/app/classes/shared/websocket_manager.py
+++ b/app/classes/shared/websocket_manager.py
@@ -37,7 +37,15 @@ class WebSocketManager(metaclass=Singleton):
def broadcast_to_admins(self, event_type: str, data):
def filter_fn(client):
- if client.get_user_id in HelperUsers.get_super_user_list():
+ if str(client.get_user_id()) in str(HelperUsers.get_super_user_list()):
+ return True
+ return False
+
+ self.broadcast_with_fn(filter_fn, event_type, data)
+
+ def broadcast_to_non_admins(self, event_type: str, data):
+ def filter_fn(client):
+ if str(client.get_user_id()) not in str(HelperUsers.get_super_user_list()):
return True
return False
diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py
deleted file mode 100644
index 34b09ee8..00000000
--- a/app/classes/web/api_handler.py
+++ /dev/null
@@ -1,446 +0,0 @@
-from datetime import datetime
-import logging
-import re
-
-from app.classes.controllers.crafty_perms_controller import EnumPermissionsCrafty
-from app.classes.controllers.server_perms_controller import EnumPermissionsServer
-from app.classes.web.base_handler import BaseHandler
-from app.classes.models.management import DatabaseShortcuts
-
-logger = logging.getLogger(__name__)
-bearer_pattern = re.compile(r"^Bearer", flags=re.IGNORECASE)
-
-
-class ApiHandler(BaseHandler):
- def return_response(self, status: int, data: dict):
- # Define a standardized response
- self.set_status(status)
- self.write(data)
-
- def check_xsrf_cookie(self):
- # Disable CSRF protection on API routes
- pass
-
- def access_denied(self, user, reason=""):
- if reason:
- reason = " because " + reason
- logger.info(
- "User %s from IP %s was denied access to the API route "
- + self.request.path
- + reason,
- user,
- self.get_remote_ip(),
- )
- self.finish(
- self.return_response(
- 403,
- {
- "error": "ACCESS_DENIED",
- "info": "You were denied access to the requested resource",
- },
- )
- )
-
- def authenticate_user(self) -> bool:
- self.permissions = {
- "Commands": EnumPermissionsServer.COMMANDS,
- "Terminal": EnumPermissionsServer.TERMINAL,
- "Logs": EnumPermissionsServer.LOGS,
- "Schedule": EnumPermissionsServer.SCHEDULE,
- "Backup": EnumPermissionsServer.BACKUP,
- "Files": EnumPermissionsServer.FILES,
- "Config": EnumPermissionsServer.CONFIG,
- "Players": EnumPermissionsServer.PLAYERS,
- "Server_Creation": EnumPermissionsCrafty.SERVER_CREATION,
- "User_Config": EnumPermissionsCrafty.USER_CONFIG,
- "Roles_Config": EnumPermissionsCrafty.ROLES_CONFIG,
- }
- try:
- logger.debug("Searching for specified token")
-
- api_token = self.get_argument("token", "")
- self.api_token = api_token
- if api_token is None and self.request.headers.get("Authorization"):
- api_token = bearer_pattern.sub(
- "", self.request.headers.get("Authorization")
- )
- elif api_token is None:
- api_token = self.get_cookie("token")
- user_data = self.controller.users.get_user_by_api_token(api_token)
-
- logger.debug("Checking results")
- if user_data:
- # Login successful! Check perms
- logger.info(f"User {user_data['username']} has authenticated to API")
-
- return True # This is to set the "authenticated"
- logging.debug("Auth unsuccessful")
- self.access_denied("unknown", "the user provided an invalid token")
- return False
- except Exception as e:
- logger.warning("An error occured while authenticating an API user: %s", e)
- self.finish(
- self.return_response(
- 403,
- {
- "error": "ACCESS_DENIED",
- "info": "An error occured while authenticating the user",
- },
- )
- )
- return False
-
-
-class ServersStats(ApiHandler):
- def get(self):
- """Get details about all servers"""
- authenticated = self.authenticate_user()
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
- if not authenticated:
- return
- if user_obj["superuser"]:
- raw_stats = self.controller.servers.get_all_servers_stats()
- else:
- raw_stats = self.controller.servers.get_authorized_servers_stats(
- user_obj["user_id"]
- )
- stats = []
- for rs in raw_stats:
- s = {}
- for k, v in rs["server_data"].items():
- if isinstance(v, datetime):
- s[k] = v.timestamp()
- else:
- s[k] = v
- stats.append(s)
-
- # Get server stats
- # TODO Check perms
- self.finish(self.write({"servers": stats}))
-
-
-class NodeStats(ApiHandler):
- def get(self):
- """Get stats for particular node"""
- authenticated = self.authenticate_user()
- if not authenticated:
- return
-
- # Get node stats
- node_stats = self.controller.servers.stats.get_node_stats()
- self.return_response(200, {"code": node_stats["node_stats"]})
-
-
-class SendCommand(ApiHandler):
- def post(self):
- user = self.authenticate_user()
-
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- if user is None:
- self.access_denied("unknown")
- return
- server_id = self.get_argument("id")
-
- if (
- not user_obj["user_id"]
- in self.controller.server_perms.get_server_user_list(server_id)
- and not user_obj["superuser"]
- ):
- self.access_denied("unknown")
- return
-
- if not self.permissions[
- "Commands"
- ] in self.controller.server_perms.get_api_key_permissions_list(
- self.controller.users.get_api_key_by_token(self.api_token), server_id
- ):
- self.access_denied(user)
- return
-
- command = self.get_argument("command", default=None, strip=True)
- server_id = self.get_argument("id")
- if command:
- server = self.controller.servers.get_server_instance_by_id(server_id)
- if server.check_running:
- server.send_command(command)
- self.return_response(200, {"run": True})
- else:
- self.return_response(200, {"error": "SER_NOT_RUNNING"})
- else:
- self.return_response(200, {"error": "NO_COMMAND"})
-
-
-class ServerBackup(ApiHandler):
- def post(self):
- user = self.authenticate_user()
-
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- if user is None:
- self.access_denied("unknown")
- return
- server_id = self.get_argument("id")
-
- if (
- not user_obj["user_id"]
- in self.controller.server_perms.get_server_user_list(server_id)
- and not user_obj["superuser"]
- ):
- self.access_denied("unknown")
- return
-
- if not self.permissions[
- "Backup"
- ] in self.controller.server_perms.get_api_key_permissions_list(
- self.controller.users.get_api_key_by_token(self.api_token), server_id
- ):
- self.access_denied(user)
- return
-
- server = self.controller.servers.get_server_instance_by_id(server_id)
-
- server.backup_server()
-
- self.return_response(200, {"code": "SER_BAK_CALLED"})
-
-
-class StartServer(ApiHandler):
- def post(self):
- user = self.authenticate_user()
- remote_ip = self.get_remote_ip()
-
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- if user is None:
- self.access_denied("unknown")
- return
- server_id = self.get_argument("id")
-
- if (
- not user_obj["user_id"]
- in self.controller.server_perms.get_server_user_list(server_id)
- and not user_obj["superuser"]
- ):
- self.access_denied("unknown")
- return
- if not self.permissions[
- "Commands"
- ] in self.controller.server_perms.get_api_key_permissions_list(
- self.controller.users.get_api_key_by_token(self.api_token), server_id
- ):
- self.access_denied("unknown")
- return
-
- server = self.controller.servers.get_server_instance_by_id(server_id)
-
- if not server.check_running():
- self.controller.management.send_command(
- user_obj["user_id"], server_id, remote_ip, "start_server"
- )
- self.return_response(200, {"code": "SER_START_CALLED"})
- else:
- self.return_response(500, {"error": "SER_RUNNING"})
-
-
-class StopServer(ApiHandler):
- def post(self):
- user = self.authenticate_user()
- remote_ip = self.get_remote_ip()
-
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- if user is None:
- self.access_denied("unknown")
- return
- server_id = self.get_argument("id")
-
- if (
- not user_obj["user_id"]
- in self.controller.server_perms.get_server_user_list(server_id)
- and not user_obj["superuser"]
- ):
- self.access_denied("unknown")
-
- if not self.permissions[
- "Commands"
- ] in self.controller.server_perms.get_api_key_permissions_list(
- self.controller.users.get_api_key_by_token(self.api_token), server_id
- ):
- self.access_denied(user)
- return
-
- server = self.controller.servers.get_server_instance_by_id(server_id)
-
- if server.check_running():
- self.controller.management.send_command(
- user, server_id, remote_ip, "stop_server"
- )
-
- self.return_response(200, {"code": "SER_STOP_CALLED"})
- else:
- self.return_response(500, {"error": "SER_NOT_RUNNING"})
-
-
-class RestartServer(ApiHandler):
- def post(self):
- user = self.authenticate_user()
- remote_ip = self.get_remote_ip()
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- if user is None:
- self.access_denied("unknown")
- return
- server_id = self.get_argument("id")
-
- if not user_obj["user_id"] in self.controller.server_perms.get_server_user_list(
- server_id
- ):
- self.access_denied("unknown")
-
- if not self.permissions[
- "Commands"
- ] in self.controller.server_perms.get_api_key_permissions_list(
- self.controller.users.get_api_key_by_token(self.api_token), server_id
- ):
- self.access_denied(user)
-
- self.controller.management.send_command(
- user, server_id, remote_ip, "restart_server"
- )
- self.return_response(200, {"code": "SER_RESTART_CALLED"})
-
-
-class CreateUser(ApiHandler):
- def post(self):
- user = self.authenticate_user()
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
- user_obj["user_id"]
- )
- if (
- not self.permissions["User_Config"] in user_perms
- and not user_obj["superuser"]
- ):
- self.access_denied("unknown")
- return
-
- if user is None:
- self.access_denied("unknown")
- return
-
- if not self.permissions[
- "User_Config"
- ] in self.controller.crafty_perms.get_api_key_permissions_list(
- self.controller.users.get_api_key_by_token(self.api_token)
- ):
- self.access_denied(user)
- return
-
- new_username = self.get_argument("username").lower()
- new_pass = self.get_argument("password")
- manager = int(user_obj["user_id"])
-
- if new_username:
- self.controller.users.add_user(
- new_username, manager, new_pass, "default@example.com", True, False
- )
-
- self.return_response(
- 200,
- {
- "code": "COMPLETE",
- "username": new_username,
- "password": new_pass,
- },
- )
- else:
- self.return_response(
- 500,
- {
- "error": "MISSING_PARAMS",
- "info": "Some paramaters failed validation",
- },
- )
-
-
-class DeleteUser(ApiHandler):
- def post(self):
- user = self.authenticate_user()
-
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
- user_obj["user_id"]
- )
-
- if (
- not self.permissions["User_Config"] in user_perms
- and not user_obj["superuser"]
- ):
- self.access_denied("unknown")
- return
-
- if user is None:
- self.access_denied("unknown")
- return
-
- if not self.permissions[
- "User_Config"
- ] in self.controller.crafty_perms.get_api_key_permissions_list(
- self.controller.users.get_api_key_by_token(self.api_token)
- ):
- self.access_denied(user)
- return
-
- user_id = self.get_argument("user_id", None, True)
- user_to_del = self.controller.users.get_user_by_id(user_id)
-
- if user_to_del["superuser"]:
- self.return_response(
- 500,
- {"error": "NOT_ALLOWED", "info": "You cannot delete a super user"},
- )
- else:
- if user_id:
- self.controller.users.remove_user(user_id)
- self.return_response(200, {"code": "COMPLETED"})
-
-
-class ListServers(ApiHandler):
- def get(self):
- user = self.authenticate_user()
- user_obj = self.controller.users.get_user_by_api_token(self.api_token)
-
- if user is None:
- self.access_denied("unknown")
- return
-
- if self.api_token is None:
- self.access_denied("unknown")
- return
-
- if user_obj["superuser"]:
- servers = self.controller.servers.get_all_defined_servers()
- servers = [str(i) for i in servers]
- else:
- servers = self.controller.servers.get_authorized_servers(
- user_obj["user_id"]
- )
- page_servers = []
- for server in servers:
- if server not in page_servers:
- page_servers.append(
- DatabaseShortcuts.get_data_obj(server.server_object)
- )
- servers = page_servers
- servers = [str(i) for i in servers]
-
- self.return_response(
- 200,
- {
- "code": "COMPLETED",
- "servers": servers,
- },
- )
diff --git a/app/classes/web/base_handler.py b/app/classes/web/base_handler.py
index 2504bc13..d8181b94 100644
--- a/app/classes/web/base_handler.py
+++ b/app/classes/web/base_handler.py
@@ -14,6 +14,7 @@ from app.classes.shared.translation import Translation
from app.classes.shared.main_models import DatabaseShortcuts
logger = logging.getLogger(__name__)
+auth_log = logging.getLogger("auth")
bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE)
@@ -231,9 +232,16 @@ class BaseHandler(tornado.web.RequestHandler):
user,
)
logging.debug("Auth unsuccessful")
+ auth_log.error(
+ f"Authentication attempted from {self.get_remote_ip()}. Invalid token"
+ )
self.access_denied(None, "the user provided an invalid token")
return None
except Exception as auth_exception:
+ auth_log.error(
+ f"Authentication attempted from {self.get_remote_ip()}."
+ f" Error: {auth_exception}"
+ )
logger.debug(
"An error occured while authenticating an API user:",
exc_info=auth_exception,
diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py
index e1d21f03..8ac827c3 100644
--- a/app/classes/web/panel_handler.py
+++ b/app/classes/web/panel_handler.py
@@ -210,6 +210,8 @@ class PanelHandler(BaseHandler):
error = self.get_argument("error", "WTF Error!")
template = "panel/denied.html"
+ if self.helper.crafty_starting:
+ page = "loading"
now = time.time()
formatted_time = str(
@@ -243,9 +245,13 @@ class PanelHandler(BaseHandler):
for r in exec_user["roles"]:
role = self.controller.roles.get_role(r)
exec_user_role.add(role["role_name"])
- defined_servers = self.controller.servers.get_authorized_servers(
- exec_user["user_id"]
- )
+ # get_auth_servers will throw an exception if run while Crafty is starting
+ if not self.helper.crafty_starting:
+ defined_servers = self.controller.servers.get_authorized_servers(
+ exec_user["user_id"]
+ )
+ else:
+ defined_servers = []
user_order = self.controller.users.get_user_by_id(exec_user["user_id"])
user_order = user_order["server_order"].split(",")
@@ -481,6 +487,12 @@ class PanelHandler(BaseHandler):
subpage = nh3.clean(self.get_argument("subpage", ""))
server_id = self.check_server_id()
+ # load page the user was on last
+ server_subpage = self.controller.servers.server_subpage.get(server_id, "")
+ if subpage == "" and server_subpage != "":
+ subpage = self.controller.servers.server_subpage.get(server_id, "")
+ else:
+ self.controller.servers.server_subpage[server_id] = subpage
if server_id is None:
return
if not self.failed_server:
@@ -1609,7 +1621,8 @@ class PanelHandler(BaseHandler):
logs_thread.start()
self.redirect("/panel/dashboard")
return
-
+ if self.helper.crafty_starting:
+ template = "panel/loading.html"
self.render(
template,
data=page_data,
diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py
index b7d1be9b..57e6ddd8 100644
--- a/app/classes/web/public_handler.py
+++ b/app/classes/web/public_handler.py
@@ -6,6 +6,7 @@ from app.classes.models.users import HelperUsers
from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__)
+auth_log = logging.getLogger("auth")
class PublicHandler(BaseHandler):
@@ -96,18 +97,27 @@ class PublicHandler(BaseHandler):
page_data["query"] = self.request.query
if page == "login":
+ auth_log.info(
+ f"User attempting to authenticate from {self.get_remote_ip()}"
+ )
next_page = "/login"
if self.request.query:
next_page = "/login?" + self.request.query
entered_username = nh3.clean(self.get_argument("username"))
- entered_password = nh3.clean(self.get_argument("password"))
+ entered_password = self.get_argument("password")
# pylint: disable=no-member
try:
user_id = HelperUsers.get_user_id_by_name(entered_username.lower())
user_data = HelperUsers.get_user_model(user_id)
except:
+ self.controller.log_attempt(self.get_remote_ip(), entered_username)
+ auth_log.error(
+ f"User attempted to log into {entered_username}."
+ f" Authentication failed from remote IP {self.get_remote_ip()}"
+ " Users does not exist."
+ )
error_msg = "Incorrect username or password. Please try again."
# self.clear_cookie("user")
# self.clear_cookie("user_data")
@@ -120,6 +130,12 @@ class PublicHandler(BaseHandler):
# if we don't have a user
if not user_data:
+ auth_log.error(
+ f"User attempted to log into {entered_username}. Authentication"
+ f" failed from remote IP {self.get_remote_ip()}"
+ " User does not exist."
+ )
+ self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = "Incorrect username or password. Please try again."
# self.clear_cookie("user")
# self.clear_cookie("user_data")
@@ -132,6 +148,12 @@ class PublicHandler(BaseHandler):
# if they are disabled
if not user_data.enabled:
+ auth_log.error(
+ f"User attempted to log into {entered_username}. "
+ f"Authentication failed from remote IP {self.get_remote_ip()}."
+ " User account disabled"
+ )
+ self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = (
"User account disabled. Please contact "
"your system administrator for more info."
@@ -159,7 +181,11 @@ class PublicHandler(BaseHandler):
user_data.last_ip = self.get_remote_ip()
user_data.last_login = Helpers.get_time_as_string()
user_data.save()
-
+ auth_log.info(
+ f"{entered_username} successfully"
+ " authenticated and logged"
+ f" into panel from remote IP {self.get_remote_ip()}"
+ )
# log this login
self.controller.management.add_to_audit_log(
user_data.user_id, "Logged in", 0, self.get_remote_ip()
@@ -172,6 +198,11 @@ class PublicHandler(BaseHandler):
self.redirect(next_page)
else:
+ auth_log.error(
+ f"User attempted to log into {entered_username}."
+ f" Authentication failed from remote IP {self.get_remote_ip()}"
+ )
+ self.controller.log_attempt(self.get_remote_ip(), entered_username)
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.clear_cookie("token")
diff --git a/app/classes/web/routes/api/auth/login.py b/app/classes/web/routes/api/auth/login.py
index 84ae2815..b91b295d 100644
--- a/app/classes/web/routes/api/auth/login.py
+++ b/app/classes/web/routes/api/auth/login.py
@@ -7,7 +7,7 @@ from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
-
+auth_log = logging.getLogger("auth")
login_schema = {
"type": "object",
"properties": {
@@ -29,6 +29,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
+ logger.error(
+ "Invalid JSON schema for API"
+ f" login attempt from {self.get_remote_ip()}"
+ )
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
@@ -36,6 +40,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
try:
validate(data, login_schema)
except ValidationError as e:
+ logger.error(
+ "Invalid JSON schema for API"
+ f" login attempt from {self.get_remote_ip()}"
+ )
return self.finish_json(
400,
{
@@ -52,12 +60,23 @@ class ApiAuthLoginHandler(BaseApiHandler):
user_data = Users.get_or_none(Users.username == username)
if user_data is None:
+ self.controller.log_attempt(self.get_remote_ip(), username)
+ auth_log.error(
+ f"User attempted to log into {username}."
+ " Authentication failed from remote IP"
+ f" {self.get_remote_ip()}. User not found"
+ )
return self.finish_json(
401,
{"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None},
)
if not user_data.enabled:
+ auth_log.error(
+ f"User attempted to log into {username}."
+ " Authentication failed from remote"
+ f" IP {self.get_remote_ip()} account disabled"
+ )
self.finish_json(
403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None}
)
@@ -67,6 +86,11 @@ class ApiAuthLoginHandler(BaseApiHandler):
# Valid Login
if login_result:
+ auth_log.info(
+ f"{username} successfully"
+ " authenticated and logged"
+ f" into panel from remote IP {self.get_remote_ip()}"
+ )
logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}")
# record this login
diff --git a/app/classes/web/routes/api/crafty/imports/index.py b/app/classes/web/routes/api/crafty/imports/index.py
index e6c8c548..2aca2fa9 100644
--- a/app/classes/web/routes/api/crafty/imports/index.py
+++ b/app/classes/web/routes/api/crafty/imports/index.py
@@ -7,6 +7,7 @@ from jsonschema.exceptions import ValidationError
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
+from app.classes.web.websocket_handler import WebSocketManager
logger = logging.getLogger(__name__)
files_get_schema = {
@@ -73,7 +74,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
else:
if user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
- self.helper.websocket_helper.broadcast_user(
+ WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@@ -85,7 +86,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
else:
if not self.helper.check_path_exists(folder) and user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
- self.helper.websocket_helper.broadcast_user(
+ WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
diff --git a/app/classes/web/routes/api/roles/index.py b/app/classes/web/routes/api/roles/index.py
index 0d46e11b..b0c773a7 100644
--- a/app/classes/web/routes/api/roles/index.py
+++ b/app/classes/web/routes/api/roles/index.py
@@ -110,7 +110,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
try:
data = orjson.loads(self.request.body)
- except orjson.decoder.JSONDecodeError as e:
+ except orjson.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
diff --git a/app/classes/web/routes/api/servers/server/backups/backup/index.py b/app/classes/web/routes/api/servers/server/backups/backup/index.py
index b92e1e9f..9a4ecc30 100644
--- a/app/classes/web/routes/api/servers/server/backups/backup/index.py
+++ b/app/classes/web/routes/api/servers/server/backups/backup/index.py
@@ -72,9 +72,9 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
FileHelpers.del_file(
os.path.join(backup_conf["backup_path"], data["filename"])
)
- except Exception:
+ except Exception as e:
return self.finish_json(
- 400, {"status": "error", "error": "NO BACKUP FOUND"}
+ 400, {"status": "error", "error": f"DELETE FAILED with error {e}"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
diff --git a/app/classes/web/routes/api/users/user/index.py b/app/classes/web/routes/api/users/user/index.py
index d416e800..1b7f6f91 100644
--- a/app/classes/web/routes/api/users/user/index.py
+++ b/app/classes/web/routes/api/users/user/index.py
@@ -215,7 +215,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
user_obj = HelperUsers.get_user_model(user_id)
if "password" in data and str(user["user_id"]) != str(user_id):
- if str(user["user_id"]) != str(user_obj.manager):
+ if str(user["user_id"]) != str(user_obj.manager) and not user["superuser"]:
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
diff --git a/app/classes/web/tornado_handler.py b/app/classes/web/tornado_handler.py
index 621c930a..f5501d31 100644
--- a/app/classes/web/tornado_handler.py
+++ b/app/classes/web/tornado_handler.py
@@ -22,18 +22,6 @@ from app.classes.web.default_handler import DefaultHandler
from app.classes.web.routes.api.api_handlers import api_handlers
from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers
from app.classes.web.server_handler import ServerHandler
-from app.classes.web.api_handler import (
- ServersStats,
- NodeStats,
- ServerBackup,
- StartServer,
- StopServer,
- RestartServer,
- CreateUser,
- DeleteUser,
- ListServers,
- SendCommand,
-)
from app.classes.web.websocket_handler import WebSocketHandler
from app.classes.web.static_handler import CustomStaticHandler
from app.classes.web.upload_handler import UploadHandler
@@ -162,17 +150,6 @@ class Webserver:
(r"/ws", WebSocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args),
- # API Routes V1
- (r"/api/v1/stats/servers", ServersStats, handler_args),
- (r"/api/v1/stats/node", NodeStats, handler_args),
- (r"/api/v1/server/send_command", SendCommand, handler_args),
- (r"/api/v1/server/backup", ServerBackup, handler_args),
- (r"/api/v1/server/start", StartServer, handler_args),
- (r"/api/v1/server/stop", StopServer, handler_args),
- (r"/api/v1/server/restart", RestartServer, handler_args),
- (r"/api/v1/list_servers", ListServers, handler_args),
- (r"/api/v1/users/create_user", CreateUser, handler_args),
- (r"/api/v1/users/delete_user", DeleteUser, handler_args),
# API Routes V2
*api_handlers(handler_args),
# API Routes OpenMetrics
diff --git a/app/config/logging.json b/app/config/logging.json
index 99de60e4..fd1173eb 100644
--- a/app/config/logging.json
+++ b/app/config/logging.json
@@ -10,16 +10,20 @@
},
"schedule": {
"format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s"
+ },
+ "auth": {
+ "format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s"
+ },
+ "cmd_queue": {
+ "format": "%(asctime)s - [CMD_QUEUE] - %(levelname)s - %(message)s"
}
},
-
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "commander",
"stream": "ext://sys.stdout"
},
-
"main_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "commander",
@@ -50,24 +54,60 @@
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
+ },
+ "auth_file_handler": {
+ "class": "logging.handlers.RotatingFileHandler",
+ "formatter": "auth",
+ "filename": "logs/auth.log",
+ "maxBytes": 10485760,
+ "backupCount": 20,
+ "encoding": "utf8"
+ },
+ "cmd_queue_file_handler": {
+ "class": "logging.handlers.RotatingFileHandler",
+ "formatter": "auth",
+ "filename": "logs/cmd_queue.log",
+ "maxBytes": 10485760,
+ "backupCount": 20,
+ "encoding": "utf8"
}
},
-
"loggers": {
"": {
"level": "INFO",
- "handlers": ["main_file_handler", "session_file_handler"],
+ "handlers": [
+ "main_file_handler",
+ "session_file_handler"
+ ],
"propagate": false
},
"tornado.access": {
"level": "INFO",
- "handlers": ["tornado_access_file_handler"],
+ "handlers": [
+ "tornado_access_file_handler"
+ ],
"propagate": false
},
"apscheduler": {
"level": "INFO",
- "handlers": ["schedule_file_handler"],
+ "handlers": [
+ "schedule_file_handler"
+ ],
+ "propagate": false
+ },
+ "auth": {
+ "level": "INFO",
+ "handlers": [
+ "auth_file_handler"
+ ],
+ "propagate": false
+ },
+ "cmd_queue": {
+ "level": "INFO",
+ "handlers": [
+ "cmd_queue_file_handler"
+ ],
"propagate": false
}
}
-}
+}
\ No newline at end of file
diff --git a/app/config/version.json b/app/config/version.json
index 47d57814..e629f1ba 100644
--- a/app/config/version.json
+++ b/app/config/version.json
@@ -1,5 +1,5 @@
{
"major": 4,
"minor": 2,
- "sub": 1
+ "sub": 2
}
diff --git a/app/frontend/static/assets/images/Crafty_4-0.png b/app/frontend/static/assets/images/Crafty_4-0.png
index d873eedc..0671d421 100644
Binary files a/app/frontend/static/assets/images/Crafty_4-0.png and b/app/frontend/static/assets/images/Crafty_4-0.png differ
diff --git a/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico b/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico
index 68095bf6..7b354581 100644
Binary files a/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico and b/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico 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
index 65dd0671..cec34236 100644
Binary files a/app/frontend/static/assets/images/crafty-logo-square-1024.png 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
index 6928e039..8c3a34b2 100644
Binary files a/app/frontend/static/assets/images/crafty-logo-square-96.png and b/app/frontend/static/assets/images/crafty-logo-square-96.png differ
diff --git a/app/frontend/static/assets/images/crafty-logo-square.svg b/app/frontend/static/assets/images/crafty-logo-square.svg
new file mode 100644
index 00000000..c0a686d3
--- /dev/null
+++ b/app/frontend/static/assets/images/crafty-logo-square.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html
index 40ee757b..48c6ee95 100755
--- a/app/frontend/templates/base.html
+++ b/app/frontend/templates/base.html
@@ -82,6 +82,9 @@
+
+
+
{% include notify.html %}
@@ -381,18 +384,21 @@
if (x) {
x.remove()
}
- bootbox.alert({
+ bootbox.confirm({
title: "{{ translate('notify', 'downloadLogs', data['lang']) }}",
message: "{{ translate('notify', 'finishedPreparing', data['lang']) }}",
buttons: {
- ok: {
+ confirm: {
label: 'Download',
className: 'btn-info'
}
},
- callback: function () {
- console.log("in callback")
- location.href = "/panel/download_support_package";
+ callback: function (result) {
+ if (result){
+ location.href = "/panel/download_support_package";
+ } else {
+ bootbox.close();
+ }
}
});
});
@@ -602,4 +608,4 @@