servii-backend/generic_executor.py
Charles Le Maux 20047990b5 [+] Fixed server creation vulnerability
When a user wanted to create a server, it was created on firebase before the preliminary checks were made, reordering the function has fixed that.
2024-09-16 01:52:16 +02:00

337 lines
15 KiB
Python

import os
from http import HTTPStatus
from typing import Union
from cloudflare.types.dns import SRVRecord
from firebase_admin.auth import UserRecord
import cloudflare_manager
import file_manager
import firebase_manager
from server_mc_manager import MinecraftServerManager
mc_manager: MinecraftServerManager = MinecraftServerManager()
def set_subdomain(user: UserRecord, subdomain: str) -> tuple[HTTPStatus, Union[str, None]]:
user_id: str = user.uid
store = firebase_manager.firestore_database
_users = store.collection("users")
query = _users.where(field_path="subdomain", op_string="==", value=subdomain)
for _ in query.stream():
return HTTPStatus.ALREADY_REPORTED, "Subdomain already associated."
try:
if firebase_manager.get_user_field(user_id, "subdomain"):
return HTTPStatus.OK, "You cannot change your subdomain."
exists: bool = firebase_manager.update_firestore(user_id, {'subdomain': subdomain})
if not exists:
account_create(user)
return set_subdomain(user, subdomain)
port = firebase_manager.get_server_port(user_id)
if port is None:
return HTTPStatus.NOT_FOUND, "Port not found on database"
try:
record: SRVRecord = cloudflare_manager.add_cloudflare_dns(subdomain, port)
record_id: str = record.id
if record_id == "":
return HTTPStatus.BAD_REQUEST, "Failed to add record to cloudflare"
firebase_manager.update_firestore(user_id, {'cloudflare_entry': str(record_id)})
except Exception as e:
firebase_manager.update_firestore(user_id, {'subdomain': None})
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Cloudflare configuration failed. {str(e)}"
return HTTPStatus.OK, "Successfully associated subdomain."
except Exception as e:
return HTTPStatus.FORBIDDEN, str(e)
def fetch_servers(user: UserRecord) -> tuple[HTTPStatus, Union[str, list]]:
user_id: str = user.uid
server_ref = firebase_manager.firestore_database.collection('users').document(user_id).collection('servers')
try:
servers = list(server_ref.stream())
servers_data: list[dict[str, any]] = []
for doc in servers:
server_info = doc.to_dict()
if server_info.get('running', False):
try:
connected_players = mc_manager.get_online_players(server_info.get('port', 25565))
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
connected_players = 0
server_info['onlinePlayers'] = connected_players
servers_data.append(server_info)
if not servers_data:
account_create(user)
return HTTPStatus.OK, servers_data
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return account_create(user)
def fetch_logs(user: UserRecord, name: str) -> tuple[HTTPStatus, Union[str, None]]:
user_id: str = user.uid
lines_to_read: int = 200
log_file = f"users/{user_id}/{name}/logs/latest.log"
try:
with open(log_file, "r") as f:
logs = f.readlines()[-lines_to_read:]
return HTTPStatus.OK, str(logs)
except FileNotFoundError:
return HTTPStatus.NOT_FOUND, "Log file not found."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
def fetch_history(user: UserRecord, name: str) -> tuple[HTTPStatus, Union[str, None]]:
user_id: str = user.uid
history = f"users/{user_id}/{name}/history.log"
try:
with open(history, "r") as f:
logs = f.readlines()
return HTTPStatus.OK, str(logs)
except FileNotFoundError:
return HTTPStatus.NOT_FOUND, "History file not found."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, "Unknown error."
def fetch_players_status(user: UserRecord, name: str) -> tuple[HTTPStatus, Union[dict[str, str], str]]:
user_id: str = user.uid
server_path = f"users/{user_id}/{name}"
players_status_files = {
"Whitelist": "whitelist.json",
"BannedPlayers": "banned-players.json",
"BannedIps": "banned-ips.json",
"Operators": "ops.json"
}
try:
file_contents = {}
for file_name, file_path in players_status_files.items():
full_file_path = f"{server_path}/{file_path}"
if os.path.exists(full_file_path):
with open(full_file_path, 'r') as file:
file_contents[file_name] = file.read()
else:
return HTTPStatus.NOT_FOUND, "Launch the server at least once."
return HTTPStatus.OK, file_contents
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, "Unknown error."
def fetch_dir_content(user: UserRecord, name: str) -> tuple[HTTPStatus, Union[str, list]]:
user_id: str = user.uid
server_path: str = f"users/{user_id}/{name}/"
dirs: dict[str, [str, str]] = {
'plugins': ['plugins', '.jar'],
'datapack' : ['world/datapacks', '.zip'],
}
files: list[dict[str, list[str]]] = []
try:
for key, (path, extension) in dirs.items():
final_path = f"{server_path}/{path}"
if os.path.exists(final_path):
parsed_content = file_manager.filter_directory_contents( extension, os.listdir(final_path))
files.append({key: parsed_content})
return HTTPStatus.OK, files
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Error fetching files for server {name}"
def account_create(user: UserRecord) -> tuple[HTTPStatus, Union[str, None]]:
if firebase_manager.user_field_exists(user.uid):
return HTTPStatus.FORBIDDEN, "User already exists."
try:
file_manager.create_folder("users/" + user.uid)
firebase_manager.set_firestore(user.uid, {'mail': user.email,
'name': user.display_name,
'photo_url': user.photo_url,
'subdomain': None,
'port': firebase_manager.fetch_port() + 1,
'cloudflare_entry': None,
})
return HTTPStatus.CREATED, "Successfully created account."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, "Error creating account."
def account_delete(user: UserRecord) -> tuple[HTTPStatus, Union[str, None]]:
user_id = user.uid
try:
dns_record_id = firebase_manager.get_user_field(user_id, "cloudflare_entry")
if dns_record_id is None:
return HTTPStatus.IM_A_TEAPOT, "Cloudflare 'dns_record_id' not found in Firestore."
cloudflare_manager.delete_cloudflare_dns(dns_record_id)
except Exception as e:
return HTTPStatus.NOT_MODIFIED, f"Account deletion failed. {str(e)}"
try:
firebase_manager.delete_user(user_id)
except Exception as e:
return HTTPStatus.EXPECTATION_FAILED, f"Database deletion failed | {e}"
try:
file_manager.delete_non_empty_folder("users/" + user_id)
return HTTPStatus.OK, f"Successfully deleted user '{user_id}'."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Error deleting user '{user_id}' on the server."
allowed_frameworks: list[str] = ["bukkit", "fabric", "forge", "spigot", "paper"]
def server_create(user: UserRecord, name: str, version: str, framework: str = "paper") -> (
tuple)[HTTPStatus, Union[str, None]]:
if framework not in allowed_frameworks:
return HTTPStatus.METHOD_NOT_ALLOWED, f"Framework {framework} not recognized."
user_id = user.uid
server_path: str = f"users/{user_id}/{name}"
server_template_path: str = f"servers/{framework}/{version}"
try:
port = firebase_manager.get_server_port(user_id)
if port is None:
return HTTPStatus.NOT_FOUND, "Port not found on database"
subdomain = firebase_manager.firestore_database.collection("users").document(user_id).get().get("subdomain")
if subdomain is None:
firebase_manager.delete_server(user_id, name)
return HTTPStatus.NOT_FOUND, f"You haven't associated a subdomain yet."
if firebase_manager.server_name_taken(user_id, name):
return HTTPStatus.CONFLICT, f"Server name '{name}' already in use."
file_manager.create_folder(server_path)
file_manager.copy_folder_contents(server_template_path, server_path)
file_manager.copy_folder_contents("servers/shared", server_path)
prop_path: str = server_path + "/server.properties"
file_manager.update_server_property(prop_path, "server-port", port)
file_manager.update_server_property(prop_path, "query.port", port)
file_manager.update_server_property(prop_path, "enable-query", "true")
file_manager.log_action(user_id, name, "ServerCreate")
firebase_manager.create_server(user_id, name, version, port, framework)
return HTTPStatus.CREATED, f"Successfully created server '{name}'."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Server creation failed | {e}"
def server_delete(name: str, user: UserRecord) -> tuple[HTTPStatus, Union[str, None]]:
user_id = user.uid
try:
port: int = firebase_manager.get_server_port(user_id)
mc_manager.stop_server_forcefully(port)
except Exception as e:
file_manager.log_error(type(e).__name__, str(e)+f" error when stopping server {name}")
server_path: str = f"users/{user_id}/{name}"
try:
firebase_manager.delete_server(user_id, name)
file_manager.delete_non_empty_folder(server_path)
return HTTPStatus.OK, f"Successfully deleted server '{name}'."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, None
def server_run(user: UserRecord, name: str) -> tuple[HTTPStatus, Union[str, None]]:
user_id = user.uid
mc_manager.set_cooldown(user_id=user_id)
try:
port: int = firebase_manager.get_server_port(user_id)
version: str = firebase_manager.get_server_field(user_id, name, "version")
framework: str = firebase_manager.get_server_field(user_id, name, "framework")
if version is None or framework is None:
return HTTPStatus.NOT_FOUND, f"Server {name} not found."
if (framework == "forge") or (framework == "fabric"):
server_id = mc_manager.start_server(f"users/{user_id}/{name}", port, user_id,
name, version, modded=True)
else:
server_id = mc_manager.start_server(f"users/{user_id}/{name}", port, user_id, name, version)
if server_id is None:
return HTTPStatus.OK, f"You cannot run multiples instances at this time."
try:
firebase_manager.update_server_running_state(user_id, name, True)
except Exception as e:
return HTTPStatus.INTERNAL_SERVER_ERROR, (f"Error updating server {name}'s running state in database. "
f"{type(e).__name__}{str(e)}.")
file_manager.log_action(user_id, name, "ServerRun")
return HTTPStatus.ACCEPTED, f"Successfully started server '{name}'."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Error when running server: {e}"
def server_stop(user: UserRecord, name: str) -> tuple[HTTPStatus, Union[str, None]]:
port = None
user_id: str = user.uid
if mc_manager.has_cooldown(user_id=user_id):
return HTTPStatus.FORBIDDEN, f"Wait 30 seconds before stopping '{name}'."
mc_manager.set_cooldown(user_id=user_id)
try:
port = firebase_manager.get_server_port(user_id)
if port is None:
return HTTPStatus.NOT_FOUND, f"Server '{name}' not found in firestore."
exists: bool = mc_manager.stop_server(port)
if exists:
firebase_manager.update_server_running_state(user_id, name, False)
file_manager.log_action(user_id, name, "ServerStop")
return HTTPStatus.OK, f"Successfully stopped server '{name}'."
return HTTPStatus.OK, f"Server '{name}' already stopped."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
if port:
mc_manager.stop_server_forcefully(port)
firebase_manager.update_server_running_state(user_id, name, False)
file_manager.log_action(user_id, name, "ServerStop")
return HTTPStatus.OK, f"Successfully stopped server '{name}'."
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Error occurred when stopping server '{name}'."
def update_property(uid: str, name: str, prop: str, value: str) -> tuple[HTTPStatus, Union[str, None]]:
property_file_path: str = f"users/{uid}/{name}/server.properties"
try:
file_manager.update_server_property(property_file_path, prop, value)
firebase_manager.update_server_property(uid, name, prop, value)
return HTTPStatus.OK, f"Successfully set '{prop}' to '{value}'."
except ValueError as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.BAD_REQUEST, f"Property '{prop}' not found."
except FileNotFoundError:
return HTTPStatus.NOT_FOUND, f"File server.properties for server '{name}' not found."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Unhandled error: {type(e).__name__}, {str(e)}"
def update_properties(user: UserRecord, name: str, props: list[tuple[str, str]]) -> tuple[HTTPStatus, Union[str, None]]:
errors: list[str] = []
for prop, value in props:
if prop not in mc_manager.allowed_properties:
return HTTPStatus.FORBIDDEN, f"Property '{prop}' not allowed."
status, message = update_property(uid=user.uid, name=name, prop=prop, value=value)
if status != HTTPStatus.OK:
errors.append(message)
if len(errors) > 0:
return HTTPStatus.IM_A_TEAPOT, str(errors)
file_manager.log_action(user.uid, name, "UpdateProperties", str(props))
return HTTPStatus.OK, f"Successfully updated server '{name}'."
def run_command(user: UserRecord, command: str, name: str) -> tuple[HTTPStatus, Union[str, None]]:
try:
port = firebase_manager.get_server_port(user.uid)
if port is None:
return HTTPStatus.NOT_FOUND, f"Server '{name}' not found in firestore."
mc_manager.execute_server_command(port, command)
file_manager.log_action(user.uid, name, "Command", command)
return HTTPStatus.OK, f"Command '{command}' executed successfully."
except Exception as e:
file_manager.log_error(type(e).__name__, str(e))
return HTTPStatus.INTERNAL_SERVER_ERROR, f"Error executing command: {command} || {str(e)}"
def scheduled_actions() -> None:
mc_manager.check_servers_idle()
if __name__ == "__main__":
pass