From 11a004ea748642b14d72ddb0f177706a1268a0aa Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 2 Jun 2021 19:53:01 +0300 Subject: [PATCH 01/13] Handle KeyboardInterrupt in prompt (non-daemon) mode --- main.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 71fd2489..7a58d968 100644 --- a/main.py +++ b/main.py @@ -129,7 +129,13 @@ if __name__ == '__main__': Crafty = MainPrompt(tasks_manager) if not args.daemon: - Crafty.cmdloop() + try: + Crafty.cmdloop() + except KeyboardInterrupt: + print() # for newline + logger.info("Recieved SIGINT, stopping Crafty") + console.info("Recieved SIGINT, stopping Crafty") + Crafty.universal_exit() else: print("Crafty started in daemon mode, no shell will be printed") while True: @@ -139,6 +145,7 @@ if __name__ == '__main__': time.sleep(1) except KeyboardInterrupt: logger.info("Recieved SIGINT, stopping Crafty") + console.info("Recieved SIGINT, stopping Crafty") break - + Crafty.universal_exit() From 5a5cd65d4f61e00d035221350dc8ea4fe5d9974d Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 2 Jun 2021 21:17:50 +0300 Subject: [PATCH 02/13] Sort server types, I suffered a lot while doing this --- app/classes/minecraft/serverjars.py | 23 +++++++++++++++++++++++ app/classes/web/server_handler.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/classes/minecraft/serverjars.py b/app/classes/minecraft/serverjars.py index dc76bffa..1006ab2c 100644 --- a/app/classes/minecraft/serverjars.py +++ b/app/classes/minecraft/serverjars.py @@ -71,6 +71,29 @@ class ServerJars: data = self._read_cache() return data.get('servers') + def get_serverjar_data_sorted(self): + data = self.get_serverjar_data() + + def str_to_int(x, counter=0): + try: + return ord(x[0]) + str_to_int(x[1:], counter + 1) + len(x) + except IndexError: + return 0 + + def to_int(x): + try: + return int(x) + except ValueError: + temp = x.split('-') + return to_int(temp[0]) + str_to_int(temp[1]) / 100000 + + sort_key_fn = lambda x: [to_int(y) for y in x.split('.')] + + for key in data.keys(): + data[key] = sorted(data[key], key=sort_key_fn) + + return data + def _check_api_alive(self): logger.info("Checking serverjars.com API status") diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index 788748ad..7768280d 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -63,7 +63,7 @@ class ServerHandler(BaseHandler): if page == "step1": - page_data['server_types'] = server_jar_obj.get_serverjar_data() + page_data['server_types'] = server_jar_obj.get_serverjar_data_sorted() template = "server/wizard.html" self.render( From 344c2219ec4fff768a691cb02fcee2979fe49c53 Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 2 Jun 2021 21:30:12 +0300 Subject: [PATCH 03/13] Handle SIGTERM --- main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main.py b/main.py index 7a58d968..4489bc9e 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ import json import time import argparse import logging.config +import signal """ Our custom classes / pip packages """ from app.classes.shared.console import console @@ -128,6 +129,15 @@ if __name__ == '__main__': tasks_manager.start_main_kill_switch_watcher() Crafty = MainPrompt(tasks_manager) + + def sigterm_handler(signum, current_stack_frame): + print() # for newline + logger.info("Recieved SIGTERM, stopping Crafty") + console.info("Recieved SIGTERM, stopping Crafty") + Crafty.universal_exit() + + signal.signal(signal.SIGTERM, sigterm_handler) + if not args.daemon: try: Crafty.cmdloop() From a79f42f4da049db99251db76ce4897446fc1542e Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 2 Jun 2021 21:47:08 +0300 Subject: [PATCH 04/13] Escape logfile output, fixes weird formatting and remote code execution vulnerability --- app/classes/web/ajax_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index 0796527e..0f0cdb4c 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -5,6 +5,7 @@ import tornado.escape import bleach import os import shutil +import html from app.classes.shared.console import console from app.classes.shared.models import Users, installer @@ -68,7 +69,7 @@ class AjaxHandler(BaseHandler): for d in data: try: - line = helper.log_colors(d) + line = helper.log_colors(html.escape(d)) self.write('{}
'.format(line)) # self.write(d.encode("utf-8")) From 3559c0a942788f99a1fd745b9adf753e8c991453 Mon Sep 17 00:00:00 2001 From: luukas Date: Fri, 30 Jul 2021 18:37:36 +0300 Subject: [PATCH 05/13] Basic README --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..666efb42 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Crafty Controller 4.0.0-alpha.2 +> Python based Control Panel for your Minecraft Server + +## What is Crafty Controller? +Crafty Controller is a Minecraft Server Control Panel / Launcher. The purpose +of Crafty Controller is to launch a Minecraft Server in the background and present +a web interface for the server administrators to interact with their servers. Crafty +is compatible with Docker, Linux, Windows 7, Windows 8 and Windows 10. + +## Documentation +Temporary documentation available on [GitLab](https://gitlab.com/crafty-controller/crafty-commander/wikis/home) + +## Meta +Project Homepage - https://craftycontrol.com + +Discord Server - https://discord.gg/9VJPhCE + +Git Repository - https://gitlab.com/crafty-controller/crafty-web \ No newline at end of file From 7acf095463d6f7238d1ad5b1c472bbb8224cf6a8 Mon Sep 17 00:00:00 2001 From: luukas Date: Fri, 30 Jul 2021 19:20:01 +0300 Subject: [PATCH 06/13] Make HTTP handler functions not return anything else than None to remove an error in the log. Also make users not log out when visiting a page on the public handler --- app/classes/web/ajax_handler.py | 40 +++++++++++++++--------------- app/classes/web/api_handler.py | 4 +-- app/classes/web/panel_handler.py | 6 ++--- app/classes/web/public_handler.py | 20 +++++++++++---- app/classes/web/server_handler.py | 12 ++++----- app/frontend/templates/notify.html | 2 +- 6 files changed, 47 insertions(+), 37 deletions(-) diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index 0f0cdb4c..4f98d98f 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -48,7 +48,7 @@ class AjaxHandler(BaseHandler): if server_id is None: logger.warning("Server ID not found in server_log ajax call") self.redirect("/panel/error?error=Server ID Not Found") - return False + return server_id = bleach.clean(server_id) @@ -86,14 +86,14 @@ class AjaxHandler(BaseHandler): file_path = self.get_argument('file_path', None) server_id = self.get_argument('id', None) - if not self.check_server_id(server_id, 'get_file'): return False + if not self.check_server_id(server_id, 'get_file'): return else: server_id = bleach.clean(server_id) if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path)\ or not helper.check_file_exists(os.path.abspath(file_path)): logger.warning("Invalid path in get_file ajax call ({})".format(file_path)) console.warning("Invalid path in get_file ajax call ({})".format(file_path)) - return False + return error = None @@ -114,7 +114,7 @@ class AjaxHandler(BaseHandler): elif page == "get_tree": server_id = self.get_argument('id', None) - if not self.check_server_id(server_id, 'get_tree'): return False + if not self.check_server_id(server_id, 'get_tree'): return else: server_id = bleach.clean(server_id) self.write(db_helper.get_server_data_by_id(server_id)['path'] + '\n' + @@ -152,14 +152,14 @@ class AjaxHandler(BaseHandler): server_id = self.get_argument('id', None) print(server_id) - if not self.check_server_id(server_id, 'create_file'): return False + if not self.check_server_id(server_id, 'create_file'): return else: server_id = bleach.clean(server_id) if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path) \ or helper.check_file_exists(os.path.abspath(file_path)): logger.warning("Invalid path in create_file ajax call ({})".format(file_path)) console.warning("Invalid path in create_file ajax call ({})".format(file_path)) - return False + return # Create the file by opening it with open(file_path, 'w') as file_object: @@ -172,14 +172,14 @@ class AjaxHandler(BaseHandler): server_id = self.get_argument('id', None) print(server_id) - if not self.check_server_id(server_id, 'create_dir'): return False + if not self.check_server_id(server_id, 'create_dir'): return else: server_id = bleach.clean(server_id) if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], dir_path) \ or helper.check_path_exists(os.path.abspath(dir_path)): logger.warning("Invalid path in create_dir ajax call ({})".format(dir_path)) console.warning("Invalid path in create_dir ajax call ({})".format(dir_path)) - return False + return # Create the directory os.mkdir(dir_path) @@ -192,7 +192,7 @@ class AjaxHandler(BaseHandler): console.warning("delete {} for server {}".format(file_path, server_id)) - if not self.check_server_id(server_id, 'del_file'): return False + if not self.check_server_id(server_id, 'del_file'): return else: server_id = bleach.clean(server_id) server_info = db_helper.get_server_data_by_id(server_id) @@ -201,7 +201,7 @@ class AjaxHandler(BaseHandler): or not helper.check_file_exists(os.path.abspath(file_path)): logger.warning("Invalid path in del_file ajax call ({})".format(file_path)) console.warning("Invalid path in del_file ajax call ({})".format(file_path)) - return False + return # Delete the file os.remove(file_path) @@ -213,7 +213,7 @@ class AjaxHandler(BaseHandler): console.warning("delete {} for server {}".format(file_path, server_id)) - if not self.check_server_id(server_id, 'del_dir'): return False + if not self.check_server_id(server_id, 'del_dir'): return else: server_id = bleach.clean(server_id) server_info = db_helper.get_server_data_by_id(server_id) @@ -221,7 +221,7 @@ class AjaxHandler(BaseHandler): or not helper.check_path_exists(os.path.abspath(dir_path)): logger.warning("Invalid path in del_file ajax call ({})".format(dir_path)) console.warning("Invalid path in del_file ajax call ({})".format(dir_path)) - return False + return # Delete the directory # os.rmdir(dir_path) # Would only remove empty directories @@ -237,14 +237,14 @@ class AjaxHandler(BaseHandler): print(file_path) print(server_id) - if not self.check_server_id(server_id, 'save_file'): return False + if not self.check_server_id(server_id, 'save_file'): return else: server_id = bleach.clean(server_id) if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path)\ or not helper.check_file_exists(os.path.abspath(file_path)): logger.warning("Invalid path in save_file ajax call ({})".format(file_path)) console.warning("Invalid path in save_file ajax call ({})".format(file_path)) - return False + return # Open the file in write mode and store the content in file_object with open(file_path, 'w') as file_object: @@ -256,19 +256,19 @@ class AjaxHandler(BaseHandler): server_id = self.get_argument('id', None) print(server_id) - if not self.check_server_id(server_id, 'rename_item'): return False + if not self.check_server_id(server_id, 'rename_item'): return else: server_id = bleach.clean(server_id) if item_path is None or new_item_name is None: logger.warning("Invalid path(s) in rename_item ajax call") console.warning("Invalid path(s) in rename_item ajax call") - return False + return if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], item_path) \ or not helper.check_path_exists(os.path.abspath(item_path)): logger.warning("Invalid old name path in rename_item ajax call ({})".format(server_id)) console.warning("Invalid old name path in rename_item ajax call ({})".format(server_id)) - return False + return new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name) @@ -276,7 +276,7 @@ class AjaxHandler(BaseHandler): or helper.check_path_exists(os.path.abspath(new_item_path)): logger.warning("Invalid new name path in rename_item ajax call ({})".format(server_id)) console.warning("Invalid new name path in rename_item ajax call ({})".format(server_id)) - return False + return # RENAME os.rename(item_path, new_item_path) @@ -284,7 +284,7 @@ class AjaxHandler(BaseHandler): if server_id is None: logger.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id)) console.warning("Server ID not defined in {} ajax call ({})".format(page_name, server_id)) - return False + return else: server_id = bleach.clean(server_id) @@ -292,5 +292,5 @@ class AjaxHandler(BaseHandler): if not db_helper.server_id_exists(server_id): logger.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id)) console.warning("Server ID not found in {} ajax call ({})".format(page_name, server_id)) - return False + return return True diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py index 933b6235..50035c9f 100644 --- a/app/classes/web/api_handler.py +++ b/app/classes/web/api_handler.py @@ -41,11 +41,11 @@ class ApiHandler(BaseHandler): else: logging.debug("Auth unsuccessful") self.access_denied("unknown", "the user provided an invalid token") - return False + return 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 + return class ServersStats(ApiHandler): diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 98a0b4a1..aade9b2e 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -140,7 +140,7 @@ class PanelHandler(BaseHandler): #if not db_helper.server_id_authorized(server_id, exec_user_id): if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id): self.redirect("/panel/error?error=Invalid Server ID") - return False + return valid_subpages = ['term', 'logs', 'backup', 'config', 'files', 'admin_controls'] @@ -200,7 +200,7 @@ class PanelHandler(BaseHandler): #if not db_helper.server_id_authorized(server_id, exec_user_id): if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id): self.redirect("/panel/error?error=Invalid Server ID") - return False + return server_info = db_helper.get_server_data_by_id(server_id) backup_file = os.path.abspath(os.path.join(server_info["backup_path"], file)) @@ -249,7 +249,7 @@ class PanelHandler(BaseHandler): #if not db_helper.server_id_authorized(server_id, exec_user_id): if not db_helper.server_id_authorized_from_roles(int(server_id), exec_user_id): self.redirect("/panel/error?error=Invalid Server ID") - return False + return server = self.controller.get_server_obj(server_id).backup_server() self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py index 3e9913db..4388f909 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -37,9 +37,6 @@ class PublicHandler(BaseHandler): def get(self, page=None): - self.clear_cookie("user") - self.clear_cookie("user_data") - error = bleach.clean(self.get_argument('error', "Invalid Login!")) page_data = { @@ -59,9 +56,16 @@ class PublicHandler(BaseHandler): elif page == "error": template = "public/error.html" + elif page == "logout": + self.clear_cookie("user") + self.clear_cookie("user_data") + self.redirect('/public/login') + return + # if we have no page, let's go to login else: self.redirect('/public/login') + return self.render( template, @@ -82,14 +86,18 @@ class PublicHandler(BaseHandler): # if we don't have a user if not user_data: next_page = "/public/error?error=Login Failed" + self.clear_cookie("user") + self.clear_cookie("user_data") self.redirect(next_page) - return False + return # if they are disabled if not user_data.enabled: next_page = "/public/error?error=Login Failed" + self.clear_cookie("user") + self.clear_cookie("user_data") self.redirect(next_page) - return False + return login_result = helper.verify_pass(entered_password, user_data.password) @@ -118,6 +126,8 @@ class PublicHandler(BaseHandler): next_page = "/panel/dashboard" self.redirect(next_page) else: + self.clear_cookie("user") + self.clear_cookie("user_data") # 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') diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index 7768280d..f89fc1e2 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -96,7 +96,7 @@ class ServerHandler(BaseHandler): for server in db_helper.get_all_defined_servers(): if server['server_name'] == name: return True - return False + return server_data = db_helper.get_server_data_by_id(server_id) server_uuid = server_data.get('server_uuid') @@ -165,14 +165,14 @@ class ServerHandler(BaseHandler): if not server_name: self.redirect("/panel/error?error=Server name cannot be empty!") - return False + return if import_type == 'import_jar': good_path = self.controller.verify_jar_server(import_server_path, import_server_jar) if not good_path: self.redirect("/panel/error?error=Server path or Server Jar not found!") - return False + return 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'], @@ -184,12 +184,12 @@ class ServerHandler(BaseHandler): good_path = self.controller.verify_zip_server(import_server_path) if not good_path: self.redirect("/panel/error?error=Zip file not found!") - return False + return 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)) - return False + return 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, @@ -197,7 +197,7 @@ class ServerHandler(BaseHandler): else: if len(server_parts) != 2: self.redirect("/panel/error?error=Invalid server data") - return False + return 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_type, server_version, server_name, min_mem, max_mem, port) diff --git a/app/frontend/templates/notify.html b/app/frontend/templates/notify.html index 09e6c7da..5cafcacd 100644 --- a/app/frontend/templates/notify.html +++ b/app/frontend/templates/notify.html @@ -29,7 +29,7 @@ {% end %} Activity - Sign Out + Sign Out \ No newline at end of file From f5b6583dbf876ea02c7d2a3832626392992a3e5f Mon Sep 17 00:00:00 2001 From: luukas Date: Fri, 30 Jul 2021 19:27:48 +0300 Subject: [PATCH 07/13] Remove unnecessary console.debug's --- app/classes/shared/tasks.py | 1 - app/classes/web/server_handler.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index 660af321..3a23eeaf 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -211,7 +211,6 @@ class TasksManager: time.sleep(8) def log_watcher(self): - console.debug('in log_watcher') helper.check_for_old_logs(db_helper) schedule.every(6).hours.do(lambda: helper.check_for_old_logs(db_helper)).tag('log-mgmt') diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index f89fc1e2..d5aac5a2 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -107,8 +107,6 @@ class ServerHandler(BaseHandler): name_counter += 1 new_server_name = server_data.get('server_name') + " (Copy {})".format(name_counter) - console.debug('new_server_name: "{}"'.format(new_server_name)) - new_server_uuid = helper.create_uuid() while os.path.exists(os.path.join(helper.servers_dir, new_server_uuid)): new_server_uuid = helper.create_uuid() @@ -145,7 +143,6 @@ class ServerHandler(BaseHandler): }).execute() self.controller.init_all_servers() - console.debug('initted all servers') return From 801d79fdca5e61455f6d217b804c0816c20bd38a Mon Sep 17 00:00:00 2001 From: luukas Date: Fri, 30 Jul 2021 22:50:33 +0300 Subject: [PATCH 08/13] Fix Crafty version string --- app/classes/shared/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 8ee0de64..176c98db 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -157,7 +157,7 @@ class Helpers: version = "{}.{}.{}-{}".format(version_data.get('major', '?'), version_data.get('minor', '?'), version_data.get('sub', '?'), - version_data.get('patch', '?')) + version_data.get('meta', '?')) return str(version) def do_exit(self): From 7b66cc261ef7d9803e20397b801b19ede3c9e355 Mon Sep 17 00:00:00 2001 From: luukas Date: Tue, 10 Aug 2021 22:36:17 +0300 Subject: [PATCH 09/13] Add cache for if language file exists --- app/classes/shared/translation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/classes/shared/translation.py b/app/classes/shared/translation.py index 5809e592..8099ac09 100644 --- a/app/classes/shared/translation.py +++ b/app/classes/shared/translation.py @@ -13,19 +13,19 @@ class Translation(): self.translations_path = os.path.join(helper.root_dir, 'app', 'translations') self.cached_translation = None self.cached_translation_lang = None + self.lang_file_exists = [] def translate(self, page, word): translated_word = None lang = helper.get_setting('language') fallback_lang = 'en_EN' - lang_file_exists = helper.check_file_exists( - os.path.join( - self.translations_path, lang + '.json' - ) - ) + if lang not in self.lang_file_exists and \ + helper.check_file_exists(os.path.join(self.translations_path, lang + '.json')): + self.lang_file_exists.append(lang) + translated_word = self.translate_inner(page, word, lang) \ - if lang_file_exists else self.translate_inner(page, word, fallback_lang) + if lang in self.lang_file_exists else self.translate_inner(page, word, fallback_lang) if translated_word: if isinstance(translated_word, dict): return json.dumps(translated_word) From 4bac56e84aba83ccc392267d69014d680f6a20ed Mon Sep 17 00:00:00 2001 From: luukas Date: Tue, 10 Aug 2021 23:17:56 +0300 Subject: [PATCH 10/13] Use stdout for virtual terminal. WebSockets seem to be "laggy". --- app/classes/shared/helpers.py | 6 ++ app/classes/shared/server.py | 62 ++++++++++++++++++- app/classes/shared/tasks.py | 8 +-- app/classes/web/ajax_handler.py | 6 +- app/classes/web/websocket_handler.py | 15 +++-- app/classes/web/websocket_helper.py | 50 ++++++++++++--- app/config/config.json | 4 +- app/config/logging.json | 2 +- app/frontend/templates/base.html | 5 +- app/frontend/templates/panel/server_term.html | 16 ++++- 10 files changed, 146 insertions(+), 28 deletions(-) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 176c98db..2e675e3a 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -562,4 +562,10 @@ class Helpers: os.path.relpath(os.path.join(root, file), os.path.join(path, '..'))) + @staticmethod + def remove_prefix(text, prefix): + if text.startswith(prefix): + return text[len(prefix):] + return text + helper = Helpers() diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index ab6f6372..c4901125 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -10,11 +10,13 @@ import threading import schedule import logging.config import zipfile +import html from app.classes.shared.helpers import helper from app.classes.shared.console import console from app.classes.shared.models import db_helper, Servers +from app.classes.web.websocket_helper import websocket_helper logger = logging.getLogger(__name__) @@ -27,6 +29,58 @@ except ModuleNotFoundError as e: console.critical("Import Error: Unable to load {} module".format(e.name)) sys.exit(1) +class ServerOutBuf: + lines = {} + def __init__(self, p, server_id): + self.p = p + self.server_id = str(server_id) + # Buffers text for virtual_terminal_lines config number of lines + self.max_lines = helper.get_setting('virtual_terminal_lines') + self.line_buffer = '' + ServerOutBuf.lines[self.server_id] = [] + + def check(self): + while self.p.isalive(): + char = self.p.read(1) + if char == os.linesep: + ServerOutBuf.lines[self.server_id].append(self.line_buffer) + self.new_line_handler(self.line_buffer) + self.line_buffer = '' + # Limit list length to self.max_lines: + if len(ServerOutBuf.lines[self.server_id]) > self.max_lines: + ServerOutBuf.lines[self.server_id].pop(0) + else: + self.line_buffer += char + + def new_line_handler(self, new_line): + console.debug('New line: {}'.format(new_line)) + + highlighted = helper.log_colors(html.escape(new_line)) + + print('broadcasting new vterm line') + + websocket_helper.broadcast_page_params( + '/panel/server_detail', + { + 'id': self.server_id + }, + 'notification', + 'test test test' + ) + + # TODO: Do not send data to clients who do not have permission to view this server's console + websocket_helper.broadcast_page_params( + '/panel/server_detail', + { + 'id': self.server_id + }, + 'vterm_new_line', + { + 'line': highlighted + '
', + 'server_id': self.server_id + } + ) + class Server: @@ -127,7 +181,13 @@ class Server: logger.info("Linux Detected") logger.info("Starting server in {p} with command: {c}".format(p=self.server_path, c=self.server_command)) - self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding=None) + + self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding='utf-8') + out_buf = ServerOutBuf(self.process, self.server_id) + + console.cyan('Start vterm listener') + threading.Thread(target=out_buf.check, daemon=True).start() + self.is_crashed = False self.start_time = str(datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')) diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index 3a23eeaf..69c98e93 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -196,8 +196,9 @@ class TasksManager: host_stats = db_helper.get_latest_hosts_stats() if len(websocket_helper.clients) > 0: + print('there are clients') # There are clients - websocket_helper.broadcast('update_host_stats', { + websocket_helper.broadcast_page('/panel/dashboard', 'update_host_stats', { 'cpu_usage': host_stats.get('cpu_usage'), 'cpu_cores': host_stats.get('cpu_cores'), 'cpu_cur_freq': host_stats.get('cpu_cur_freq'), @@ -205,10 +206,7 @@ class TasksManager: 'mem_percent': host_stats.get('mem_percent'), 'mem_usage': host_stats.get('mem_usage') }) - time.sleep(4) - else: - # Stats are same - time.sleep(8) + time.sleep(4) def log_watcher(self): helper.check_for_old_logs(db_helper) diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index 4f98d98f..35bfb990 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -12,6 +12,7 @@ from app.classes.shared.models import Users, installer from app.classes.web.base_handler import BaseHandler from app.classes.shared.models import db_helper from app.classes.shared.helpers import helper +from app.classes.shared.server import ServerOutBuf logger = logging.getLogger(__name__) @@ -56,16 +57,17 @@ class AjaxHandler(BaseHandler): if not server_data: logger.warning("Server Data not found in server_log ajax call") self.redirect("/panel/error?error=Server ID Not Found") + return if not server_data['log_path']: logger.warning("Log path not found in server_log ajax call ({})".format(server_id)) if full_log: log_lines = helper.get_setting('max_log_lines') + data = helper.tail_file(server_data['log_path'], log_lines) else: - log_lines = helper.get_setting('virtual_terminal_lines') + data = ServerOutBuf.lines.get(server_id, []) - data = helper.tail_file(server_data['log_path'], log_lines) for d in data: try: diff --git a/app/classes/web/websocket_handler.py b/app/classes/web/websocket_handler.py index 107acdc5..3c83dbc0 100644 --- a/app/classes/web/websocket_handler.py +++ b/app/classes/web/websocket_handler.py @@ -1,9 +1,10 @@ import json import logging +from urllib.parse import parse_qsl import tornado.websocket -from app.classes.shared.console import console from app.classes.shared.models import Users, db_helper +from app.classes.shared.helpers import helper from app.classes.web.websocket_helper import websocket_helper logger = logging.getLogger(__name__) @@ -35,6 +36,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler): def open(self): + logger.debug('Checking WebSocket authentication') if self.check_auth(): self.handle() else: @@ -42,10 +44,15 @@ class SocketHandler(tornado.websocket.WebSocketHandler): self.close() db_helper.add_to_audit_log_raw('unknown', 0, 0, 'Someone tried to connect via WebSocket without proper authentication', self.get_remote_ip()) websocket_helper.broadcast('notification', 'Someone tried to connect via WebSocket without proper authentication') + logger.warning('Someone tried to connect via WebSocket without proper authentication') def handle(self): - - websocket_helper.addClient(self) + self.page = self.get_query_argument('page') + self.page_query_params = dict(parse_qsl(helper.remove_prefix( + self.get_query_argument('page_query_params'), + '?' + ))) + websocket_helper.add_client(self) logger.debug('Opened WebSocket connection') # websocket_helper.broadcast('notification', 'New client connected') @@ -56,7 +63,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler): logger.debug('Event Type: {}, Data: {}'.format(message['event'], message['data'])) def on_close(self): - websocket_helper.removeClient(self) + websocket_helper.remove_client(self) logger.debug('Closed WebSocket connection') # websocket_helper.broadcast('notification', 'Client disconnected') diff --git a/app/classes/web/websocket_helper.py b/app/classes/web/websocket_helper.py index 37a85cb4..5fdd72fb 100644 --- a/app/classes/web/websocket_helper.py +++ b/app/classes/web/websocket_helper.py @@ -6,25 +6,59 @@ from app.classes.shared.console import console logger = logging.getLogger(__name__) class WebSocketHelper: - clients = set() + def __init__(self): + self.clients = set() - def addClient(self, client): + def add_client(self, client): self.clients.add(client) - def removeClient(self, client): - self.clients.add(client) + def remove_client(self, client): + self.clients.remove(client) - def send_message(self, client, event_type, data): + def send_message(self, client, event_type: str, data): if client.check_auth(): message = str(json.dumps({'event': event_type, 'data': data})) client.write_message(message) - def broadcast(self, event_type, data): - logger.debug('Sending: ' + str(json.dumps({'event': event_type, 'data': data}))) + def broadcast(self, event_type: str, data): + logger.debug('Sending to {} clients: {}'.format(len(self.clients), json.dumps({'event': event_type, 'data': data}))) for client in self.clients: try: self.send_message(client, event_type, data) - except: + except Exception: + pass + + def broadcast_page(self, page: str, event_type: str, data): + def filter_fn(client): + return client.page == page + + clients = list(filter(filter_fn, self.clients)) + + logger.debug('Sending to {} out of {} clients: {}'.format(len(clients), len(self.clients), json.dumps({'event': event_type, 'data': data}))) + + for client in clients: + try: + self.send_message(client, event_type, data) + except Exception: + pass + + def broadcast_page_params(self, page: str, params: dict, event_type: str, data): + def filter_fn(client): + if client.page != page: + return False + for key, param in params.items(): + if param != client.page_query_params.get(key, None): + return False + return True + + clients = list(filter(filter_fn, self.clients)) + + logger.debug('Sending to {} out of {} clients: {}'.format(len(clients), len(self.clients), json.dumps({'event': event_type, 'data': data}))) + + for client in clients: + try: + self.send_message(client, event_type, data) + except Exception: pass def disconnect_all(self): diff --git a/app/config/config.json b/app/config/config.json index f6b866f3..693c1440 100644 --- a/app/config/config.json +++ b/app/config/config.json @@ -10,7 +10,7 @@ "stats_update_frequency": 30, "delete_default_json": false, "show_contribute_link": true, - "virtual_terminal_lines": 10, + "virtual_terminal_lines": 30, "max_log_lines": 700, "keywords": ["help", "chunk"] -} \ No newline at end of file +} diff --git a/app/config/logging.json b/app/config/logging.json index 8c524b01..c6ccd7b1 100644 --- a/app/config/logging.json +++ b/app/config/logging.json @@ -8,7 +8,7 @@ "tornado_access": { "format": "%(asctime)s - [Tornado] - [Access] - %(levelname)s - %(message)s" }, - "schedule": { + "schedule": { "format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s" } }, diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html index 2a780c64..ad2cb9a5 100644 --- a/app/frontend/templates/base.html +++ b/app/frontend/templates/base.html @@ -173,8 +173,9 @@ let listenEvents = []; try { - - var wsInternal = new WebSocket('wss://' + location.host + '/ws'); + pageQueryParams = 'page_query_params=' + encodeURIComponent(location.search) + page = 'page=' + encodeURIComponent(location.pathname) + var wsInternal = new WebSocket('wss://' + location.host + '/ws?' + page + '&' + pageQueryParams); wsInternal.onopen = function() { console.log('opened WebSocket connection:', wsInternal) }; diff --git a/app/frontend/templates/panel/server_term.html b/app/frontend/templates/panel/server_term.html index 196c56da..92c7e54c 100644 --- a/app/frontend/templates/panel/server_term.html +++ b/app/frontend/templates/panel/server_term.html @@ -161,6 +161,12 @@ } } + function new_line_handler(data) { + if (server_id === data.server_id) { + $('#virt_console').append(data.line) + } + } + //used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); @@ -171,9 +177,13 @@ console.log( "ready!" ); get_server_log() - setInterval(function(){ - get_server_log() // this will run after every 5 seconds - }, 1500); + if (webSocket) { + webSocket.on('vterm_new_line', new_line_handler) + } else { + setInterval(function(){ + get_server_log() // this will run after every 5 seconds + }, 1500); + } }); $('#server_command').on('keydown', function (e) { From d1f582edfe07576361194037e47c402423c6883f Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 11 Aug 2021 23:29:31 +0300 Subject: [PATCH 11/13] Full release on stdout fix! Basically I ditch the ANSI codes and use the old highlighting system. --- app/classes/shared/controller.py | 2 +- app/classes/shared/server.py | 24 +++++------------ app/classes/shared/tasks.py | 1 - app/classes/web/ajax_handler.py | 10 +++---- app/classes/web/tornado.py | 2 +- app/classes/web/websocket_handler.py | 16 +++++++++++- app/classes/web/websocket_helper.py | 26 +++++++++++++------ app/config/config.json | 2 +- app/frontend/templates/panel/server_term.html | 23 ++++++---------- app/translations/en_EN.json | 2 +- app/translations/fi_FI.json | 2 +- 11 files changed, 56 insertions(+), 54 deletions(-) diff --git a/app/classes/shared/controller.py b/app/classes/shared/controller.py index cd568504..72b33ed6 100644 --- a/app/classes/shared/controller.py +++ b/app/classes/shared/controller.py @@ -285,7 +285,7 @@ class Controller: server_command = 'java -Xms{}M -Xmx{}M -jar {} nogui'.format(helper.float_to_string(min_mem), helper.float_to_string(max_mem), full_jar_path) - print('command: ' + server_command) + logger.debug('command: ' + server_command) server_log_file = "{}/logs/latest.log".format(new_server_dir) server_stop = "stop" diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index c4901125..b9e029bf 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -53,20 +53,11 @@ class ServerOutBuf: self.line_buffer += char def new_line_handler(self, new_line): - console.debug('New line: {}'.format(new_line)) - + new_line = re.sub('(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)|(> )', '', new_line) + new_line = re.sub('[A-z]{2}\b\b', '', new_line) highlighted = helper.log_colors(html.escape(new_line)) - print('broadcasting new vterm line') - - websocket_helper.broadcast_page_params( - '/panel/server_detail', - { - 'id': self.server_id - }, - 'notification', - 'test test test' - ) + logger.debug('Broadcasting new virtual terminal line') # TODO: Do not send data to clients who do not have permission to view this server's console websocket_helper.broadcast_page_params( @@ -76,8 +67,7 @@ class ServerOutBuf: }, 'vterm_new_line', { - 'line': highlighted + '
', - 'server_id': self.server_id + 'line': highlighted + '
' } ) @@ -137,7 +127,7 @@ class Server: def run_threaded_server(self): # start the server - self.server_thread = threading.Thread(target=self.start_server, daemon=True) + self.server_thread = threading.Thread(target=self.start_server, daemon=True, name='{}_server_thread'.format(self.server_id)) self.server_thread.start() def setup_server_run_command(self): @@ -185,8 +175,8 @@ class Server: self.process = pexpect.spawn(self.server_command, cwd=self.server_path, timeout=None, encoding='utf-8') out_buf = ServerOutBuf(self.process, self.server_id) - console.cyan('Start vterm listener') - threading.Thread(target=out_buf.check, daemon=True).start() + logger.debug('Starting virtual terminal listener for server {}'.format(self.name)) + threading.Thread(target=out_buf.check, daemon=True, name='{}_virtual_terminal'.format(self.server_id)).start() self.is_crashed = False diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index 69c98e93..2d2586a8 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -196,7 +196,6 @@ class TasksManager: host_stats = db_helper.get_latest_hosts_stats() if len(websocket_helper.clients) > 0: - print('there are clients') # There are clients websocket_helper.broadcast_page('/panel/dashboard', 'update_host_stats', { 'cpu_usage': host_stats.get('cpu_usage'), diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index 35bfb990..68572b61 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -6,6 +6,7 @@ import bleach import os import shutil import html +import re from app.classes.shared.console import console from app.classes.shared.models import Users, installer @@ -71,6 +72,8 @@ class AjaxHandler(BaseHandler): for d in data: try: + d = re.sub('(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)|(> )', '', d) + d = re.sub('[A-z]{2}\b\b', '', d) line = helper.log_colors(html.escape(d)) self.write('{}
'.format(line)) # self.write(d.encode("utf-8")) @@ -152,7 +155,6 @@ class AjaxHandler(BaseHandler): file_name = self.get_body_argument('file_name', default=None, strip=True) file_path = os.path.join(file_parent, file_name) server_id = self.get_argument('id', None) - print(server_id) if not self.check_server_id(server_id, 'create_file'): return else: server_id = bleach.clean(server_id) @@ -172,7 +174,6 @@ class AjaxHandler(BaseHandler): dir_name = self.get_body_argument('dir_name', default=None, strip=True) dir_path = os.path.join(dir_parent, dir_name) server_id = self.get_argument('id', None) - print(server_id) if not self.check_server_id(server_id, 'create_dir'): return else: server_id = bleach.clean(server_id) @@ -211,7 +212,6 @@ class AjaxHandler(BaseHandler): elif page == "del_dir": dir_path = self.get_body_argument('dir_path', default=None, strip=True) server_id = self.get_argument('id', None) - print(server_id) console.warning("delete {} for server {}".format(file_path, server_id)) @@ -235,9 +235,6 @@ class AjaxHandler(BaseHandler): file_contents = self.get_body_argument('file_contents', default=None, strip=True) file_path = self.get_body_argument('file_path', default=None, strip=True) server_id = self.get_argument('id', None) - print(file_contents) - print(file_path) - print(server_id) if not self.check_server_id(server_id, 'save_file'): return else: server_id = bleach.clean(server_id) @@ -256,7 +253,6 @@ class AjaxHandler(BaseHandler): item_path = self.get_body_argument('item_path', default=None, strip=True) new_item_name = self.get_body_argument('new_item_name', default=None, strip=True) server_id = self.get_argument('id', None) - print(server_id) if not self.check_server_id(server_id, 'rename_item'): return else: server_id = bleach.clean(server_id) diff --git a/app/classes/web/tornado.py b/app/classes/web/tornado.py index ced565ea..e3f51619 100644 --- a/app/classes/web/tornado.py +++ b/app/classes/web/tornado.py @@ -159,7 +159,7 @@ class Webserver: console.info("Server Init Complete: Listening For Connections:") - self.ioloop = tornado.ioloop.IOLoop.instance() + self.ioloop = tornado.ioloop.IOLoop.current() self.ioloop.start() def stop_web_server(self): diff --git a/app/classes/web/websocket_handler.py b/app/classes/web/websocket_handler.py index 3c83dbc0..ad98c2ab 100644 --- a/app/classes/web/websocket_handler.py +++ b/app/classes/web/websocket_handler.py @@ -1,14 +1,21 @@ import json import logging +import asyncio from urllib.parse import parse_qsl -import tornado.websocket from app.classes.shared.models import Users, db_helper from app.classes.shared.helpers import helper from app.classes.web.websocket_helper import websocket_helper logger = logging.getLogger(__name__) +try: + import tornado.websocket + +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)) + sys.exit(1) class SocketHandler(tornado.websocket.WebSocketHandler): @@ -16,6 +23,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler): self.controller = controller self.tasks_manager = tasks_manager self.translator = translator + self.io_loop = tornado.ioloop.IOLoop.current() def get_remote_ip(self): remote_ip = self.request.headers.get("X-Real-IP") or \ @@ -67,3 +75,9 @@ class SocketHandler(tornado.websocket.WebSocketHandler): logger.debug('Closed WebSocket connection') # websocket_helper.broadcast('notification', 'Client disconnected') + async def write_message_int(self, message): + self.write_message(message) + + def write_message_helper(self, message): + asyncio.run_coroutine_threadsafe(self.write_message_int(message), self.io_loop.asyncio_loop) + diff --git a/app/classes/web/websocket_helper.py b/app/classes/web/websocket_helper.py index 5fdd72fb..99935ad3 100644 --- a/app/classes/web/websocket_helper.py +++ b/app/classes/web/websocket_helper.py @@ -1,10 +1,20 @@ import json import logging +import sys, threading, asyncio from app.classes.shared.console import console logger = logging.getLogger(__name__) + +try: + import tornado.ioloop + +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)) + sys.exit(1) + class WebSocketHelper: def __init__(self): self.clients = set() @@ -14,19 +24,19 @@ class WebSocketHelper: def remove_client(self, client): self.clients.remove(client) - + def send_message(self, client, event_type: str, data): if client.check_auth(): message = str(json.dumps({'event': event_type, 'data': data})) - client.write_message(message) + client.write_message_helper(message) def broadcast(self, event_type: str, data): logger.debug('Sending to {} clients: {}'.format(len(self.clients), json.dumps({'event': event_type, 'data': data}))) for client in self.clients: try: self.send_message(client, event_type, data) - except Exception: - pass + except Exception as e: + logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip())) def broadcast_page(self, page: str, event_type: str, data): def filter_fn(client): @@ -39,8 +49,8 @@ class WebSocketHelper: for client in clients: try: self.send_message(client, event_type, data) - except Exception: - pass + except Exception as e: + logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip())) def broadcast_page_params(self, page: str, params: dict, event_type: str, data): def filter_fn(client): @@ -58,8 +68,8 @@ class WebSocketHelper: for client in clients: try: self.send_message(client, event_type, data) - except Exception: - pass + except Exception as e: + logger.exception('Error catched while sending WebSocket message to {}'.format(client.get_remote_ip())) def disconnect_all(self): console.info('Disconnecting WebSocket clients') diff --git a/app/config/config.json b/app/config/config.json index 693c1440..27f08bb8 100644 --- a/app/config/config.json +++ b/app/config/config.json @@ -10,7 +10,7 @@ "stats_update_frequency": 30, "delete_default_json": false, "show_contribute_link": true, - "virtual_terminal_lines": 30, + "virtual_terminal_lines": 70, "max_log_lines": 700, "keywords": ["help", "chunk"] } diff --git a/app/frontend/templates/panel/server_term.html b/app/frontend/templates/panel/server_term.html index 92c7e54c..74e09171 100644 --- a/app/frontend/templates/panel/server_term.html +++ b/app/frontend/templates/panel/server_term.html @@ -65,7 +65,7 @@ @@ -147,7 +147,6 @@ let server_id = '{{ data['server_stats']['server_id']['server_id'] }}'; function get_server_log(){ - if( !$("#stop_scroll").is(':checked')){ $.ajax({ type: 'GET', url: '/ajax/server_log?id={{ data['server_stats']['server_id']['server_id'] }}', @@ -155,15 +154,15 @@ success: function (data) { console.log('Got Log From Server') $('#virt_console').html(data); - scroll(); - }, + scrollConsole(); + }, }); - } } function new_line_handler(data) { - if (server_id === data.server_id) { - $('#virt_console').append(data.line) + $('#virt_console').append(data.line) + if (!$("#stop_scroll").is(':checked')) { + scrollConsole() } } @@ -177,13 +176,7 @@ console.log( "ready!" ); get_server_log() - if (webSocket) { - webSocket.on('vterm_new_line', new_line_handler) - } else { - setInterval(function(){ - get_server_log() // this will run after every 5 seconds - }, 1500); - } + webSocket.on('vterm_new_line', new_line_handler) }); $('#server_command').on('keydown', function (e) { @@ -206,7 +199,7 @@ }); - function scroll(){ + function scrollConsole(){ var logview = $('#virt_console'); if(logview.length) logview.scrollTop(logview[0].scrollHeight - logview.height()); diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index d22eff97..16f47d6a 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -120,7 +120,7 @@ "playerControls": "Player Management" }, "serverTerm": { - "stopRefresh": "Stop Refresh", + "stopScroll": "Stop Auto Scrollling", "commandInput": "Enter your command", "sendCommand": "Send command", "start": "Start", diff --git a/app/translations/fi_FI.json b/app/translations/fi_FI.json index c13182fa..a9a34a0e 100644 --- a/app/translations/fi_FI.json +++ b/app/translations/fi_FI.json @@ -120,7 +120,7 @@ "playerControls": "Pelaajahallinta" }, "serverTerm": { - "stopRefresh": "Lopeta päivitys", + "stopScroll": "Lopeta automaattinen vieritys", "commandInput": "Kirjoita komento", "sendCommand": "Lähetä komento", "start": "Käynnistä", From 118fc7936faa3442287c1e1adadffd1263a1f9a5 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 12 Aug 2021 10:16:19 -0400 Subject: [PATCH 12/13] Improved text highlighting for papermc servers. Need to still figure out what's going on with some random characters showing up. --- app/classes/shared/helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 2e675e3a..e1894ae8 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -193,6 +193,9 @@ class Helpers: (r'(\[.+?/ERROR\])', r'\1'), (r'(\w+?\[/\d+?\.\d+?\.\d+?\.\d+?\:\d+?\])', r'\1'), (r'\[(\d\d:\d\d:\d\d)\]', r'[\1]'), + (r'(\[.+? INFO\])', r'\1'), + (r'(\[.+? WARN\])', r'\1'), + (r'(\[.+? ERROR\])', r'\1') ] # highlight users keywords From a54115bed92466323f46b98e12b42ae430d1eda1 Mon Sep 17 00:00:00 2001 From: luukas Date: Thu, 12 Aug 2021 17:54:52 +0300 Subject: [PATCH 13/13] Add pointer cursor to all file tree items --- app/frontend/templates/panel/server_files.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/frontend/templates/panel/server_files.html b/app/frontend/templates/panel/server_files.html index de1cc745..fe8501f9 100644 --- a/app/frontend/templates/panel/server_files.html +++ b/app/frontend/templates/panel/server_files.html @@ -166,8 +166,9 @@ margin-left: 10px; } - /* Style the caret/arrow */ - .tree-caret { + /* Style the items */ + .tree-item, + .files-tree-title { cursor: pointer; user-select: none; /* Prevent text selection */ }