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'] %}
-