diff --git a/routes/admin.py b/routes/admin.py index 4470ea8..c1b3f66 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -196,6 +196,11 @@ def admin_get_user_servers(user_id): return jsonify({'success': False, 'error': 'Admin access required'}), 403 servers = get_user_servers(user_id) + # Admin server management does not need connection credentials. + servers = [ + {key: value for key, value in server.items() if key != 'sftp_password'} + for server in servers + ] return jsonify({'success': True, 'servers': servers}), 200 except Exception as e: @@ -331,7 +336,7 @@ def admin_get_all_servers(): cursor.execute(''' SELECT s.id, s.name, s.cpu, s.ram, s.storage, s.image, s.type, s.version, s.loader, s.java_version, s.status, s.ports, s.created_at, u.username as owner, u.id as owner_id, - s.sftp_username, s.sftp_password, s.sftp_port + s.sftp_username, s.sftp_port FROM servers s JOIN users u ON s.owner_id = u.id ORDER BY u.username, s.name diff --git a/routes/servers.py b/routes/servers.py index 2c269a6..cba8712 100644 --- a/routes/servers.py +++ b/routes/servers.py @@ -4,8 +4,10 @@ from datetime import datetime from flask import Blueprint, jsonify, request import json +from urllib.parse import urlsplit from kubernetes_service import k8s_manager import requests +from werkzeug.utils import secure_filename from runtime_config import ( MODRINTH_API_BASE, logger, @@ -317,6 +319,21 @@ def download_plugin_to_server(): if not server_id or not plugin_url: return jsonify({'error': 'Server ID and plugin URL are required'}), 400 + try: + parsed_plugin_url = urlsplit(plugin_url) + plugin_port = parsed_plugin_url.port + except (TypeError, ValueError): + return jsonify({'error': 'Plugin URL is invalid'}), 400 + if (parsed_plugin_url.scheme != 'https' + or parsed_plugin_url.hostname != 'cdn.modrinth.com' + or parsed_plugin_url.username or parsed_plugin_url.password + or plugin_port not in (None, 443)): + return jsonify({'error': 'Plugin files must come from the Modrinth CDN'}), 400 + + safe_file_name = secure_filename(str(file_name or 'plugin.jar')) + if not safe_file_name.lower().endswith('.jar'): + return jsonify({'error': 'Plugin file name must end in .jar'}), 400 + # Verify server belongs to user user_servers = get_user_servers(user['id']) server = next((s for s in user_servers if s['id'] == server_id), None) @@ -330,26 +347,37 @@ def download_plugin_to_server(): # Download plugin file logger.info(f"Downloading plugin from {plugin_url} for server {server_id}") - plugin_response = requests.get(plugin_url, stream=True, timeout=30) + plugin_response = requests.get( + plugin_url, stream=True, timeout=30, allow_redirects=False + ) if plugin_response.status_code != 200: + plugin_response.close() return jsonify({'error': f'Failed to download plugin: HTTP {plugin_response.status_code}'}), 500 # Determine plugins directory based on server type # For Minecraft servers, plugins go in /data/plugins directory plugins_dir = '/data/plugins' - file_path = f'{plugins_dir}/{file_name}' + file_path = f'{plugins_dir}/{safe_file_name}' # Save plugin to temporary file first import tempfile import os - with tempfile.NamedTemporaryFile(delete=False, suffix='.jar') as temp_file: - temp_file_path = temp_file.name - for chunk in plugin_response.iter_content(chunk_size=8192): - if chunk: - temp_file.write(chunk) - + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.jar') + temp_file_path = temp_file.name try: + with temp_file: + try: + total_bytes = 0 + for chunk in plugin_response.iter_content(chunk_size=8192): + if chunk: + total_bytes += len(chunk) + if total_bytes > 100 * 1024 * 1024: + raise ValueError('Plugin file exceeds the 100 MB limit') + temp_file.write(chunk) + finally: + plugin_response.close() + # Upload to server namespace = f"user-{user['username']}".lower() pods = k8s_manager.get_pods_for_server(namespace, server_id)