diff --git a/.gitignore b/.gitignore index 7afd4ec3..b49824d0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ venv.bak/ .idea/ servers/ +backups/ session.lock .header default.json diff --git a/app/classes/minecraft/serverjars.py b/app/classes/minecraft/serverjars.py index 9a1a2505..8c91cd96 100644 --- a/app/classes/minecraft/serverjars.py +++ b/app/classes/minecraft/serverjars.py @@ -9,7 +9,6 @@ from datetime import datetime from app.classes.shared.helpers import helper from app.classes.shared.console import console from app.classes.shared.models import Servers -# from app.classes.shared.controller import controller from app.classes.minecraft.server_props import ServerProps logger = logging.getLogger(__name__) diff --git a/app/classes/minecraft/stats.py b/app/classes/minecraft/stats.py index 2d4a4209..01e060a4 100644 --- a/app/classes/minecraft/stats.py +++ b/app/classes/minecraft/stats.py @@ -8,7 +8,6 @@ import datetime from app.classes.shared.helpers import helper from app.classes.minecraft.mc_ping import ping -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper from app.classes.shared.models import Host_Stats, Server_Stats @@ -17,6 +16,9 @@ logger = logging.getLogger(__name__) class Stats: + def __init__(self, controller): + self.controller = controller + def get_node_stats(self): boot_time = datetime.datetime.fromtimestamp(psutil.boot_time()) data = {} @@ -184,7 +186,7 @@ class Stats: server_stats_list = [] server_stats = {} - servers = controller.servers_list + servers = self.controller.servers_list logger.info("Getting Stats for all servers...") @@ -210,7 +212,7 @@ class Stats: internal_ip = server_data.get('server_ip', "127.0.0.1") server_port = server_settings.get('server_port', "25565") - logger.debug("Pinging {} on port {}".format(internal_ip, server_port)) + logger.debug("Pinging server '{}' on {}:{}".format(s.get('server_name', "ID#{}".format(server_id)), internal_ip, server_port)) int_mc_ping = ping(internal_ip, int(server_port)) int_data = False @@ -287,6 +289,4 @@ class Stats: last_week = now.day - max_age Host_Stats.delete().where(Host_Stats.time < last_week).execute() - Server_Stats.delete().where(Server_Stats.created < last_week).execute() - -stats = Stats() + Server_Stats.delete().where(Server_Stats.created < last_week).execute() \ No newline at end of file diff --git a/app/classes/shared/cmd.py b/app/classes/shared/cmd.py index 06f29434..b7124f79 100644 --- a/app/classes/shared/cmd.py +++ b/app/classes/shared/cmd.py @@ -9,7 +9,6 @@ logger = logging.getLogger(__name__) from app.classes.shared.console import console from app.classes.shared.helpers import helper -from app.classes.shared.tasks import tasks_manager from app.classes.web.websocket_helper import websocket_helper try: @@ -21,7 +20,11 @@ except ModuleNotFoundError as e: sys.exit(1) -class MainPrompt(cmd.Cmd): +class MainPrompt(cmd.Cmd, object): + + def __init__(self, tasks_manager): + super().__init__() + self.tasks_manager = tasks_manager # overrides the default Prompt prompt = "Crafty Controller v{} > ".format(helper.get_version_string()) @@ -48,7 +51,7 @@ class MainPrompt(cmd.Cmd): self._clean_shutdown() console.info('Waiting for main thread to stop') while True: - if tasks_manager.get_main_thread_run_status(): + if self.tasks_manager.get_main_thread_run_status(): sys.exit(0) time.sleep(1) diff --git a/app/classes/shared/controller.py b/app/classes/shared/controller.py index cf849c40..35922477 100644 --- a/app/classes/shared/controller.py +++ b/app/classes/shared/controller.py @@ -15,6 +15,7 @@ from app.classes.shared.models import db_helper, Servers from app.classes.shared.server import Server from app.classes.minecraft.server_props import ServerProps from app.classes.minecraft.serverjars import server_jar_obj +from app.classes.minecraft.stats import Stats logger = logging.getLogger(__name__) @@ -23,6 +24,7 @@ class Controller: def __init__(self): self.servers_list = [] + self.stats = Stats(self) def check_server_loaded(self, server_id_to_check: int): @@ -72,7 +74,7 @@ class Controller: temp_server_dict = { 'server_id': s.get('server_id'), 'server_data_obj': s, - 'server_obj': Server(), + 'server_obj': Server(self.stats), 'server_settings': settings.props } @@ -94,7 +96,6 @@ class Controller: server_obj.reload_server_settings() def get_server_obj(self, server_id): - for s in self.servers_list: if int(s['server_id']) == int(server_id): return s['server_obj'] @@ -190,12 +191,14 @@ class Controller: def create_jar_server(self, server: str, version: str, name: str, min_mem: int, max_mem: int, port: int): server_id = helper.create_uuid() server_dir = os.path.join(helper.servers_dir, server_id) + backup_path = os.path.join(helper.backup_path, server_id) server_file = "{server}-{version}.jar".format(server=server, version=version) full_jar_path = os.path.join(server_dir, server_file) # make the dir - perhaps a UUID? helper.ensure_dir_exists(server_dir) + helper.ensure_dir_exists(backup_path) try: # do a eula.txt @@ -221,7 +224,7 @@ class Controller: # download the jar server_jar_obj.download_jar(server, version, full_jar_path) - new_id = self.register_server(name, server_id, server_dir, server_command, server_file, server_log_file, server_stop) + new_id = self.register_server(name, server_id, server_dir, backup_path, server_command, server_file, server_log_file, server_stop) return new_id @staticmethod @@ -242,8 +245,10 @@ class Controller: def import_jar_server(self, server_name: str, server_path: str, server_jar: str, min_mem: int, max_mem: int, port: int): server_id = helper.create_uuid() new_server_dir = os.path.join(helper.servers_dir, server_id) + backup_path = os.path.join(helper.backup_path, server_id) helper.ensure_dir_exists(new_server_dir) + helper.ensure_dir_exists(backup_path) dir_util.copy_tree(server_path, new_server_dir) full_jar_path = os.path.join(new_server_dir, server_jar) @@ -253,15 +258,18 @@ class Controller: server_log_file = "{}/logs/latest.log".format(new_server_dir) server_stop = "stop" - new_id = self.register_server(server_name, server_id, new_server_dir, server_command, server_jar, + new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_jar, server_log_file, server_stop, port) return new_id def import_zip_server(self, server_name: str, zip_path: str, server_jar: str, min_mem: int, max_mem: int, port: int): server_id = helper.create_uuid() new_server_dir = os.path.join(helper.servers_dir, server_id) + backup_path = os.path.join(helper.backup_path, server_id) + if helper.check_file_perms(zip_path): helper.ensure_dir_exists(new_server_dir) + helper.ensure_dir_exists(backup_path) with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(new_server_dir) else: @@ -275,11 +283,11 @@ class Controller: server_log_file = "{}/logs/latest.log".format(new_server_dir) server_stop = "stop" - new_id = self.register_server(server_name, server_id, new_server_dir, server_command, server_jar, + new_id = self.register_server(server_name, server_id, new_server_dir, backup_path, server_command, server_jar, server_log_file, server_stop, port) return new_id - def register_server(self, name: str, server_id: str, server_dir: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port=25565): + def register_server(self, name: str, server_id: str, server_dir: str, backup_path: str, server_command: str, server_file: str, server_log_file: str, server_stop: str, server_port=25565): # put data in the db new_id = Servers.insert({ Servers.server_name: name, @@ -292,7 +300,8 @@ class Controller: Servers.crash_detection: False, Servers.log_path: server_log_file, Servers.server_port: server_port, - Servers.stop_command: server_stop + Servers.stop_command: server_stop, + Servers.backup_path: backup_path }).execute() try: @@ -336,6 +345,3 @@ class Controller: self.servers_list.pop(counter) counter += 1 - - -controller = Controller() diff --git a/app/classes/shared/exceptions.py b/app/classes/shared/exceptions.py new file mode 100644 index 00000000..a5de82fa --- /dev/null +++ b/app/classes/shared/exceptions.py @@ -0,0 +1,8 @@ +class CraftyException(Exception): + pass + +class DatabaseException(CraftyException): + pass + +class SchemaError(DatabaseException): + pass \ No newline at end of file diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 4a1bc4ed..927c583e 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -10,6 +10,8 @@ import socket import random import logging import html +import zipfile +import pathlib from datetime import datetime from socket import gethostname @@ -36,6 +38,7 @@ class Helpers: self.config_dir = os.path.join(self.root_dir, 'app', 'config') self.webroot = os.path.join(self.root_dir, 'app', 'frontend') self.servers_dir = os.path.join(self.root_dir, 'servers') + self.backup_path = os.path.join(self.root_dir, 'backups') self.session_file = os.path.join(self.root_dir, 'app', 'config', 'session.lock') self.settings_file = os.path.join(self.root_dir, 'app', 'config', 'config.json') @@ -306,6 +309,8 @@ class Helpers: @staticmethod def check_path_exists(path: str): + if not path: + return False logger.debug('Looking for path: {}'.format(path)) if os.path.exists(path): @@ -372,6 +377,19 @@ class Helpers: total += entry.stat(follow_symlinks=False).st_size return total + @staticmethod + def list_dir_by_date(path: str, reverse=False): + return [str(p) for p in sorted(pathlib.Path(path).iterdir(), key=os.path.getmtime, reverse=reverse)] + + def get_human_readable_files_sizes(self, paths: list): + sizes = [] + for p in paths: + sizes.append({ + "path": p, + "size": self.human_readable_file_size(os.stat(p).st_size) + }) + return sizes + @staticmethod def base64_encode_string(string: str): s_bytes = str(string).encode('utf-8') @@ -521,7 +539,7 @@ class Helpers: @staticmethod def get_banned_players(server_id, db_helper): stats = db_helper.get_server_stats_by_id(server_id) - server_path = stats[0]['server_id']['path'] + server_path = stats['server_id']['path'] path = os.path.join(server_path, 'banned-players.json') try: @@ -534,5 +552,13 @@ class Helpers: return json.loads(content) + @staticmethod + def zip_directory(file, path, compression=zipfile.ZIP_LZMA): + with zipfile.ZipFile(file, 'w', compression) as zf: + for root, dirs, files in os.walk(path): + for file in files: + zf.write(os.path.join(root, file), + os.path.relpath(os.path.join(root, file), + os.path.join(path, '..'))) helper = Helpers() diff --git a/app/classes/shared/models.py b/app/classes/shared/models.py index 2ac6f602..634234c7 100644 --- a/app/classes/shared/models.py +++ b/app/classes/shared/models.py @@ -9,6 +9,8 @@ from app.classes.minecraft.server_props import ServerProps from app.classes.web.websocket_helper import websocket_helper logger = logging.getLogger(__name__) +peewee_logger = logging.getLogger('peewee') +peewee_logger.setLevel(logging.INFO) try: from peewee import * @@ -20,15 +22,30 @@ except ModuleNotFoundError as e: console.critical("Import Error: Unable to load {} module".format(e, e.name)) sys.exit(1) +schema_version = (0, 1, 0) # major, minor, patch semver + database = SqliteDatabase(helper.db_path, pragmas={ 'journal_mode': 'wal', 'cache_size': -1024 * 10}) - class BaseModel(Model): class Meta: database = database +class SchemaVersion(BaseModel): + # DO NOT EVER CHANGE THE SCHEMA OF THIS TABLE + # (unless we have a REALLY good reason to) + # There will only ever be one row, and it allows the database loader to detect + # what it needs to do on major version upgrades so you don't have to wipe the DB + # every time you upgrade + schema_major = IntegerField() + schema_minor = IntegerField() + schema_patch = IntegerField() + + class Meta: + table_name = 'schema_version' + primary_key = CompositeKey('schema_major', 'schema_minor', 'schema_patch') + class Users(BaseModel): user_id = AutoField() @@ -97,6 +114,7 @@ class Servers(BaseModel): server_uuid = CharField(default="", index=True) server_name = CharField(default="Server", index=True) path = CharField(default="") + backup_path = CharField(default="") executable = CharField(default="") log_path = CharField(default="") execution_command = CharField(default="") @@ -111,16 +129,6 @@ class Servers(BaseModel): class Meta: table_name = "servers" - -class User_Servers(BaseModel): - user_id = ForeignKeyField(Users, backref='user_server') - server_id = ForeignKeyField(Servers, backref='user_server') - - class Meta: - table_name = 'user_servers' - primary_key = CompositeKey('user_id', 'server_id') - - class Role_Servers(BaseModel): role_id = ForeignKeyField(Roles, backref='role_server') server_id = ForeignKeyField(Servers, backref='role_server') @@ -178,12 +186,25 @@ class Webhooks(BaseModel): class Meta: table_name = "webhooks" +class Schedules(BaseModel): + schedule_id = IntegerField(unique=True, primary_key=True) + server_id = ForeignKeyField(Servers, backref='schedule_server') + enabled = BooleanField() + action = CharField() + interval = IntegerField() + interval_type = CharField() + start_time = CharField(null=True) + command = CharField(null=True) + comment = CharField() + + class Meta: + table_name = 'schedules' class Backups(BaseModel): - directories = CharField() - storage_location = CharField() + directories = CharField(null=True) max_backups = IntegerField() - server_id = IntegerField(index=True) + server_id = ForeignKeyField(Servers, backref='backups_server') + schedule_id = ForeignKeyField(Schedules, backref='backups_schedule') class Meta: table_name = 'backups' @@ -202,17 +223,23 @@ class db_builder: Host_Stats, Webhooks, Servers, - User_Servers, Role_Servers, Server_Stats, Commands, - Audit_Log + Audit_Log, + SchemaVersion, + Schedules ]) @staticmethod def default_settings(): logger.info("Fresh Install Detected - Creating Default Settings") console.info("Fresh Install Detected - Creating Default Settings") + SchemaVersion.insert({ + SchemaVersion.schema_major: schema_version[0], + SchemaVersion.schema_minor: schema_version[1], + SchemaVersion.schema_patch: schema_version[2] + }).execute() default_data = helper.find_default_password() username = default_data.get("username", 'admin') @@ -239,9 +266,39 @@ class db_builder: return True pass + @staticmethod + def check_schema_version(): + svs = SchemaVersion.select().execute() + if len(svs) != 1: + raise exceptions.SchemaError("Multiple or no schema versions detected - potentially a failed upgrade?") + sv = svs[0] + svt = (sv.schema_major, sv.schema_minor, sv.schema_patch) + logger.debug("Schema: found {}, expected {}".format(svt, schema_version)) + console.debug("Schema: found {}, expected {}".format(svt, schema_version)) + if sv.schema_major > schema_version[0]: + raise exceptions.SchemaError("Major version mismatch - possible code reversion") + elif sv.schema_major < schema_version[0]: + db_shortcuts.upgrade_schema() + + if sv.schema_minor > schema_version[1]: + logger.warning("Schema minor mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + console.warning("Schema minor mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + elif sv.schema_minor < schema_version[1]: + db_shortcuts.upgrade_schema() + + if sv.schema_patch > schema_version[2]: + logger.info("Schema patch mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + console.info("Schema patch mismatch detected: found {}, expected {}. Proceed with caution".format(svt, schema_version)) + elif sv.schema_patch < schema_version[2]: + db_shortcuts.upgrade_schema() + logger.info("Schema validation successful! {}".format(schema_version)) class db_shortcuts: + @staticmethod + def upgrade_schema(): + raise NotImplemented("I don't know who you are or how you reached this code, but this should NOT have happened. Please report it to the developer with due haste.") + @staticmethod def return_rows(query): rows = [] @@ -258,13 +315,12 @@ class db_shortcuts: @staticmethod def get_server_data_by_id(server_id): + query = Servers.select().where(Servers.server_id == server_id).limit(1) try: - query = Servers.get_by_id(server_id) - except DoesNotExist: + return db_helper.return_rows(query)[0] + except IndexError: return {} - return model_to_dict(query) - @staticmethod def get_all_defined_servers(): query = Servers.select() @@ -277,13 +333,13 @@ class db_shortcuts: for s in servers: latest = Server_Stats.select().where(Server_Stats.server_id == s.get('server_id')).order_by(Server_Stats.created.desc()).limit(1) - server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)}) + server_data.append({'server_data': s, "stats": db_helper.return_rows(latest)[0]}) return server_data @staticmethod def get_server_stats_by_id(server_id): stats = Server_Stats.select().where(Server_Stats.server_id == server_id).order_by(Server_Stats.created.desc()).limit(1) - return db_helper.return_rows(stats) + return db_helper.return_rows(stats)[0] @staticmethod def server_id_exists(server_id): @@ -316,6 +372,8 @@ class db_shortcuts: @staticmethod def get_userid_by_name(username): + if username == "SYSTEM": + return 0 try: return (Users.get(Users.username == username)).user_id except DoesNotExist: @@ -323,6 +381,21 @@ class db_shortcuts: @staticmethod def get_user(user_id): + if user_id == 0: + return { + user_id: 0, + created: None, + last_login: None, + last_update: None, + last_ip: "127.27.23.89", + username: "SYSTEM", + password: None, + enabled: True, + superuser: False, + api_token: None, + roles: [], + servers: [] + } user = model_to_dict(Users.get(Users.user_id == user_id)) if user: @@ -331,13 +404,13 @@ class db_shortcuts: 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_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) + #for s in servers_query: + # servers.add(s.server_id.server_id) user['roles'] = roles - user['servers'] = servers + #user['servers'] = servers logger.debug("user: ({}) {}".format(user_id, user)) return user else: @@ -377,10 +450,10 @@ class db_shortcuts: # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point User_Roles.delete().where(User_Roles.user_id == user_id).where(User_Roles.role_id.in_(removed_roles)).execute() - for server in added_servers: - User_Servers.get_or_create(user_id=user_id, server_id=server) - # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point - User_Servers.delete().where(User_Servers.user_id == user_id).where(User_Servers.server_id.in_(removed_servers)).execute() + #for server in added_servers: + # User_Servers.get_or_create(user_id=user_id, server_id=server) + # # TODO: This is horribly inefficient and we should be using bulk queries but im going for functionality at this point + #User_Servers.delete().where(User_Servers.user_id == user_id).where(User_Servers.server_id.in_(removed_servers)).execute() if up_data: Users.update(up_data).where(Users.user_id == user_id).execute() @@ -555,8 +628,111 @@ class db_shortcuts: Audit_Log.source_ip: source_ip }).execute() + @staticmethod + def create_scheduled_task(server_id, action, interval, interval_type, start_time, command, comment=None, enabled=True): + sch_id = Schedules.insert({ + Schedules.server_id: server_id, + Schedules.action: action, + Schedules.enabled: enabled, + Schedules.interval: interval, + Schedules.interval_type: interval_type, + Schedules.start_time: start_time, + Schedules.command: command, + Schedules.comment: comment + }).execute() + return sch_id + @staticmethod + def delete_scheduled_task(schedule_id): + sch = Schedules.get(Schedules.schedule_id == schedule_id) + return Schedules.delete_instance(sch) + @staticmethod + def update_scheduled_task(schedule_id, updates): + Schedules.update(updates).where(Schedules.schedule_id == schedule_id).execute() + + @staticmethod + def get_scheduled_task(schedule_id): + return model_to_dict(Schedules.get(Schedules.schedule_id == schedule_id)).execute() + + @staticmethod + def get_schedules_by_server(server_id): + return Schedules.select().where(Schedules.server_id == server_id).execute() + + @staticmethod + def get_schedules_all(): + return Schedules.select().execute() + + @staticmethod + def get_schedules_enabled(): + return Schedules.select().where(Schedules.enabled == True).execute() + + @staticmethod + def get_backup_config(server_id): + try: + row = Backups.select().where(Backups.server_id == server_id).join(Schedules).join(Servers)[0] + conf = { + "backup_path": row.server_id.backup_path, + "directories": row.directories, + "max_backups": row.max_backups, + "auto_enabled": row.schedule_id.enabled, + "server_id": row.server_id.server_id + } + except IndexError: + conf = { + "backup_path": None, + "directories": None, + "max_backups": 0, + "auto_enabled": True, + "server_id": server_id + } + return conf + + @staticmethod + def set_backup_config(server_id: int, backup_path: str = None, max_backups: int = None, auto_enabled: bool = True): + logger.debug("Updating server {} backup config with {}".format(server_id, locals())) + try: + row = Backups.select().where(Backups.server_id == server_id).join(Schedules).join(Servers)[0] + new_row = False + conf = {} + schd = {} + except IndexError: + conf = { + "directories": None, + "max_backups": 0, + "server_id": server_id + } + schd = { + "enabled": True, + "action": "backup_server", + "interval_type": "days", + "interval": 1, + "start_time": "00:00", + "server_id": server_id, + "comment": "Default backup job" + } + new_row = True + if max_backups is not None: + conf['max_backups'] = max_backups + schd['enabled'] = bool(auto_enabled) + if not new_row: + with database.atomic(): + if backup_path is not None: + u1 = Servers.update(backup_path=backup_path).where(Servers.server_id == server_id).execute() + else: + u1 = 0 + u2 = Backups.update(conf).where(Backups.server_id == server_id).execute() + u3 = Schedules.update(schd).where(Schedules.schedule_id == row.schedule_id).execute() + logger.debug("Updating existing backup record. {}+{}+{} rows affected".format(u1, u2, u3)) + else: + with database.atomic(): + conf["server_id"] = server_id + if backup_path is not None: + u = Servers.update(backup_path=backup_path).where(Servers.server_id == server_id) + s = Schedules.create(**schd) + conf['schedule_id'] = s.schedule_id + b = Backups.create(**conf) + logger.debug("Creating new backup record.") installer = db_builder() -db_helper = db_shortcuts() +db_helper = db_shortcuts() \ No newline at end of file diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index 6f5c27ec..6d592076 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -9,6 +9,7 @@ import datetime import threading import schedule import logging.config +import zipfile from app.classes.shared.helpers import helper @@ -29,7 +30,7 @@ except ModuleNotFoundError as e: class Server: - def __init__(self): + def __init__(self, stats): # holders for our process self.process = None self.line = False @@ -45,6 +46,7 @@ class Server: self.is_crashed = False self.restart_count = 0 self.crash_watcher_schedule = None + self.stats = stats def reload_server_settings(self): server_data = db_helper.get_server_data_by_id(self.server_id) @@ -108,7 +110,6 @@ class Server: helper.do_exit() def start_server(self): - from app.classes.minecraft.stats import stats # fail safe in case we try to start something already running if self.check_running(): @@ -160,7 +161,7 @@ class Server: logger.info("Server {} running with PID {}".format(self.name, self.PID)) console.info("Server {} running with PID {}".format(self.name, self.PID)) self.is_crashed = False - stats.record_stats() + self.stats.record_stats() else: logger.warning("Server PID {} died right after starting - is this a server config issue?".format(self.PID)) console.warning("Server PID {} died right after starting - is this a server config issue?".format(self.PID)) @@ -178,7 +179,6 @@ class Server: self.server_thread.join() def stop_server(self): - from app.classes.minecraft.stats import stats if self.settings['stop_command']: self.send_command(self.settings['stop_command']) @@ -212,7 +212,7 @@ class Server: # massive resetting of variables self.cleanup_server_object() - stats.record_stats() + self.stats.record_stats() def restart_threaded_server(self): @@ -337,3 +337,26 @@ class Server: logger.info("Removing old crash detection watcher thread") console.info("Removing old crash detection watcher thread") schedule.clear(self.name) + + def backup_server(self): + logger.info("Starting server {} (ID {}) backup".format(self.name, self.server_id)) + conf = db_helper.get_backup_config(self.server_id) + try: + backup_filename = "{}/{}.zip".format(conf['backup_path'], datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')) + logger.info("Creating backup of server '{}' (ID#{}) at '{}'".format(self.settings['server_name'], self.server_id, backup_filename)) + helper.zip_directory(backup_filename, self.server_path) + backup_list = self.list_backups() + if len(self.list_backups()) > conf["max_backups"]: + oldfile = backup_list[0] + logger.info("Removing old backup '{}'".format(oldfile)) + os.remove(oldfile) + except: + logger.exception("Failed to create backup of server {} (ID {})".format(self.name, self.server_id)) + + def list_backups(self): + conf = db_helper.get_backup_config(self.server_id) + if helper.check_path_exists(self.settings['backup_path']): + files = helper.get_human_readable_files_sizes(helper.list_dir_by_date(self.settings['backup_path'])) + return [{"path": os.path.relpath(f['path'], start=conf['backup_path']), "size": f["size"]} for f in files] + else: + return [] diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index f1a060c3..e8274b67 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -8,11 +8,9 @@ import asyncio from app.classes.shared.helpers import helper from app.classes.shared.console import console -from app.classes.web.tornado import webserver +from app.classes.web.tornado import Webserver from app.classes.web.websocket_helper import websocket_helper -from app.classes.minecraft.stats import stats -from app.classes.shared.controller import controller from app.classes.minecraft.serverjars import server_jar_obj from app.classes.shared.models import db_helper @@ -26,11 +24,26 @@ except ModuleNotFoundError as e: console.critical("Import Error: Unable to load {} module".format(e, e.name)) sys.exit(1) +scheduler_intervals = { 'seconds', + 'minutes', + 'hours', + 'days', + 'weeks', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday' + } class TasksManager: - def __init__(self): - self.tornado = webserver() + def __init__(self, controller): + self.controller = controller + self.tornado = Webserver(controller, self) + self.webserver_thread = threading.Thread(target=self.tornado.run_tornado, daemon=True, name='tornado_thread') self.main_kill_switch_thread = threading.Thread(target=self.main_kill_switch, daemon=True, name="main_loop") @@ -39,13 +52,13 @@ class TasksManager: self.schedule_thread = threading.Thread(target=self.scheduler_thread, daemon=True, name="scheduler") self.log_watcher_thread = threading.Thread(target=self.log_watcher, daemon=True, name="log_watcher") - self.log_watcher_thread.start() self.command_thread = threading.Thread(target=self.command_watcher, daemon=True, name="command_watcher") - self.command_thread.start() self.realtime_thread = threading.Thread(target=self.realtime, daemon=True, name="realtime") - self.realtime_thread.start() + + self.reload_schedule_from_db() + def get_main_thread_run_status(self): return self.main_thread_exiting @@ -60,14 +73,29 @@ class TasksManager: self._main_graceful_exit() time.sleep(5) - @staticmethod - def command_watcher(): + def reload_schedule_from_db(self): + jobs = db_helper.get_schedules_enabled() + schedule.clear(tag='backup') + schedule.clear(tag='db') + for j in jobs: + if j.interval_type in scheduler_intervals: + logger.info("Loading schedule ID#{i}: '{a}' every {n} {t} at {s}".format( + i=j.schedule_id, a=j.action, n=j.interval, t=j.interval_type, s=j.start_time)) + try: + getattr(schedule.every(j.interval), j.interval_type).at(j.start_time).do( + db_helper.send_command, 0, j.server_id, "127.27.23.89", j.action) + except schedule.ScheduleValueError as e: + logger.critical("Scheduler value error occurred: {} on ID#{}".format(e, j.schedule_id)) + else: + logger.critical("Unknown schedule job type '{}' at id {}, skipping".format(j.interval_type, j.schedule_id)) + + def command_watcher(self): while True: # select any commands waiting to be processed commands = db_helper.get_unactioned_commands() for c in commands: - svr = controller.get_server_obj(c['server_id']['server_id']) + svr = self.controller.get_server_obj(c['server_id']['server_id']) command = c.get('command', None) if command == 'start_server': @@ -79,6 +107,9 @@ class TasksManager: elif command == "restart_server": svr.restart_threaded_server() + elif command == "backup_server": + svr.backup_server() + db_helper.mark_command_complete(c.get('command_id', None)) time.sleep(1) @@ -88,7 +119,7 @@ class TasksManager: os.remove(helper.session_file) os.remove(os.path.join(helper.root_dir, 'exit.txt')) os.remove(os.path.join(helper.root_dir, '.header')) - controller.stop_all_servers() + self.controller.stop_all_servers() except: logger.info("Caught error during shutdown", exc_info=True) @@ -113,6 +144,15 @@ class TasksManager: logger.info("Launching Scheduler Thread...") console.info("Launching Scheduler Thread...") self.schedule_thread.start() + logger.info("Launching command thread...") + console.info("Launching command thread...") + self.command_thread.start() + logger.info("Launching log watcher...") + console.info("Launching log watcher...") + self.log_watcher_thread.start() + logger.info("Launching realtime thread...") + console.info("Launching realtime thread...") + self.realtime_thread.start() @staticmethod def scheduler_thread(): @@ -120,17 +160,16 @@ class TasksManager: schedule.run_pending() time.sleep(1) - @staticmethod - def start_stats_recording(): + def start_stats_recording(self): stats_update_frequency = helper.get_setting('stats_update_frequency') logger.info("Stats collection frequency set to {stats} seconds".format(stats=stats_update_frequency)) console.info("Stats collection frequency set to {stats} seconds".format(stats=stats_update_frequency)) # one for now, - stats.record_stats() + self.controller.stats.record_stats() # one for later - schedule.every(stats_update_frequency).seconds.do(stats.record_stats) + schedule.every(stats_update_frequency).seconds.do(self.controller.stats.record_stats).tag('stats-recording') @staticmethod def serverjar_cache_refresher(): @@ -138,7 +177,7 @@ class TasksManager: server_jar_obj.refresh_cache() logger.info("Scheduling Serverjars.com cache refresh service every 12 hours") - schedule.every(12).hours.do(server_jar_obj.refresh_cache) + schedule.every(12).hours.do(server_jar_obj.refresh_cache).tag('serverjars') @staticmethod def realtime(): @@ -174,9 +213,4 @@ class TasksManager: 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)) - - - - -tasks_manager = TasksManager() + schedule.every(6).hours.do(lambda: helper.check_for_old_logs(db_helper)).tag('log-mgmt') \ No newline at end of file diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index af09a8b8..59712bac 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -9,7 +9,6 @@ import shutil from app.classes.shared.console import console from app.classes.shared.models import Users, installer from app.classes.web.base_handler import BaseHandler -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper from app.classes.shared.helpers import helper @@ -56,8 +55,8 @@ class AjaxHandler(BaseHandler): logger.warning("Server Data not found in server_log ajax call") self.redirect("/panel/error?error=Server ID Not Found") - if server_data['log_path']: - logger.warning("Server ID not found in server_log ajax call ({})".format(server_id)) + 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') @@ -149,7 +148,7 @@ class AjaxHandler(BaseHandler): logger.warning("Server ID not found in send_command ajax call") console.warning("Server ID not found in send_command ajax call") - srv_obj = controller.get_server_obj(server_id) + srv_obj = self.controller.get_server_obj(server_id) if command: if srv_obj.check_running(): @@ -219,7 +218,6 @@ class AjaxHandler(BaseHandler): if page == "del_file": file_path = self.get_body_argument('file_path', default=None, strip=True) server_id = self.get_argument('id', None) - print(server_id) if server_id is None: logger.warning("Server ID not found in del_file ajax call") @@ -234,7 +232,9 @@ class AjaxHandler(BaseHandler): console.warning("Server ID not found in del_file ajax call ({})".format(server_id)) return False - if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], file_path) \ + server_info = db_helper.get_server_data_by_id(server_id) + if not helper.in_path(server_info['path'], file_path) \ + or not helper.in_path(server_info['backup_path'], file_path) \ 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)) @@ -261,7 +261,9 @@ class AjaxHandler(BaseHandler): console.warning("Server ID not found in del_file ajax call ({})".format(server_id)) return False - if not helper.in_path(db_helper.get_server_data_by_id(server_id)['path'], dir_path) \ + server_info = db_helper.get_server_data_by_id(server_id) + if not helper.in_path(server_info['path'], dir_path) \ + or not helper.in_path(server_info['backup_path'], dir_path) \ 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)) diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py index cb87a8ea..f0e57e07 100644 --- a/app/classes/web/api_handler.py +++ b/app/classes/web/api_handler.py @@ -5,13 +5,13 @@ import tornado.web import tornado.escape import logging +from app.classes.web.base_handler import BaseHandler from app.classes.shared.models import Users -from app.classes.minecraft.stats import stats log = logging.getLogger(__name__) -class BaseHandler(tornado.web.RequestHandler): +class ApiHandler(BaseHandler): def return_response(self, data: dict): # Define a standardized response @@ -25,6 +25,7 @@ class BaseHandler(tornado.web.RequestHandler): def authenticate_user(self): try: log.debug("Searching for specified token") + # TODO: YEET THIS user_data = Users.get(api_token=self.get_argument('token')) log.debug("Checking results") if user_data: @@ -40,19 +41,19 @@ class BaseHandler(tornado.web.RequestHandler): pass -class ServersStats(BaseHandler): +class ServersStats(ApiHandler): def get(self): """Get details about all servers""" self.authenticate_user() # Get server stats - self.finish(self.write({"servers": stats.get_servers_stats()})) + self.finish(self.write({"servers": self.controller.stats.get_servers_stats()})) -class NodeStats(BaseHandler): +class NodeStats(ApiHandler): def get(self): """Get stats for particular node""" self.authenticate_user() # Get node stats - node_stats = stats.get_node_stats() + node_stats = self.controller.stats.get_node_stats() node_stats.pop("servers") self.finish(self.write(node_stats)) diff --git a/app/classes/web/base_handler.py b/app/classes/web/base_handler.py index c8956894..7fe05c1b 100644 --- a/app/classes/web/base_handler.py +++ b/app/classes/web/base_handler.py @@ -1,11 +1,21 @@ import logging import tornado.web +import bleach +from typing import ( + Union, + List, + Optional +) logger = logging.getLogger(__name__) class BaseHandler(tornado.web.RequestHandler): + def initialize(self, controller=None, tasks_manager=None): + self.controller = controller + self.tasks_manager = tasks_manager + def get_remote_ip(self): remote_ip = self.request.headers.get("X-Real-IP") or \ self.request.headers.get("X-Forwarded-For") or \ @@ -14,3 +24,28 @@ class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): return self.get_secure_cookie("user", max_age_days=1) + + def autobleach(self, text): + if type(text) is bool: + return text + else: + return text + + def get_argument( + self, + name: str, + default: Union[None, str, tornado.web._ArgDefaultMarker] = tornado.web._ARG_DEFAULT, + strip: bool = True, + ) -> Optional[str]: + arg = self._get_argument(name, default, self.request.arguments, strip) + logger.debug("Bleaching {}: {}".format(name, arg)) + return bleach.clean(arg) + + def get_arguments(self, name: str, strip: bool = True) -> List[str]: + assert isinstance(strip, bool) + args = self._get_arguments(name, self.request.arguments, strip) + args_ret = [] + for arg in args: + logger.debug("Bleaching {}: {}".format(name, arg)) + args_ret += bleach.clean(arg) + return args_ret diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 66a266fd..2ee22345 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -5,14 +5,13 @@ import tornado.escape import bleach import time import datetime +import os from app.classes.shared.console import console from app.classes.shared.models import Users, installer from app.classes.web.base_handler import BaseHandler -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper, Servers from app.classes.shared.helpers import helper -from app.classes.minecraft.stats import stats logger = logging.getLogger(__name__) @@ -29,7 +28,7 @@ class PanelHandler(BaseHandler): now = time.time() formatted_time = str(datetime.datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')) - defined_servers = controller.list_defined_servers() + defined_servers = self.controller.list_defined_servers() page_data = { # todo: make this actually pull and compare version data @@ -38,8 +37,8 @@ class PanelHandler(BaseHandler): 'user_data': user_data, 'server_stats': { 'total': len(defined_servers), - 'running': len(controller.list_running_servers()), - 'stopped': (len(controller.list_defined_servers()) - len(controller.list_running_servers())) + 'running': len(self.controller.list_running_servers()), + 'stopped': (len(self.controller.list_defined_servers()) - len(self.controller.list_running_servers())) }, 'menu_servers': defined_servers, 'hosts_data': db_helper.get_latest_hosts_stats(), @@ -52,7 +51,7 @@ class PanelHandler(BaseHandler): if page_data['server_stats']['total'] == 0 and page != "error": self.set_status(301) self.redirect("/server/step1") - return False + return if page == 'unauthorized': template = "panel/denied.html" @@ -81,13 +80,12 @@ class PanelHandler(BaseHandler): server_id, self.get_remote_ip()) - controller.remove_server(server_id) + self.controller.remove_server(server_id) self.redirect("/panel/dashboard") return elif page == 'dashboard': page_data['servers'] = db_helper.get_all_servers_stats() - for s in page_data['servers']: try: data = json.loads(s['int_ping_results']) @@ -103,26 +101,30 @@ class PanelHandler(BaseHandler): if server_id is None: self.redirect("/panel/error?error=Invalid Server ID") - return False + return else: server_id = bleach.clean(server_id) # does this server id exist? if not db_helper.server_id_exists(server_id): self.redirect("/panel/error?error=Invalid Server ID") - return False + return - valid_subpages = ['term', 'logs', 'config', 'files', 'admin_controls'] + valid_subpages = ['term', 'logs', 'backup', 'config', 'files', 'admin_controls'] if subpage not in valid_subpages: logger.debug('not a valid subpage') subpage = 'term' logger.debug('Subpage: "{}"'.format(subpage)) + server = self.controller.get_server_obj(server_id) # server_data isn't needed since the server_stats also pulls server data - # page_data['server_data'] = db_helper.get_server_data_by_id(server_id) + page_data['server_data'] = db_helper.get_server_data_by_id(server_id) page_data['server_stats'] = db_helper.get_server_stats_by_id(server_id) - page_data['get_players'] = lambda: stats.get_server_players(server_id) + page_data['get_players'] = lambda: self.controller.stats.get_server_players(server_id) + if subpage == "backup": + page_data['backup_config'] = db_helper.get_backup_config(server_id) + page_data['backup_list'] = server.list_backups() def get_banned_players_html(): banned_players = helper.get_banned_players(server_id, db_helper) @@ -149,6 +151,79 @@ class PanelHandler(BaseHandler): # template = "panel/server_details.html" template = "panel/server_{subpage}.html".format(subpage=subpage) + elif page == 'download_backup': + server_id = self.get_argument('id', None) + file = self.get_argument('file', "") + + if server_id is None: + self.redirect("/panel/error?error=Invalid Server ID") + return + else: + server_id = bleach.clean(server_id) + + # does this server id exist? + if not db_helper.server_id_exists(server_id): + self.redirect("/panel/error?error=Invalid Server ID") + return + + exec_user = db_helper.get_user(user_data['user_id']) + + if not exec_user['superuser']: + self.redirect("/panel/error?error=Unauthorized access: not superuser") + 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)) + if not helper.in_path(server_info["backup_path"], backup_file) \ + or not os.path.isfile(backup_file): + self.redirect("/panel/error?error=Invalid path detected") + return + + self.set_header('Content-Type', 'application/octet-stream') + self.set_header('Content-Disposition', 'attachment; filename=' + file) + chunk_size = 1024 * 1024 * 4 # 4 MiB + + with open(backup_file, 'rb') as f: + while True: + chunk = f.read(chunk_size) + if not chunk: + break + try: + self.write(chunk) # write the chunk to response + self.flush() # send the chunk to client + except iostream.StreamClosedError: + # this means the client has closed the connection + # so break the loop + break + finally: + # deleting the chunk is very important because + # if many clients are downloading files at the + # same time, the chunks in memory will keep + # increasing and will eat up the RAM + del chunk + self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) + + elif page == 'backup_now': + server_id = self.get_argument('id', None) + + if server_id is None: + self.redirect("/panel/error?error=Invalid Server ID") + return + else: + # does this server id exist? + if not db_helper.server_id_exists(server_id): + self.redirect("/panel/error?error=Invalid Server ID") + return + + exec_user = db_helper.get_user(user_data['user_id']) + + if not exec_user['superuser']: + self.redirect("/panel/error?error=Unauthorized access: not superuser") + return + + server = self.controller.get_server_obj(server_id).backup_server() + self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) + elif page == 'panel_config': page_data['users'] = db_helper.get_all_users() page_data['roles'] = db_helper.get_all_roles() @@ -177,10 +252,10 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return page_data['roles_all'] = db_helper.get_all_roles() - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() template = "panel/panel_edit_user.html" elif page == "edit_user": @@ -188,16 +263,16 @@ class PanelHandler(BaseHandler): user_id = self.get_argument('id', None) page_data['user'] = db_helper.get_user(user_id) page_data['roles_all'] = db_helper.get_all_roles() - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif user_id is None: self.redirect("/panel/error?error=Invalid User ID") - return False + return if exec_user['user_id'] != page_data['user']['user_id']: page_data['user']['api_token'] = "********" @@ -211,19 +286,19 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif user_id is None: self.redirect("/panel/error?error=Invalid User ID") - return False + return else: # does this user id exist? target_user = db_helper.get_user(user_id) if not target_user: self.redirect("/panel/error?error=Invalid User ID") - return False + return elif target_user['superuser']: self.redirect("/panel/error?error=Cannot remove a superuser") - return False + return db_helper.remove_user(user_id) @@ -246,25 +321,25 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() template = "panel/panel_edit_role.html" elif page == "edit_role": page_data['new_role'] = False role_id = self.get_argument('id', None) page_data['role'] = db_helper.get_role(role_id) - page_data['servers_all'] = controller.list_defined_servers() + page_data['servers_all'] = self.controller.list_defined_servers() exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_id is None: self.redirect("/panel/error?error=Invalid Role ID") - return False + return template = "panel/panel_edit_role.html" @@ -276,16 +351,16 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_id is None: self.redirect("/panel/error?error=Invalid Role ID") - return False + return else: # does this user id exist? target_role = db_helper.get_user(role_id) if not target_role: self.redirect("/panel/error?error=Invalid Role ID") - return False + return db_helper.remove_role(role_id) @@ -331,17 +406,15 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif server_id is None: self.redirect("/panel/error?error=Invalid Server ID") - return False + return else: - server_id = bleach.clean(server_id) - # does this server id exist? if not db_helper.server_id_exists(server_id): self.redirect("/panel/error?error=Invalid Server ID") - return False + return Servers.update({ Servers.server_name: server_name, @@ -358,7 +431,7 @@ class PanelHandler(BaseHandler): Servers.logs_delete_after: logs_delete_after, }).where(Servers.server_id == server_id).execute() - controller.refresh_server_settings(server_id) + self.controller.refresh_server_settings(server_id) db_helper.add_to_audit_log(user_data['user_id'], "Edited server {} named {}".format(server_id, server_name), @@ -367,6 +440,41 @@ class PanelHandler(BaseHandler): self.redirect("/panel/server_detail?id={}&subpage=config".format(server_id)) + if page == "server_backup": + logger.debug(self.request.arguments) + server_id = self.get_argument('id', None) + backup_path = bleach.clean(self.get_argument('backup_path', None)) + max_backups = bleach.clean(self.get_argument('max_backups', None)) + enabled = int(float(bleach.clean(self.get_argument('auto_enabled'), '0'))) + + user_data = json.loads(self.get_secure_cookie("user_data")) + exec_user = db_helper.get_user(user_data['user_id']) + + if not exec_user['superuser']: + self.redirect("/panel/error?error=Unauthorized access: not superuser") + return + elif server_id is None: + self.redirect("/panel/error?error=Invalid Server ID") + return + else: + # does this server id exist? + if not db_helper.server_id_exists(server_id): + self.redirect("/panel/error?error=Invalid Server ID") + return + + if backup_path is not None: + Servers.update({ + Servers.backup_path: backup_path + }).where(Servers.server_id == server_id).execute() + db_helper.set_backup_config(server_id, max_backups=max_backups) + + db_helper.add_to_audit_log(user_data['user_id'], + "Edited server {}: updated backups".format(server_id), + server_id, + self.get_remote_ip()) + self.tasks_manager.reload_schedule_from_db() + self.redirect("/panel/server_detail?id={}&subpage=backup".format(server_id)) + elif page == "edit_user": user_id = bleach.clean(self.get_argument('id', None)) username = bleach.clean(self.get_argument('username', None)) @@ -380,22 +488,22 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif username is None or username == "": self.redirect("/panel/error?error=Invalid username") - return False + return elif user_id is None: self.redirect("/panel/error?error=Invalid User ID") - return False + return else: # does this user id exist? if not db_helper.user_id_exists(user_id): self.redirect("/panel/error?error=Invalid User ID") - return False + return if password0 != password1: self.redirect("/panel/error?error=Passwords must match") - return False + return roles = set() for role in db_helper.get_all_roles(): @@ -408,7 +516,7 @@ class PanelHandler(BaseHandler): roles.add(role.role_id) servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -444,19 +552,19 @@ class PanelHandler(BaseHandler): exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif username is None or username == "": self.redirect("/panel/error?error=Invalid username") - return False + return else: # does this user id exist? if db_helper.get_userid_by_name(username) is not None: self.redirect("/panel/error?error=User exists") - return False + return if password0 != password1: self.redirect("/panel/error?error=Passwords must match") - return False + return roles = set() for role in db_helper.get_all_roles(): @@ -469,7 +577,7 @@ class PanelHandler(BaseHandler): roles.add(role['role_id']) servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -500,21 +608,21 @@ class PanelHandler(BaseHandler): if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_name is None or role_name == "": self.redirect("/panel/error?error=Invalid username") - return False + return elif role_id is None: self.redirect("/panel/error?error=Invalid Role ID") - return False + return else: # does this user id exist? if not db_helper.role_id_exists(role_id): self.redirect("/panel/error?error=Invalid Role ID") - return False + return servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -543,18 +651,18 @@ class PanelHandler(BaseHandler): exec_user = db_helper.get_user(user_data['user_id']) if not exec_user['superuser']: self.redirect("/panel/error?error=Unauthorized access: not superuser") - return False + return elif role_name is None or role_name == "": self.redirect("/panel/error?error=Invalid role name") - return False + return else: # does this user id exist? if db_helper.get_roleid_by_name(role_name) is not None: self.redirect("/panel/error?error=Role exists") - return False + return servers = set() - for server in controller.list_defined_servers(): + for server in self.controller.list_defined_servers(): argument = int(float( bleach.clean( self.get_argument('server_{}_access'.format(server['server_id']), '0') @@ -575,3 +683,7 @@ class PanelHandler(BaseHandler): server_id=0, source_ip=self.get_remote_ip()) self.redirect("/panel/panel_config") + + else: + self.set_status(404) + self.render("public/404.html") \ No newline at end of file diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py index 381c0827..73bf17f4 100644 --- a/app/classes/web/server_handler.py +++ b/app/classes/web/server_handler.py @@ -6,10 +6,8 @@ import shutil from app.classes.shared.console import console from app.classes.web.base_handler import BaseHandler -from app.classes.shared.controller import controller from app.classes.shared.models import db_helper, Servers from app.classes.minecraft.serverjars import server_jar_obj -from app.classes.minecraft.stats import stats from app.classes.shared.helpers import helper @@ -35,15 +33,15 @@ class ServerHandler(BaseHandler): template = "public/404.html" - defined_servers = controller.list_defined_servers() + defined_servers = self.controller.list_defined_servers() page_data = { 'version_data': helper.get_version_string(), 'user_data': user_data, 'server_stats': { - 'total': len(controller.list_defined_servers()), - 'running': len(controller.list_running_servers()), - 'stopped': (len(controller.list_defined_servers()) - len(controller.list_running_servers())) + 'total': len(self.controller.list_defined_servers()), + 'running': len(self.controller.list_running_servers()), + 'stopped': (len(self.controller.list_defined_servers()) - len(self.controller.list_running_servers())) }, 'hosts_data': db_helper.get_latest_hosts_stats(), 'menu_servers': defined_servers, @@ -130,7 +128,7 @@ class ServerHandler(BaseHandler): Servers.stop_command: stop_command }).execute() - controller.init_all_servers() + self.controller.init_all_servers() console.debug('initted all servers') return @@ -150,26 +148,26 @@ class ServerHandler(BaseHandler): server_parts = server.split("|") if import_type == 'import_jar': - good_path = controller.verify_jar_server(import_server_path, import_server_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 - new_server_id = controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) + new_server_id = self.controller.import_jar_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) elif import_type == 'import_zip': - good_path = controller.verify_zip_server(import_server_path) + 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 - new_server_id = controller.import_zip_server(server_name, import_server_path,import_server_jar, min_mem, max_mem, port) + 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 else: # todo: add server type check here and call the correct server add functions if not a jar - new_server_id = controller.create_jar_server(server_parts[0], server_parts[1], server_name, min_mem, max_mem, port) + 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: db_helper.add_to_audit_log(user_data['user_id'], @@ -180,7 +178,7 @@ class ServerHandler(BaseHandler): logger.error("Unable to create server") console.error("Unable to create server") - stats.record_stats() + self.controller.stats.record_stats() self.redirect("/panel/dashboard") self.render( diff --git a/app/classes/web/tornado.py b/app/classes/web/tornado.py index 866d0a10..096dd746 100644 --- a/app/classes/web/tornado.py +++ b/app/classes/web/tornado.py @@ -33,12 +33,14 @@ except ModuleNotFoundError as e: -class webserver: +class Webserver: - def __init__(self): + def __init__(self, controller, tasks_manager): self.ioloop = None self.HTTP_Server = None self.HTTPS_Server = None + self.controller = controller + self.tasks_manager = tasks_manager self._asyncio_patch() @@ -118,15 +120,16 @@ class webserver: tornado.locale.set_default_locale(lang) + handler_args = {"controller": self.controller, "tasks_manager": self.tasks_manager} handlers = [ - (r'/', DefaultHandler), - (r'/public/(.*)', PublicHandler), - (r'/panel/(.*)', PanelHandler), - (r'/server/(.*)', ServerHandler), - (r'/ajax/(.*)', AjaxHandler), - (r'/api/stats/servers', ServersStats), - (r'/api/stats/node', NodeStats), - (r'/ws', SocketHandler), + (r'/', DefaultHandler, handler_args), + (r'/public/(.*)', PublicHandler, handler_args), + (r'/panel/(.*)', PanelHandler, handler_args), + (r'/server/(.*)', ServerHandler, handler_args), + (r'/ajax/(.*)', AjaxHandler, handler_args), + (r'/api/stats/servers', ServersStats, handler_args), + (r'/api/stats/node', NodeStats, handler_args), + (r'/ws', SocketHandler, handler_args), ] app = tornado.web.Application( diff --git a/app/classes/web/websocket_handler.py b/app/classes/web/websocket_handler.py index 5abd1684..a33707f6 100644 --- a/app/classes/web/websocket_handler.py +++ b/app/classes/web/websocket_handler.py @@ -8,6 +8,10 @@ from app.classes.web.websocket_helper import websocket_helper class SocketHandler(tornado.websocket.WebSocketHandler): + def initialize(self, controller=None, tasks_manager=None): + self.controller = controller + self.tasks_manager = tasks_manager + def get_remote_ip(self): remote_ip = self.request.headers.get("X-Real-IP") or \ self.request.headers.get("X-Forwarded-For") or \ diff --git a/app/frontend/templates/panel/credits.html b/app/frontend/templates/panel/credits.html index 484193ca..2efee656 100644 --- a/app/frontend/templates/panel/credits.html +++ b/app/frontend/templates/panel/credits.html @@ -50,8 +50,10 @@
+ {% if person['loc'] %}

{{ person['loc'] }}

+ {% end %}
@@ -113,8 +115,10 @@
+ {% if person['loc'] %}

{{ person['loc'] }}

+ {% end %}
@@ -177,8 +181,10 @@
+ {% if person['loc'] %}

{{ person['loc'] }}

+ {% end %}
diff --git a/app/frontend/templates/panel/dashboard.html b/app/frontend/templates/panel/dashboard.html index 4085c250..1ce71648 100644 --- a/app/frontend/templates/panel/dashboard.html +++ b/app/frontend/templates/panel/dashboard.html @@ -125,7 +125,7 @@ - {% if server['stats'][0]['running'] %} + {% if server['stats']['running'] %}     {% else %} @@ -136,59 +136,59 @@ -
+
+ " role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
- {{server['stats'][0]['cpu']}}% + {{server['stats']['cpu']}}% -
+
+ " role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
- {{server['stats'][0]['mem_percent']}}% - + {{server['stats']['mem_percent']}}% - - {% if server['stats'][0]['mem'] == 0 %} + {% if server['stats']['mem'] == 0 %} 0 MB {% else %} - {{server['stats'][0]['mem']}} + {{server['stats']['mem']}} {% end %} - {{ server['stats'][0]['world_name'] }} : {{ server['stats'][0]['world_size'] }} + {{ server['stats']['world_name'] }} : {{ server['stats']['world_size'] }} - {% if server['stats'][0]['int_ping_results'] %} - {{ server['stats'][0]['online'] }} / {{ server['stats'][0]['max'] }} Max
+ {% if server['stats']['int_ping_results'] %} + {{ server['stats']['online'] }} / {{ server['stats']['max'] }} Max
- {% if server['stats'][0]['desc'] != 'False' %} - {{ server['stats'][0]['desc'] }}
+ {% if server['stats']['desc'] != 'False' %} + {{ server['stats']['desc'] }}
{% end %} - {% if server['stats'][0]['version'] != 'False' %} - {{ server['stats'][0]['version'] }} + {% if server['stats']['version'] != 'False' %} + {{ server['stats']['version'] }} {% end %} {% end %} - {% if server['stats'][0]['running'] %} + {% if server['stats']['running'] %} Online {% else %} Offline diff --git a/app/frontend/templates/panel/parts/details_stats.html b/app/frontend/templates/panel/parts/details_stats.html index 4d44c117..204ad6c1 100644 --- a/app/frontend/templates/panel/parts/details_stats.html +++ b/app/frontend/templates/panel/parts/details_stats.html @@ -4,9 +4,9 @@
- {% if data['server_stats'][0]['running'] %} + {% if data['server_stats']['running'] %} Server Status: Online
- Server Started: {{ data['server_stats'][0]['started'] }} (Server Time)
+ Server Started: {{ data['server_stats']['started'] }} (Server Time)
Server Uptime: Error Calculating {% else %} Server Status: Offline
@@ -16,19 +16,19 @@
- CPU: {{ data['server_stats'][0]['cpu'] }}%
- Mem: {{ data['server_stats'][0]['mem'] }}
- {% if data['server_stats'][0]['int_ping_results'] %} - Players: {{ data['server_stats'][0]['online'] }} / {{ data['server_stats'][0]['max'] }}
+ CPU: {{ data['server_stats']['cpu'] }}%
+ Mem: {{ data['server_stats']['mem'] }}
+ {% if data['server_stats']['int_ping_results'] %} + Players: {{ data['server_stats']['online'] }} / {{ data['server_stats']['max'] }}
{% else %} Players: 0/0
{% end %}
- {% if data['server_stats'][0]['version'] != 'False' %} - Server: {{ data['server_stats'][0]['version'] }}
- Desc: {{ data['server_stats'][0]['desc'] }}
+ {% if data['server_stats']['version'] != 'False' %} + Server: {{ data['server_stats']['version'] }}
+ Desc: {{ data['server_stats']['desc'] }}
{% else %} Server: Unable To Connect
Desc: Unable To Connect
@@ -86,9 +86,9 @@ let startedLocal; if (started != null) { - console.log('88', '{{ data['server_stats'][0]['started'] }}'); - {% if data['server_stats'][0]['started'] != 'False' %} - startedUTC = '{{ (datetime.datetime.strptime(data['server_stats'][0]['started'], '%Y-%m-%d %H:%M:%S') - datetime.timedelta(seconds=-time.timezone)).strftime('%Y-%m-%d %H:%M:%S') }}'; + 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 = moment.utc(startedUTC, 'YYYY-MM-DD HH:mm:ss'); @@ -105,7 +105,7 @@ } let nowServerTime = '{{ data['time'] }}'; - let startedServerTime = '{{ data['server_stats'][0]['started'] }}'; + let startedServerTime = '{{ data['server_stats']['started'] }}'; if (uptime != null && started != null) { diff --git a/app/frontend/templates/panel/server_admin_controls.html b/app/frontend/templates/panel/server_admin_controls.html index 1cc8348d..2e35fc2f 100644 --- a/app/frontend/templates/panel/server_admin_controls.html +++ b/app/frontend/templates/panel/server_admin_controls.html @@ -15,9 +15,9 @@
@@ -34,31 +34,31 @@