diff --git a/.gitlab/scripts/linux_perms_fix.sh b/.gitlab/scripts/linux_perms_fix.sh new file mode 100644 index 00000000..24b92176 --- /dev/null +++ b/.gitlab/scripts/linux_perms_fix.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Prompt the user for the directory path +read -p "Enter the directory path to set permissions (/var/opt/minecraft/crafty): " directory_path + +# Check if the script is running within a Docker container +if [ -f "/.dockerenv" ]; then + echo "Script is running within a Docker container. Exiting with error." + exit 1 # Exit with an error code if running in Docker +else + echo "Script is not running within a Docker container. Executing permissions changes..." + # Run the commands to set permissions + sudo chmod 700 $(find "$directory_path" -type d) + sudo chmod 644 $(find "$directory_path" -type f) +fi \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9c9fc6..aee7e09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## --- [4.2.4] - 2023/TBD +## --- [4.3.0] - 2023/TBD ### New features TBD ### Refactor @@ -9,10 +9,15 @@ TBD - Make sure default.json is read from correct location ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/714)) - Do not allow users at server limit to clone servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/718)) - Fix bug where you cannot get to config with unloaded server ([Commit](https://gitlab.com/crafty-controller/crafty-4/-/commit/9de08973b6bb2ddf91283c5c6b0e189ff34f7e24)) +- Fix forge install v1.20, 1.20.1 and 1.20.2 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/710)) +- Fix Sanitisation on Passwords ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/715)) +- Fix `Upload Imports` on unix systems, that have a space in the root dir name ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/722)) +- Fix Bedrock downloads, add `www` to download URL ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/723)) ### Tweaks - Bump pyOpenSSL & cryptography for CVE-2024-0727, CVE-2023-50782 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/716)) +- Bump cryptography for CVE-2024-26130 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/724)) ### Lang -TBD +- Update `de_DE, en_EN, es_ES, fr_FR, he_IL, lol_EN, lv_LV, nl_BE pl_PL, th_TH, tr_TR, uk_UA, zh_CN` translations for `4.3.0` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/715))

## --- [4.2.3] - 2023/02/02 diff --git a/README.md b/README.md index b1b401d7..75da23a7 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.4 +# Crafty Controller 4.3.0 > Python based Control Panel for your Minecraft Server ## What is Crafty Controller? diff --git a/app/classes/controllers/users_controller.py b/app/classes/controllers/users_controller.py index 5c6dd3d2..5425fbf8 100644 --- a/app/classes/controllers/users_controller.py +++ b/app/classes/controllers/users_controller.py @@ -52,7 +52,7 @@ class UsersController: }, "password": { "type": "string", - "minLength": 8, + "minLength": self.helper.minimum_password_length, "examples": ["crafty"], "title": "Password", }, diff --git a/app/classes/shared/command.py b/app/classes/shared/command.py index ab7a494a..4b7abbc3 100644 --- a/app/classes/shared/command.py +++ b/app/classes/shared/command.py @@ -82,11 +82,11 @@ class MainPrompt(cmd.Cmd): # get new password from user new_pass = getpass.getpass(prompt=f"NEW password for: {username} > ") # check to make sure it fits our requirements. - if len(new_pass) > 512: - Console.warning("Passwords must be greater than 6char long and under 512") - return False - if len(new_pass) < 6: - Console.warning("Passwords must be greater than 6char long and under 512") + if len(new_pass) < self.helper.minimum_password_length: + Console.warning( + "Passwords must be greater than" + f" {self.helper.minimum_password_length} char long" + ) return False # grab repeated password input new_pass_conf = getpass.getpass(prompt="Re-enter your password: > ") diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 1ed3d71f..6b821f9d 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -81,6 +81,7 @@ class Helpers: self.update_available = False self.ignored_names = ["crafty_managed.txt", "db_stats"] self.crafty_starting = False + self.minimum_password_length = 8 @staticmethod def auto_installer_fix(ex): @@ -117,7 +118,7 @@ class Helpers: Get latest bedrock executable url \n\n returns url if successful, False if not """ - url = "https://minecraft.net/en-us/download/server/bedrock/" + url = "https://www.minecraft.net/en-us/download/server/bedrock/" headers = { "Accept-Encoding": "identity", "Accept-Language": "en", diff --git a/app/classes/shared/main_models.py b/app/classes/shared/main_models.py index c166b7fb..0cced56f 100644 --- a/app/classes/shared/main_models.py +++ b/app/classes/shared/main_models.py @@ -18,13 +18,20 @@ class DatabaseBuilder: 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: + 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", password) + if self.helper.minimum_password_length > default_data.get("password", password): + Console.critical( + "Default password too short" + " using Crafty's created default." + " Find it in app/config/default-creds.txt" + ) + else: + password = default_data.get("password", password) self.users_helper.add_user( username=username, diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index 812cedce..8d07af23 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -696,6 +696,10 @@ class ServerInstance: version_param = version[0][0].split(".") version_major = int(version_param[0]) version_minor = int(version_param[1]) + if len(version_param) > 2: + version_sub = int(version_param[2]) + else: + version_sub = 0 # Checking which version we are with if version_major <= 1 and version_minor < 17: @@ -729,8 +733,8 @@ class ServerInstance: server_obj.execution_command = execution_command Console.debug(SUCCESSMSG) - elif version_major <= 1 and version_minor < 20: - # NEW VERSION >= 1.17 and <= 1.20 + elif version_major <= 1 and version_minor <= 20 and version_sub < 3: + # NEW VERSION >= 1.17 and <= 1.20.2 # (no jar file in server dir, only run.bat and run.sh) run_file_path = "" @@ -777,7 +781,7 @@ class ServerInstance: server_obj.execution_command = execution_command Console.debug(SUCCESSMSG) else: - # NEW VERSION >= 1.20 + # NEW VERSION >= 1.20.3 # (executable jar is back in server dir) # Retrieving the executable jar filename diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py index 762d3fb1..467765ea 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -1,5 +1,8 @@ import logging +import json import nh3 +from jsonschema import validate +from jsonschema.exceptions import ValidationError from app.classes.shared.helpers import Helpers from app.classes.models.users import HelperUsers @@ -45,7 +48,7 @@ class PublicHandler(BaseHandler): } if self.request.query: - page_data["query"] = self.request.query + page_data["query"] = self.request.query_arguments.get("next")[0].decode() # sensible defaults template = "public/404.html" @@ -75,11 +78,7 @@ class PublicHandler(BaseHandler): # if we have no page, let's go to login else: - if self.request.query: - self.redirect("/login?" + self.request.query) - else: - self.redirect("/login") - return + return self.redirect("/login") self.render( template, @@ -89,33 +88,61 @@ class PublicHandler(BaseHandler): ) def post(self, page=None): - # pylint: disable=no-member - error = nh3.clean(self.get_argument("error", "Invalid Login!")) - error_msg = nh3.clean(self.get_argument("error_msg", "")) - # pylint: enable=no-member + login_schema = { + "type": "object", + "properties": { + "username": { + "type": "string", + }, + "password": {"type": "string"}, + }, + "required": ["username", "password"], + "additionalProperties": False, + } + 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)} + ) + + 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, + { + "status": "error", + "error": "VWggb2ghIFN0aW5reS 🪠", + "error_data": str(e), + }, + ) page_data = { "version": self.helper.get_version_string(), - "error": error, "lang": self.helper.get_setting("language"), "lang_page": self.helper.get_lang_page(self.helper.get_setting("language")), "query": "", } if self.request.query: - page_data["query"] = self.request.query + page_data["query"] = self.request.query_arguments.get("next")[0].decode() if page == "login": + data = json.loads(self.request.body) + 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 - - # pylint: disable=no-member - entered_username = nh3.clean(self.get_argument("username")) - entered_password = self.get_argument("password") - # pylint: enable=no-member + entered_username = nh3.clean(data["username"]) # pylint: disable=no-member + entered_password = data["password"] try: user_id = HelperUsers.get_user_id_by_name(entered_username.lower()) @@ -127,16 +154,18 @@ class PublicHandler(BaseHandler): f" Authentication failed from remote IP {self.get_remote_ip()}" " Users does not exist." ) - error_msg = "Incorrect username or password. Please try again." + self.finish_json( + 403, + { + "status": "error", + "error": self.helper.translation.translate( + "login", "incorrect", self.helper.get_setting("language") + ), + }, + ) # self.clear_cookie("user") # self.clear_cookie("user_data") - self.clear_cookie("token") - if self.request.query: - self.redirect(f"/login?error_msg={error_msg}&{self.request.query}") - else: - self.redirect(f"/login?error_msg={error_msg}") - return - + return self.clear_cookie("token") # if we don't have a user if not user_data: auth_log.error( @@ -145,15 +174,18 @@ class PublicHandler(BaseHandler): " User does not exist." ) self.controller.log_attempt(self.get_remote_ip(), entered_username) - error_msg = "Incorrect username or password. Please try again." + self.finish_json( + 403, + { + "status": "error", + "error": self.helper.translation.translate( + "login", "incorrect", self.helper.get_setting("language") + ), + }, + ) # self.clear_cookie("user") # self.clear_cookie("user_data") - self.clear_cookie("token") - if self.request.query: - self.redirect(f"/login?error_msg={error_msg}&{self.request.query}") - else: - self.redirect(f"/login?error_msg={error_msg}") - return + return self.clear_cookie("token") # if they are disabled if not user_data.enabled: @@ -163,19 +195,18 @@ class PublicHandler(BaseHandler): " 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." + self.finish_json( + 403, + { + "status": "error", + "error": self.helper.translation.translate( + "login", "disabled", self.helper.get_setting("language") + ), + }, ) # self.clear_cookie("user") # self.clear_cookie("user_data") - self.clear_cookie("token") - if self.request.query: - self.redirect(f"/login?error_msg={error_msg}&{self.request.query}") - else: - self.redirect(f"/login?error_msg={error_msg}") - return - + return self.clear_cookie("token") login_result = self.helper.verify_pass(entered_password, user_data.password) # Valid Login @@ -200,32 +231,34 @@ class PublicHandler(BaseHandler): user_data.user_id, "Logged in", 0, self.get_remote_ip() ) - if self.request.query_arguments.get("next"): - next_page = self.request.query_arguments.get("next")[0].decode() - else: - next_page = "/panel/dashboard" + return self.finish_json( + 200, {"status": "ok", "data": {"message": "login successful!"}} + ) - 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()}" + # We'll continue on and handle unsuccessful logins + 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") + error_msg = self.helper.translation.translate( + "login", "incorrect", self.helper.get_setting("language") + ) + if entered_password == "app/config/default-creds.txt": + error_msg += ". " + error_msg += self.helper.translation.translate( + "login", "defaultPath", self.helper.get_setting("language") ) - self.controller.log_attempt(self.get_remote_ip(), entered_username) - # self.clear_cookie("user") - # self.clear_cookie("user_data") - self.clear_cookie("token") - error_msg = "Incorrect username or password. Please try again." - # log this failed login attempt - self.controller.management.add_to_audit_log( - user_data.user_id, "Tried to log in", 0, self.get_remote_ip() - ) - if self.request.query: - self.redirect(f"/login?error_msg={error_msg}&{self.request.query}") - else: - self.redirect(f"/login?error_msg={error_msg}") + # log this failed login attempt + self.controller.management.add_to_audit_log( + user_data.user_id, "Tried to log in", 0, self.get_remote_ip() + ) + return self.finish_json( + 403, + {"status": "error", "error": error_msg}, + ) else: - if self.request.query: - self.redirect("/login?" + self.request.query) - else: - self.redirect("/login") + self.redirect("/login?") diff --git a/app/classes/web/routes/api/auth/login.py b/app/classes/web/routes/api/auth/login.py index b91b295d..7a27c6f8 100644 --- a/app/classes/web/routes/api/auth/login.py +++ b/app/classes/web/routes/api/auth/login.py @@ -17,7 +17,7 @@ login_schema = { "minLength": 4, "pattern": "^[a-z0-9_]+$", }, - "password": {"type": "string", "maxLength": 20, "minLength": 4}, + "password": {"type": "string", "minLength": 4}, }, "required": ["username", "password"], "additionalProperties": False, diff --git a/app/config/version.json b/app/config/version.json index 3c001e77..db68adb0 100644 --- a/app/config/version.json +++ b/app/config/version.json @@ -1,5 +1,5 @@ { "major": 4, - "minor": 2, - "sub": 4 + "minor": 3, + "sub": 0 } diff --git a/app/frontend/templates/public/login.html b/app/frontend/templates/public/login.html index 1b39d8c4..5a54ecca 100644 --- a/app/frontend/templates/public/login.html +++ b/app/frontend/templates/public/login.html @@ -77,55 +77,49 @@ box-shadow: 0 12px 16px 0 hsla(0, 0%, 0%, 0.4); } - {% if data['query'] %} -
- {% else %} - - {% end %} - {% raw xsrf_form_html() %} -
- -
- -
+ + {% raw xsrf_form_html() %} +
+ +
+
-
- -
- -
+
+
+ +
+
-
- -
- {% if error_msg is not None %} -
- {{error_msg}} -
- {% end %} -
-
-   -
- +
+
+ +
+
+
+
+
+  
+ +
- - - - + + + +
@@ -155,13 +149,13 @@ document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')'; //Register Service worker for mobile app if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', {scope: '/'}) + navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', { scope: '/' }) .then(function (registration) { console.log('Service Worker Registered'); }); } }); - async function resetPass(){ + async function resetPass() { let res = await fetch(`/api/v2/crafty/resetPass/`, { method: 'GET', }); @@ -170,7 +164,38 @@ bootbox.alert(responseData.data) } + $("#login-form").on("submit", async function (e) { + e.preventDefault(); + let loginForm = document.getElementById("login-form"); + let formData = new FormData(loginForm); + + //Create an object from the form data entries + let formDataObject = Object.fromEntries(formData.entries()); + console.log(formDataObject) + let res = await fetch(`/login`, { + method: 'POST', + headers: { + 'X-XSRFToken': formDataObject._xsrf, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "username": formDataObject.username, + "password": formDataObject.password + }), + }); + let responseData = await res.json(); + if (responseData.status === "ok") { + console.log("OK") + if ($("#login-form").data("query")) { + location.href = `${$("#login-form").data("query")}`; + } else { + location.href = `/panel/dashboard` + } + } else { + $("#error-field").html(responseData.error); + } + });