diff --git a/app/classes/minecraft/serverjars.py b/app/classes/minecraft/serverjars.py index 8c91cd96..dc76bffa 100644 --- a/app/classes/minecraft/serverjars.py +++ b/app/classes/minecraft/serverjars.py @@ -17,8 +17,8 @@ try: import requests except ModuleNotFoundError as e: - logger.critical("Import Error: Unable to load {} module".format(e, e.name)) - console.critical("Import Error: Unable to load {} module".format(e, e.name)) + logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) diff --git a/app/classes/shared/cmd.py b/app/classes/shared/cmd.py index 7d2428b1..5c65c46d 100644 --- a/app/classes/shared/cmd.py +++ b/app/classes/shared/cmd.py @@ -16,7 +16,7 @@ try: except ModuleNotFoundError as e: logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) - console.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) @@ -29,10 +29,6 @@ class MainPrompt(cmd.Cmd, object): # overrides the default Prompt prompt = "Crafty Controller v{} > ".format(helper.get_version_string()) - def __init__(self, tasks_manager): - super().__init__() - self.tasks_manager = tasks_manager - @staticmethod def emptyline(): pass @@ -49,6 +45,9 @@ class MainPrompt(cmd.Cmd, object): console.critical("Unable to write exit file due to error: {}".format(e)) def do_exit(self, line): + self.universal_exit() + + def universal_exit(self): logger.info("Stopping all server daemons / threads") console.info("Stopping all server daemons / threads - This may take a few seconds") websocket_helper.disconnect_all() diff --git a/app/classes/shared/console.py b/app/classes/shared/console.py index 087cdcc4..18733adc 100644 --- a/app/classes/shared/console.py +++ b/app/classes/shared/console.py @@ -9,8 +9,8 @@ try: from termcolor import colored except ModuleNotFoundError as e: - logging.critical("Import Error: Unable to load {} module".format(e, e.name)) - print("Import Error: Unable to load {} module".format(e, e.name)) + logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + print("Import Error: Unable to load {} module".format(e.name)) from app.classes.shared.installer import installer installer.do_install() sys.exit(1) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 1610869f..8ee0de64 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -28,7 +28,7 @@ try: except ModuleNotFoundError as e: logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) - console.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) class Helpers: diff --git a/app/classes/shared/models.py b/app/classes/shared/models.py index 4b470eb8..b53c2903 100644 --- a/app/classes/shared/models.py +++ b/app/classes/shared/models.py @@ -18,8 +18,8 @@ try: import yaml except ModuleNotFoundError as e: - logger.critical("Import Error: Unable to load {} module".format(e, e.name)) - console.critical("Import Error: Unable to load {} module".format(e, e.name)) + logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) schema_version = (0, 1, 0) # major, minor, patch semver @@ -502,6 +502,43 @@ class db_shortcuts: return (Users.get(Users.username == username)).user_id except DoesNotExist: return None + + @staticmethod + def get_user_by_api_token(token: str): + query = Users.select().where(Users.api_token == token) + + if query.exists(): + user = model_to_dict(Users.get(Users.api_token == token)) + # I know it should apply it without setting it but I'm just making sure + user = db_shortcuts.add_user_roles(user) + return user + else: + return {} + + @staticmethod + def add_user_roles(user): + if type(user) == dict: + user_id = user['user_id'] + else: + user_id = user.user_id + + # I just copied this code from get_user, it had those TODOs & comments made by mac - Lukas + + roles_query = User_Roles.select().join(Roles, JOIN.INNER).where(User_Roles.user_id == user_id) + # TODO: this query needs to be narrower + roles = set() + for r in roles_query: + roles.add(r.role_id.role_id) + #servers_query = User_Servers.select().join(Servers, JOIN.INNER).where(User_Servers.user_id == user_id) + ## TODO: this query needs to be narrower + servers = set() + #for s in servers_query: + # servers.add(s.server_id.server_id) + user['roles'] = roles + #user['servers'] = servers + #logger.debug("user: ({}) {}".format(user_id, user)) + return user + @staticmethod def get_user(user_id): @@ -523,19 +560,8 @@ class db_shortcuts: user = model_to_dict(Users.get(Users.user_id == user_id)) if user: - roles_query = User_Roles.select().join(Roles, JOIN.INNER).where(User_Roles.user_id == user_id) - # TODO: this query needs to be narrower - roles = set() - for r in roles_query: - roles.add(r.role_id.role_id) - #servers_query = User_Servers.select().join(Servers, JOIN.INNER).where(User_Servers.user_id == user_id) - ## TODO: this query needs to be narrower - servers = set() - #for s in servers_query: - # servers.add(s.server_id.server_id) - user['roles'] = roles - #user['servers'] = servers - #logger.debug("user: ({}) {}".format(user_id, user)) + # I know it should apply it without setting it but I'm just making sure + user = db_shortcuts.add_user_roles(user) return user else: #logger.debug("user: ({}) {}".format(user_id, {})) diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index c770bf5d..ab6f6372 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -23,8 +23,8 @@ try: import pexpect except ModuleNotFoundError as e: - logger.critical("Import Error: Unable to load {} module".format(e, e.name)) - console.critical("Import Error: Unable to load {} module".format(e, e.name)) + logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) @@ -130,8 +130,7 @@ class Server: self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding=None) self.is_crashed = False - ts = time.time() - self.start_time = str(datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')) + self.start_time = str(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')) if psutil.pid_exists(self.process.pid): self.PID = self.process.pid diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index bc572379..660af321 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -21,7 +21,7 @@ try: except ModuleNotFoundError as e: logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) - console.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) scheduler_intervals = { 'seconds', diff --git a/app/classes/shared/translation.py b/app/classes/shared/translation.py index 76d44aaa..5809e592 100644 --- a/app/classes/shared/translation.py +++ b/app/classes/shared/translation.py @@ -11,14 +11,21 @@ logger = logging.getLogger(__name__) class Translation(): def __init__(self): self.translations_path = os.path.join(helper.root_dir, 'app', 'translations') + self.cached_translation = None + self.cached_translation_lang = None def translate(self, page, word): translated_word = None lang = helper.get_setting('language') fallback_lang = 'en_EN' - translated_word = \ - self.translate_inner(page, word, lang) or \ - self.translate_inner(page, word, fallback_lang) + lang_file_exists = helper.check_file_exists( + os.path.join( + self.translations_path, lang + '.json' + ) + ) + + translated_word = self.translate_inner(page, word, lang) \ + if lang_file_exists else self.translate_inner(page, word, fallback_lang) if translated_word: if isinstance(translated_word, dict): return json.dumps(translated_word) @@ -31,8 +38,17 @@ class Translation(): lang + '.json' ) try: - with open(lang_file, 'r') as f: - data = json.load(f) + if not self.cached_translation: + with open(lang_file, 'r') as f: + data = json.load(f) + self.cached_translation = data + elif self.cached_translation_lang != lang: + with open(lang_file, 'r') as f: + data = json.load(f) + self.cached_translation = data + self.cached_translation_lang = lang + else: + data = self.cached_translation try: translated_page = data[page] diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py index f0e57e07..933b6235 100644 --- a/app/classes/web/api_handler.py +++ b/app/classes/web/api_handler.py @@ -6,53 +6,63 @@ import tornado.escape import logging from app.classes.web.base_handler import BaseHandler -from app.classes.shared.models import Users +from app.classes.shared.models import db_shortcuts log = logging.getLogger(__name__) class ApiHandler(BaseHandler): - def return_response(self, data: dict): + def return_response(self, status: int, data: dict): # Define a standardized response + self.set_status(status) self.write(data) - def access_denied(self, user): - log.info("User %s was denied access to API route", user) - self.set_status(403) - self.finish(self.return_response(403, {'error':'ACCESS_DENIED', 'info':'You were denied access to the requested resource'})) + def access_denied(self, user, reason=''): + if reason: reason = ' because ' + reason + log.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): + def authenticate_user(self) -> bool: try: log.debug("Searching for specified token") # TODO: YEET THIS - user_data = Users.get(api_token=self.get_argument('token')) + user_data = db_shortcuts.get_user_by_api_token(self.get_argument('token')) log.debug("Checking results") if user_data: # Login successful! Check perms - log.info("User {} has authenticated to API".format(user_data.username)) + log.info("User {} has authenticated to API".format(user_data['username'])) # TODO: Role check + + return True # This is to set the "authenticated" else: logging.debug("Auth unsuccessful") - return self.access_denied("unknown") - except: - log.warning("Traceback occurred when authenticating user to API. Most likely wrong token") - return self.access_denied("unknown") - pass + self.access_denied("unknown", "the user provided an invalid token") + return False + except Exception as e: + log.warning("An error occured while authenticating an API user: %s", e) + self.access_denied("unknown"), "an error occured while authenticating the user" + return False class ServersStats(ApiHandler): def get(self): """Get details about all servers""" - self.authenticate_user() + authenticated = self.authenticate_user() + if not authenticated: return # Get server stats + # TODO Check perms self.finish(self.write({"servers": self.controller.stats.get_servers_stats()})) class NodeStats(ApiHandler): def get(self): """Get stats for particular node""" - self.authenticate_user() + authenticated = self.authenticate_user() + if not authenticated: return # Get node stats node_stats = self.controller.stats.get_node_stats() node_stats.pop("servers") diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 65347e33..98a0b4a1 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -61,7 +61,7 @@ class PanelHandler(BaseHandler): } # if no servers defined, let's go to the build server area - if page_data['server_stats']['total'] == 0 and page != "error": + if page_data['server_stats']['total'] == 0 and page != "error" and page != "credits" and page != "contribute": self.set_status(301) self.redirect("/server/step1") return diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py index 7afb8ace..3e9913db 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -15,8 +15,8 @@ try: import bleach except ModuleNotFoundError as e: - logger.critical("Import Error: Unable to load {} module".format(e, e.name)) - console.critical("Import Error: Unable to load {} module".format(e, e.name)) + logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) @@ -81,13 +81,13 @@ class PublicHandler(BaseHandler): # if we don't have a user if not user_data: - next_page = "/public/error?error=Login_Failed" + next_page = "/public/error?error=Login Failed" self.redirect(next_page) return False # if they are disabled if not user_data.enabled: - next_page = "/public/error?error=Login_Failed" + next_page = "/public/error?error=Login Failed" self.redirect(next_page) return False @@ -117,6 +117,10 @@ class PublicHandler(BaseHandler): next_page = "/panel/dashboard" self.redirect(next_page) + else: + # log this failed login attempt + db_helper.add_to_audit_log(user_data.user_id, "Tried to log in", 0, self.get_remote_ip()) + self.redirect('/public/error?error=Login Failed') else: self.redirect("/public/login") diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index 64c2d9ca..788748ad 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -19,8 +19,8 @@ try: import bleach except ModuleNotFoundError as e: - logger.critical("Import Error: Unable to load {} module".format(e, e.name)) - console.critical("Import Error: Unable to load {} module".format(e, e.name)) + logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True) + console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) @@ -163,6 +163,10 @@ class ServerHandler(BaseHandler): import_server_jar = bleach.clean(self.get_argument('server_jar', '')) server_parts = server.split("|") + if not server_name: + self.redirect("/panel/error?error=Server name cannot be empty!") + return False + if import_type == 'import_jar': good_path = self.controller.verify_jar_server(import_server_path, import_server_jar) @@ -171,7 +175,12 @@ class ServerHandler(BaseHandler): return False new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) + db_helper.add_to_audit_log(exec_user_data['user_id'], + "imported a jar server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative" + new_server_id, + self.get_remote_ip()) elif import_type == 'import_zip': + # here import_server_path means the zip path good_path = self.controller.verify_zip_server(import_server_path) if not good_path: self.redirect("/panel/error?error=Zip file not found!") @@ -179,28 +188,24 @@ class ServerHandler(BaseHandler): new_server_id = self.controller.import_zip_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) if new_server_id == "false": - self.redirect("/panel/error?error=ZIP file not accessible! You can fix this permissions issue with sudo chown -R crafty:crafty {} And sudo chmod 2775 -R {}".format(import_server_path, import_server_path)) + self.redirect("/panel/error?error=Zip file not accessible! You can fix this permissions issue with sudo chown -R crafty:crafty {} And sudo chmod 2775 -R {}".format(import_server_path, import_server_path)) return False + db_helper.add_to_audit_log(exec_user_data['user_id'], + "imported a zip server named \"{}\"".format(server_name), # Example: Admin imported a server named "old creative" + new_server_id, + self.get_remote_ip()) else: + if len(server_parts) != 2: + self.redirect("/panel/error?error=Invalid server data") + return False + server_type, server_version = server_parts # todo: add server type check here and call the correct server add functions if not a jar - new_server_id = self.controller.create_jar_server(server_parts[0], server_parts[1], server_name, min_mem, max_mem, port) - - if new_server_id is not None and exec_user_data is not None and len(server_parts) > 1: + new_server_id = self.controller.create_jar_server(server_type, server_version, server_name, min_mem, max_mem, port) db_helper.add_to_audit_log(exec_user_data['user_id'], - "created a {} {} server named \"{}\"".format(server_parts[1], str(server_parts[0]).capitalize(), server_name), # Example: Admin created a 1.16.5 Bukkit server named "survival" + "created a {} {} server named \"{}\"".format(server_version, str(server_type).capitalize(), server_name), # Example: Admin created a 1.16.5 Bukkit server named "survival" new_server_id, self.get_remote_ip()) - elif new_server_id is not None and exec_user_data is not None: - db_helper.add_to_audit_log(exec_user_data['user_id'], - "created a {} {} server named \"{}\"".format("Minecraft", str(server_parts[0]).capitalize(), server_name), # Example: Admin created a 1.16.5 Bukkit server named "survival" - new_server_id, - self.get_remote_ip()) - - else: - logger.error("Unable to create server") - console.error("Unable to create server") - self.controller.stats.record_stats() self.redirect("/panel/dashboard") diff --git a/app/classes/web/static_handler.py b/app/classes/web/static_handler.py new file mode 100644 index 00000000..27855434 --- /dev/null +++ b/app/classes/web/static_handler.py @@ -0,0 +1,15 @@ +import tornado.web +from typing import ( + Optional +) + +from app.classes.shared.console import console + +class CustomStaticHandler(tornado.web.StaticFileHandler): + def validate_absolute_path(self, root: str, absolute_path: str) -> Optional[str]: + try: + return super().validate_absolute_path(root, absolute_path) + except tornado.web.HTTPError as error: + if 'HTTP 404: Not Found' in str(error): + self.set_status(404) + self.finish({'error':'NOT_FOUND', 'info':'The requested resource was not found on the server'}) \ No newline at end of file diff --git a/app/classes/web/tornado.py b/app/classes/web/tornado.py index ceb2a12f..ced565ea 100644 --- a/app/classes/web/tornado.py +++ b/app/classes/web/tornado.py @@ -25,6 +25,7 @@ try: from app.classes.web.ajax_handler import AjaxHandler from app.classes.web.api_handler import ServersStats, NodeStats from app.classes.web.websocket_handler import SocketHandler + from app.classes.web.static_handler import CustomStaticHandler from app.classes.shared.translation import translation except ModuleNotFoundError as e: @@ -112,9 +113,6 @@ class Webserver: logger.info("Starting Web Server on ports http:{} https:{}".format(http_port, https_port)) - console.info("http://{}:{} is up and ready for connection:".format(helper.get_local_ip(), http_port)) - console.info("https://{}:{} is up and ready for connection:".format(helper.get_local_ip(), https_port)) - asyncio.set_event_loop(asyncio.new_event_loop()) tornado.template.Loader('.') @@ -143,7 +141,9 @@ class Webserver: autoreload=False, log_function=self.log_function, login_url="/login", - default_handler_class=PublicHandler + default_handler_class=PublicHandler, + static_handler_class=CustomStaticHandler, + serve_traceback=debug_errors, ) self.HTTP_Server = tornado.httpserver.HTTPServer(app) @@ -152,6 +152,11 @@ class Webserver: self.HTTPS_Server = tornado.httpserver.HTTPServer(app, ssl_options=cert_objects) self.HTTPS_Server.listen(https_port) + logger.info("http://{}:{} is up and ready for connections.".format(helper.get_local_ip(), http_port)) + logger.info("https://{}:{} is up and ready for connections.".format(helper.get_local_ip(), https_port)) + console.info("http://{}:{} is up and ready for connections.".format(helper.get_local_ip(), http_port)) + console.info("https://{}:{} is up and ready for connections.".format(helper.get_local_ip(), https_port)) + console.info("Server Init Complete: Listening For Connections:") self.ioloop = tornado.ioloop.IOLoop.instance() diff --git a/app/frontend/templates/panel/panel_edit_user.html b/app/frontend/templates/panel/panel_edit_user.html index 17730ccf..36ff4120 100644 --- a/app/frontend/templates/panel/panel_edit_user.html +++ b/app/frontend/templates/panel/panel_edit_user.html @@ -43,10 +43,12 @@ Config + {% if data['new_user'] %} + {% end %}
diff --git a/app/frontend/templates/panel/parts/details_stats.html b/app/frontend/templates/panel/parts/details_stats.html index 046cf3c2..c3288385 100644 --- a/app/frontend/templates/panel/parts/details_stats.html +++ b/app/frontend/templates/panel/parts/details_stats.html @@ -86,11 +86,8 @@ let startedLocal; if (started != null) { - console.log('88', '{{ data['server_stats']['started'] }}'); - {% if data['server_stats']['started'] != 'False' %} - startedUTC = '{{ (datetime.datetime.strptime(data['server_stats']['started'], '%Y-%m-%d %H:%M:%S') - datetime.timedelta(seconds=-time.timezone)).strftime('%Y-%m-%d %H:%M:%S') }}'; - {% end %} - console.log('utc', startedUTC); + startedUTC = '{{ data['server_stats']['started'] }}'; + console.log('started utc:', startedUTC); startedUTC = moment.utc(startedUTC, 'YYYY-MM-DD HH:mm:ss'); let browserUTCOffset = moment().utcOffset(); // This is in minutes @@ -98,32 +95,25 @@ startedLocal = startedUTC.utcOffset(browserUTCOffset); startedLocalFormatted = startedLocal.format('YYYY-MM-DD HH:mm:ss'); - console.log('startedLocal', startedLocal); - console.log('startedLocalFormatted', startedLocalFormatted); + console.log('started local time:', startedLocalFormatted); started.textContent = startedLocalFormatted } - let nowServerTime = '{{ data['time'] }}'; - let startedServerTime = '{{ data['server_stats']['started'] }}'; - - if (uptime != null && started != null) { - - var msdiff = moment(nowServerTime,"YYYY-MM-DD hh:mm:ss") - .diff(moment(startedServerTime,"YYYY-MM-DD hh:mm:ss")); + var calculateUptime = () => { + var msdiff = moment() + .diff(startedLocal); var diff = moment.duration(msdiff); uptime.textContent = durationToHumanizedString(diff); + } + + if (uptime != null && started != null) { console.log('startedLocal', startedLocal) if (startedLocal) { - var uptimeLoop = setInterval(() => { - var msdiff = moment() - .diff(startedLocal); - var diff = moment.duration(msdiff); - - uptime.textContent = durationToHumanizedString(diff); - }, 1000) + calculateUptime() + var uptimeLoop = setInterval(calculateUptime, 1000) } } diff --git a/app/frontend/templates/panel/server_files.html b/app/frontend/templates/panel/server_files.html index 60037056..de1cc745 100644 --- a/app/frontend/templates/panel/server_files.html +++ b/app/frontend/templates/panel/server_files.html @@ -194,6 +194,7 @@ }
+

{{ translate('serverFiles', 'editingFile') }}
file_contents
@@ -344,10 +345,13 @@ console.log('Got File Contents From Server'); json = JSON.parse(data) if (json.error) { - $('#editorParent').toggle(false) + $('#editorParent').toggle(false) // hide + $('#fileError').toggle(true) // show + $('#fileError').text("{{ translate('serverFiles', 'fileReadError') }}: " + json.error) // show error editor.blur() } else { - $('#editorParent').toggle(true) + $('#editorParent').toggle(true) // show + $('#fileError').toggle(false) // hide setFileName(event.target.innerText); editor.session.setValue(json.content); } @@ -376,7 +380,8 @@ } setFileName(); - $('#editorParent').toggle(false) + $('#editorParent').toggle(false) // show + $('#fileError').toggle(false) // hide editor.blur() function setMode (extension) { diff --git a/app/frontend/templates/server/wizard.html b/app/frontend/templates/server/wizard.html index 4177aa92..e8098d95 100644 --- a/app/frontend/templates/server/wizard.html +++ b/app/frontend/templates/server/wizard.html @@ -14,7 +14,7 @@

-

+ {% raw xsrf_form_html() %}
@@ -47,22 +47,22 @@
- - + +
- - + +
- - + +
@@ -84,7 +84,7 @@

- + {% raw xsrf_form_html() %}

@@ -120,22 +120,22 @@
- - + +
- - + +
- - + +
@@ -157,7 +157,7 @@

- + {% raw xsrf_form_html() %} @@ -194,22 +194,22 @@

- - + +
- - + +
- - + +
@@ -231,12 +231,19 @@