diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd883a7..15bd4c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Changelog ## --- [4.2.2] - 2023/TBD ### New features -TBD +- Loading Screen for Crafty during startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668)) ### Refactor - Remove deprecated API V1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/670)) +- Tidy up main.py to be more comprehensive ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668)) ### Bug fixes - Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664)) +- Bump cryptography for CVE-2023-49083 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/680)) ### Tweaks - Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666)) - Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667)) @@ -15,7 +17,7 @@ TBD - Fix Unban button failing to pardon users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/671)) - Fix stack in API error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/674)) ### Lang -TBD +- pl_PL Minor fixes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/675))

## --- [4.2.1] - 2023/11/01 diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index 99151a32..88923194 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -22,6 +22,7 @@ from app.classes.models.server_permissions import ( PermissionsServers, EnumPermissionsServer, ) +from app.classes.shared.websocket_manager import WebSocketManager logger = logging.getLogger(__name__) @@ -36,6 +37,7 @@ class ServersController(metaclass=Singleton): self.management_helper = management_helper self.servers_list = [] self.stats = Stats(self.helper, self) + self.web_sock = WebSocketManager() self.server_subpage = {} # ********************************************************************************** @@ -170,8 +172,15 @@ class ServersController(metaclass=Singleton): def init_all_servers(self): servers = self.get_all_defined_servers() self.failed_servers = [] - for server in servers: + self.web_sock.broadcast_to_admins( + "update", + {"section": "server", "server": server["server_name"]}, + ) + self.web_sock.broadcast_to_non_admins( + "update", + {"section": "init"}, + ) server_id = server.get("server_id") # if we have already initialized this server, let's skip it. diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 62ce8819..7524cf90 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -80,6 +80,7 @@ class Helpers: self.translation = Translation(self) self.update_available = False self.ignored_names = ["crafty_managed.txt", "db_stats"] + self.crafty_starting = False @staticmethod def auto_installer_fix(ex): diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index 27104b62..c8ac3b3c 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -85,7 +85,7 @@ class Controller: encoding="utf-8", ) as f: self.auth_tracker = json.load(f) - except: + except (FileNotFoundError, json.JSONDecodeError): self.auth_tracker = {} def log_attempt(self, remote_ip, username): diff --git a/app/classes/shared/websocket_manager.py b/app/classes/shared/websocket_manager.py index f48adef8..7cda296d 100644 --- a/app/classes/shared/websocket_manager.py +++ b/app/classes/shared/websocket_manager.py @@ -37,7 +37,15 @@ class WebSocketManager(metaclass=Singleton): def broadcast_to_admins(self, event_type: str, data): def filter_fn(client): - if client.get_user_id in HelperUsers.get_super_user_list(): + if str(client.get_user_id()) in str(HelperUsers.get_super_user_list()): + return True + return False + + self.broadcast_with_fn(filter_fn, event_type, data) + + def broadcast_to_non_admins(self, event_type: str, data): + def filter_fn(client): + if str(client.get_user_id()) not in str(HelperUsers.get_super_user_list()): return True return False diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index e8643aa7..8ac827c3 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -210,6 +210,8 @@ class PanelHandler(BaseHandler): error = self.get_argument("error", "WTF Error!") template = "panel/denied.html" + if self.helper.crafty_starting: + page = "loading" now = time.time() formatted_time = str( @@ -243,9 +245,13 @@ class PanelHandler(BaseHandler): for r in exec_user["roles"]: role = self.controller.roles.get_role(r) exec_user_role.add(role["role_name"]) - defined_servers = self.controller.servers.get_authorized_servers( - exec_user["user_id"] - ) + # get_auth_servers will throw an exception if run while Crafty is starting + if not self.helper.crafty_starting: + defined_servers = self.controller.servers.get_authorized_servers( + exec_user["user_id"] + ) + else: + defined_servers = [] user_order = self.controller.users.get_user_by_id(exec_user["user_id"]) user_order = user_order["server_order"].split(",") @@ -1615,7 +1621,8 @@ class PanelHandler(BaseHandler): logs_thread.start() self.redirect("/panel/dashboard") return - + if self.helper.crafty_starting: + template = "panel/loading.html" self.render( template, data=page_data, diff --git a/app/frontend/templates/panel/loading.html b/app/frontend/templates/panel/loading.html new file mode 100644 index 00000000..a56a75ee --- /dev/null +++ b/app/frontend/templates/panel/loading.html @@ -0,0 +1,73 @@ +{% extends ../base.html %} + +{% block meta %} +{% end %} + +{% block title %}Crafty Controller Starting{% end %} + +{% block content %} +
+
+
+ Crafty Logo, Crafty is loading +
+
+
+
+

+ {{ translate('startup', 'starting', data['lang']) }}

+
+ +
+
+ + +{% end %} diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index 6db96dfc..ab4c54ce 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -590,6 +590,15 @@ "newServer": "Create New Server", "servers": "Servers" }, + "startup": { + "starting": "Crafty Is Starting...", + "serverInit": "Initializing Servers", + "server": "Initializing ", + "internet": "Checking for internet connection", + "tasks": "Starting Tasks Scheduler", + "internals": "Configuring and starting Crafty's internal componenets", + "almost": "Finishing up. Hang on tight..." + }, "userConfig": { "apiKey": "API Keys", "auth": "Authorized? ", @@ -655,4 +664,4 @@ "webhook_body": "Webhook Body", "webhooks": "Webhooks" } -} +} \ No newline at end of file diff --git a/app/translations/pl_PL.json b/app/translations/pl_PL.json index 828ec0b1..2c60c6df 100644 --- a/app/translations/pl_PL.json +++ b/app/translations/pl_PL.json @@ -326,8 +326,8 @@ "bePatientDeleteFiles": "Poczekaj, aż usuniemy twój serwer i jego pliki. Strona za chwilę się zamknie.", "bePatientUpdate": "Poczekaj kiedy my aktualizujemy twój serwer. Pobieranie zależy od prędkości twojego internetu.
Strona się odświeży za chwile.", "cancel": "Anuluj", - "crashTime": "Crash wyszedł poza limit czasu", - "crashTimeDesc": "How long should we wait before we consider your server as crashed?", + "crashTime": "Crash serwera wyszedł poza limit czasu", + "crashTimeDesc": "Jak długo powinniśmy poczekać zanim uznać serwer za zcrashowany?", "deleteFilesQuestion": "Usuń pliki serwera z maszyny?", "deleteFilesQuestionMessage": "Czy chcesz aby Crafty usunął wszystkie pliki tego serwera?

To zawiera backupy.", "deleteServer": "Usuń serwer", @@ -403,7 +403,7 @@ "filterList": "Filtrowane słowa", "logs": "Logi", "metrics": "Statystyki", - "playerControls": "Player Management", + "playerControls": "Zarządzanie użytkownikami", "reset": "Resetuj Scrolla", "schedule": "Harmonogram", "serverDetails": "Detale serwera", @@ -421,7 +421,7 @@ "deleteItemQuestion": "Czy jesteś pewien że chcesz usunąć \" + name + \"?", "deleteItemQuestionMessage": "Usuwasz \\\"\" + path + \"\\\"!

Ta akcja jest nieodwracalna i zostanie usunięta na zawsze!", "download": "Pobierz", - "editingFile": "Edytuję plik", + "editingFile": "Edytuj plik", "error": "Error while getting files", "fileReadError": "Error odczytu pliku", "files": "Pliki", @@ -432,7 +432,7 @@ "rename": "Zmień nazwę", "renameItemQuestion": "Jaka ma być nowa nazwa?", "save": "Zapisz", - "size": "Włącz zmienianie rozmiaru edytora", + "size": "Włącz rozszerzanie i zmniejszanie edytora", "stayHere": "NIE WYCHODŹ Z TEJ STRONY!", "unsupportedLanguage": "Uwaga: To nie jest wspierany typ pliku", "unzip": "Rozpakuj", @@ -545,7 +545,7 @@ "buildServer": "Zbuduj serwer!", "clickRoot": "Kilknij tutaj aby zaznaczyć główną ścieżkę", "close": "Zamknij", - "defaultPort": "25565 podstawowy", + "defaultPort": "Domyślnie 25565", "downloading": "Pobieranie serwera...", "explainRoot": "Proszę, kliknij przycisk poniżej aby zaznaczyć główną ścieżkę w tym archiwum", "importServer": "Importuj egzystujący serwer", @@ -640,12 +640,12 @@ "edit": "Edytuj", "enabled": "Włączony", "jar_update": "Plik startowy zaktualizowany", - "kill": "Serwer zatrzymany", + "kill": "Serwer został zabity", "name": "Nazwa", "new": "Nowy Webhook", "newWebhook": "Nowy Webhook", "no-webhook": "Nie posiadasz aktualnie żadnych Webhooków dla tego serwera. Aby dodać webhook kliknij na", - "run": "Włącz Webhook", + "run": "Przetestuj Webhook", "send_command": "Komenda serwera otrzymana!", "start_server": "Serwer włączony", "stop_server": "Serwer wyłączony", diff --git a/main.py b/main.py index 4cd78814..cac130c1 100644 --- a/main.py +++ b/main.py @@ -20,6 +20,18 @@ from app.classes.shared.websocket_manager import WebSocketManager console = Console() helper = Helpers() +# Get the path our application is running on. +if getattr(sys, "frozen", False): + APPLICATION_PATH = os.path.dirname(sys.executable) + RUNNING_MODE = "Frozen/executable" +else: + try: + app_full_path = os.path.realpath(__file__) + APPLICATION_PATH = os.path.dirname(app_full_path) + RUNNING_MODE = "Non-interactive (e.g. 'python main.py')" + except NameError: + APPLICATION_PATH = os.getcwd() + RUNNING_MODE = "Interactive" if helper.check_root(): Console.critical( "Root detected. Root/Admin access denied. " @@ -51,7 +63,178 @@ except ModuleNotFoundError as err: helper.auto_installer_fix(err) +def internet_check(): + """ + This checks to see if the Crafty host is connected to the + internet. This will show a warning in the console if no interwebs. + """ + print() + logger.info("Checking Internet. This may take a minute.") + Console.info("Checking Internet. This may take a minute.") + + if not helper.check_internet(): + logger.warning( + "We have detected the machine running Crafty has no " + "connection to the internet. Client connections to " + "the server may be limited." + ) + Console.warning( + "We have detected the machine running Crafty has no " + "connection to the internet. Client connections to " + "the server may be limited." + ) + + +def controller_setup(): + """ + Method sets up the software controllers. + This also sets the application path as well as the + master server dir (if not set). + + This also clears the support logs status. + """ + if not controller.check_system_user(): + controller.add_system_user() + + master_server_dir = controller.management.get_master_server_dir() + if master_server_dir == "": + logger.debug("Could not find master server path. Setting default") + controller.set_master_server_dir( + os.path.join(controller.project_root, "servers") + ) + else: + helper.servers_dir = master_server_dir + + Console.debug(f"Execution Mode: {RUNNING_MODE}") + Console.debug(f"Application path : '{APPLICATION_PATH}'") + + controller.clear_support_status() + + +def tasks_starter(): + """ + Method starts stats recording, app scheduler, and + serverjars/steamCMD cache refreshers + """ + # start stats logging + tasks_manager.start_stats_recording() + + # once the controller is up and stats are logging, we can kick off + # the scheduler officially + tasks_manager.start_scheduler() + + # refresh our cache and schedule for every 12 hoursour cache refresh + # for serverjars.com + tasks_manager.serverjar_cache_refresher() + + +def signal_handler(signum, _frame): + """ + Method handles sigterm and shuts the app down. + """ + if not args.daemon: + print() # for newline after prompt + signame = signal.Signals(signum).name + logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") + Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") + tasks_manager._main_graceful_exit() + crafty_prompt.universal_exit() + + +def do_cleanup(): + """ + Checks Crafty's temporary directory and clears it out on boot. + """ + try: + logger.info("Removing old temp dirs") + FileHelpers.del_dirs(os.path.join(controller.project_root, "temp")) + except: + logger.info("Did not find old temp dir.") + os.mkdir(os.path.join(controller.project_root, "temp")) + + +def do_version_check(): + """ + Checks for remote version differences. + + Prints in terminal with differences if true. + + Also sets helper variable to update available when pages + are served. + """ + + # Check if new version available + remote_ver = helper.check_remote_version() + if remote_ver: + notice = f""" + A new version of Crafty is available! + {'/' * 37} + New version available: {remote_ver} + Current version: {pkg_version.parse(helper.get_version_string())} + {'/' * 37} + """ + Console.yellow(notice) + + crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > " + + +def setup_starter(): + """ + This method starts our setup threads. + (tasks scheduler, internet checks, controller setups) + + Once our threads complete we will set our startup + variable to false and send a reload to any clients waiting. + + + """ + if not args.daemon: + time.sleep(0.01) # Wait for the prompt to start + print() # Make a newline after the prompt so logs are on an empty line + else: + time.sleep(0.01) # Wait for the daemon info message + + Console.info("Setting up Crafty's internal components...") + # Start the setup threads + web_sock.broadcast("update", {"section": "tasks"}) + time.sleep(2) + tasks_starter_thread.start() + web_sock.broadcast("update", {"section": "internet"}) + time.sleep(2) + internet_check_thread.start() + web_sock.broadcast( + "update", + {"section": "internals"}, + ) + time.sleep(2) + controller_setup_thread.start() + + # Wait for the setup threads to finish + web_sock.broadcast( + "update", + {"section": "almost"}, + ) + tasks_starter_thread.join() + internet_check_thread.join() + controller_setup_thread.join() + helper.crafty_starting = False + web_sock.broadcast("send_start_reload", "") + do_version_check() + Console.info("Crafty has fully started and is now ready for use!") + + do_cleanup() + + if not args.daemon: + # Put the prompt under the cursor + crafty_prompt.print_prompt() + + def do_intro(): + """ + Runs the Crafty Controller Terminal Intro with information about the software + This method checks for a "settings file" or config.json. If it does not find + one it will create one. + """ logger.info("***** Crafty Controller Started *****") version = helper.get_version_string() @@ -72,6 +255,11 @@ def do_intro(): def setup_logging(debug=True): + """ + This method sets up our logging for Crafty. It takes + one optional (defaulted to True) parameter which + determines whether or not the logging level is "debug" or verbose. + """ logging_config_file = os.path.join(os.path.curdir, "app", "config", "logging.json") if not helper.check_file_exists( os.path.join(os.path.curdir, "logs", "auth_tracker.log") @@ -117,11 +305,11 @@ if __name__ == "__main__": ) args = parser.parse_args() - helper.ensure_logging_setup() - + helper.crafty_starting = True + # Init WebSocket Manager Here + web_sock = WebSocketManager() setup_logging(debug=args.verbose) - if args.verbose: Console.level = "debug" @@ -133,23 +321,27 @@ if __name__ == "__main__": # print our pretty start message do_intro() - # our session file, helps prevent multiple controller agents on the same machine. helper.create_session_file(ignore=args.ignore) - # start the database database = peewee.SqliteDatabase( helper.db_path, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10} ) database_proxy.initialize(database) - migration_manager = MigrationManager(database, helper) migration_manager.up() # Automatically runs migrations - # do our installer stuff + # init classes + # now the tables are created, we can load the tasks_manager and server controller user_helper = HelperUsers(database, helper) management_helper = HelpersManagement(database, helper) installer = DatabaseBuilder(database, helper, user_helper, management_helper) + file_helper = FileHelpers(helper) + import_helper = ImportHelpers(helper, file_helper) + controller = Controller(database, helper, file_helper, import_helper) + controller.set_project_root(APPLICATION_PATH) + tasks_manager = TasksManager(helper, controller, file_helper) + import3 = Import3(helper, controller) FRESH_INSTALL = installer.is_fresh_install() if FRESH_INSTALL: @@ -171,153 +363,45 @@ if __name__ == "__main__": helper.set_setting("reset_secrets_on_next_boot", False) else: Console.info("No flag found. Secrets are staying") - file_helper = FileHelpers(helper) - import_helper = ImportHelpers(helper, file_helper) - # Init WebSocket Manager Here - WebSocketManager() - # now the tables are created, we can load the tasks_manager and server controller - controller = Controller(database, helper, file_helper, import_helper) + + # Check to see if client config.json version is different than the + # Master config.json in helpers.py Console.info("Checking for remote changes to config.json") controller.get_config_diff() Console.info("Remote change complete.") - import3 = Import3(helper, controller) - tasks_manager = TasksManager(helper, controller, file_helper) + # startup the web server tasks_manager.start_webserver() - def signal_handler(signum, _frame): - if not args.daemon: - print() # for newline after prompt - signame = signal.Signals(signum).name - logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") - Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") - tasks_manager._main_graceful_exit() - crafty_prompt.universal_exit() - signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) # init servers logger.info("Initializing all servers defined") Console.info("Initializing all servers defined") + web_sock.broadcast( + "update", + {"section": "serverInit"}, + ) controller.servers.init_all_servers() - def tasks_starter(): - # start stats logging - tasks_manager.start_stats_recording() - - # once the controller is up and stats are logging, we can kick off - # the scheduler officially - tasks_manager.start_scheduler() - - # refresh our cache and schedule for every 12 hoursour cache refresh - # for serverjars.com - tasks_manager.serverjar_cache_refresher() - + # start up our tasks handler in tasks.py tasks_starter_thread = Thread(target=tasks_starter, name="tasks_starter") - def internet_check(): - print() - logger.info("Checking Internet. This may take a minute.") - Console.info("Checking Internet. This may take a minute.") - - if not helper.check_internet(): - logger.warning( - "We have detected the machine running Crafty has no " - "connection to the internet. Client connections to " - "the server may be limited." - ) - Console.warning( - "We have detected the machine running Crafty has no " - "connection to the internet. Client connections to " - "the server may be limited." - ) - + # check to see if instance has internet internet_check_thread = Thread(target=internet_check, name="internet_check") - def controller_setup(): - if not controller.check_system_user(): - controller.add_system_user() - - if getattr(sys, "frozen", False): - application_path = os.path.dirname(sys.executable) - running_mode = "Frozen/executable" - else: - try: - app_full_path = os.path.realpath(__file__) - application_path = os.path.dirname(app_full_path) - running_mode = "Non-interactive (e.g. 'python main.py')" - except NameError: - application_path = os.getcwd() - running_mode = "Interactive" - - controller.set_project_root(application_path) - master_server_dir = controller.management.get_master_server_dir() - if master_server_dir == "": - logger.debug("Could not find master server path. Setting default") - controller.set_master_server_dir( - os.path.join(controller.project_root, "servers") - ) - else: - helper.servers_dir = master_server_dir - - Console.debug(f"Execution Mode: {running_mode}") - Console.debug(f"Application path : '{application_path}'") - - controller.clear_support_status() - + # start the Crafty console. crafty_prompt = MainPrompt( helper, tasks_manager, migration_manager, controller, import3 ) + # set up all controllers controller_setup_thread = Thread(target=controller_setup, name="controller_setup") - def setup_starter(): - if not args.daemon: - time.sleep(0.01) # Wait for the prompt to start - print() # Make a newline after the prompt so logs are on an empty line - else: - time.sleep(0.01) # Wait for the daemon info message + setup_starter_thread = Thread(target=setup_starter, name="setup_starter") - Console.info("Setting up Crafty's internal components...") - - # Start the setup threads - tasks_starter_thread.start() - internet_check_thread.start() - controller_setup_thread.start() - - # Wait for the setup threads to finish - tasks_starter_thread.join() - internet_check_thread.join() - controller_setup_thread.join() - - Console.info("Crafty has fully started and is now ready for use!") - - # Check if new version available - remote_ver = helper.check_remote_version() - if remote_ver: - notice = f""" - A new version of Crafty is available! - {'/' * 37} - New version available: {remote_ver} - Current version: {pkg_version.parse(helper.get_version_string())} - {'/' * 37} - """ - Console.yellow(notice) - - crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > " - try: - logger.info("Removing old temp dirs") - FileHelpers.del_dirs(os.path.join(controller.project_root, "temp")) - except: - logger.info("Did not find old temp dir.") - os.mkdir(os.path.join(controller.project_root, "temp")) - - if not args.daemon: - # Put the prompt under the cursor - crafty_prompt.print_prompt() - - Thread(target=setup_starter, name="setup_starter").start() + setup_starter_thread.start() if not args.daemon: # Start the Crafty prompt diff --git a/requirements.txt b/requirements.txt index 1e9feb0f..7b1adcfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,13 +4,13 @@ argon2-cffi==23.1.0 cached_property==1.5.2 colorama==0.4.6 croniter==1.4.1 -cryptography==41.0.4 +cryptography==41.0.7 libgravatar==1.0.4 nh3==0.2.14 packaging==23.2 peewee==3.13 psutil==5.9.5 -pyOpenSSL==23.2.0 +pyOpenSSL==23.3.0 pyjwt==2.8.0 PyYAML==6.0.1 requests==2.31.0