diff --git a/.env.example b/.env.example index a339221..5d90c91 100644 --- a/.env.example +++ b/.env.example @@ -9,13 +9,8 @@ SECRET_TOKEN=asdasdasd # Services Directory (where managed services will be stored) SERVICES_DIR=/root/services -# Scoreboard Configuration (ForcAD uses Socket.IO) +# Scoreboard Configuration (ForcAD Socket.IO) SCOREBOARD_URL=http://94.228.113.149:8080 -USE_SOCKETIO=true -USE_HTTP_POLLING=false -SCOREBOARD_WS_URL=ws://10.60.0.1:8080/api/events -SCOREBOARD_API_URL=http://10.60.0.1:8080/api -POLLING_INTERVAL=10 OUR_TEAM_ID=1 ALERT_THRESHOLD_POINTS=100 ALERT_THRESHOLD_TIME=300 diff --git a/docker-compose.yaml b/docker-compose.yaml index 33343f5..c65ca82 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -41,7 +41,7 @@ services: networks: - adctrl-network - # Scoreboard Injector - monitors attacks + # Scoreboard Injector - monitors attacks via Socket.IO scoreboard-injector: build: ./scoreboard_injector container_name: adctrl-scoreboard @@ -49,11 +49,6 @@ services: DATABASE_URL: postgresql://${POSTGRES_USER:-adctrl}:${POSTGRES_PASSWORD:-adctrl_secure_password}@postgres:5432/${POSTGRES_DB:-adctrl} SECRET_TOKEN: ${SECRET_TOKEN} SCOREBOARD_URL: ${SCOREBOARD_URL:-http://10.60.0.1:8080} - USE_SOCKETIO: ${USE_SOCKETIO:-true} - USE_HTTP_POLLING: ${USE_HTTP_POLLING:-false} - SCOREBOARD_WS_URL: ${SCOREBOARD_WS_URL:-ws://10.60.0.1:8080/api/events} - SCOREBOARD_API_URL: ${SCOREBOARD_API_URL:-http://10.60.0.1:8080/api} - POLLING_INTERVAL: ${POLLING_INTERVAL:-10} OUR_TEAM_ID: ${OUR_TEAM_ID:-1} ALERT_THRESHOLD_POINTS: ${ALERT_THRESHOLD_POINTS:-100} ALERT_THRESHOLD_TIME: ${ALERT_THRESHOLD_TIME:-300} diff --git a/scoreboard_injector/main.py b/scoreboard_injector/main.py index a072a34..03f4f2b 100644 --- a/scoreboard_injector/main.py +++ b/scoreboard_injector/main.py @@ -1,13 +1,12 @@ """ Scoreboard Injector for ForcAD -Monitors WebSocket for attacks and alerts on critical situations +Monitors Socket.IO events for attacks and alerts on critical situations """ import os import json import asyncio from datetime import datetime, timedelta from typing import Optional, Dict, Any -import aiohttp import socketio from fastapi import FastAPI, HTTPException, Depends, Header from pydantic import BaseModel @@ -17,12 +16,7 @@ from contextlib import asynccontextmanager # Configuration DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://adctrl:adctrl@postgres:5432/adctrl") SECRET_TOKEN = os.getenv("SECRET_TOKEN", "change-me-in-production") -SCOREBOARD_URL = os.getenv("SCOREBOARD_URL", "http://10.60.0.1:8080") # Base URL for Socket.IO -SCOREBOARD_WS_URL = os.getenv("SCOREBOARD_WS_URL", "ws://scoreboard:8080/api/events") -SCOREBOARD_API_URL = os.getenv("SCOREBOARD_API_URL", "http://10.60.0.1:8080/api") -USE_HTTP_POLLING = os.getenv("USE_HTTP_POLLING", "false").lower() == "true" -USE_SOCKETIO = os.getenv("USE_SOCKETIO", "true").lower() == "true" # Use Socket.IO by default -POLLING_INTERVAL = int(os.getenv("POLLING_INTERVAL", "10")) # seconds +SCOREBOARD_URL = os.getenv("SCOREBOARD_URL", "http://10.60.0.1:8080") OUR_TEAM_ID = int(os.getenv("OUR_TEAM_ID", "1")) ALERT_THRESHOLD_POINTS = float(os.getenv("ALERT_THRESHOLD_POINTS", "100")) ALERT_THRESHOLD_TIME = int(os.getenv("ALERT_THRESHOLD_TIME", "300")) # seconds @@ -178,6 +172,7 @@ async def check_and_create_alerts(conn, attacker_id: int, service_name: str): async def send_telegram_alert(message: str): """Send alert to telegram bot""" + import aiohttp try: async with aiohttp.ClientSession() as session: async with session.post( @@ -190,91 +185,6 @@ async def send_telegram_alert(message: str): except Exception as e: print(f"Error sending telegram alert: {e}") -async def http_polling_listener(): - """Poll scoreboard HTTP API for attacks as alternative to WebSocket""" - last_check_time = datetime.utcnow() - - while True: - try: - async with aiohttp.ClientSession() as session: - # ForcAD real API endpoints (not Vue.js router paths) - # These should be accessed without /api prefix or with different structure - base_url = SCOREBOARD_API_URL.rstrip('/api').rstrip('/') - - endpoints_to_try = [ - # Try direct API access patterns - (f'{base_url}/flags', 'GET'), - (f'{base_url}/stolen_flags', 'GET'), - (f'{base_url}/flag_stats', 'GET'), - (f'{base_url}/game_state', 'GET'), - (f'{base_url}/scoreboard', 'GET'), - # Try with /api/ prefix variations - (f'{base_url}/api/flags/', 'GET'), - (f'{base_url}/api/stolen_flags/', 'GET'), - (f'{base_url}/api/game_state/', 'GET'), - ] - - data_found = False - - for endpoint, method in endpoints_to_try: - try: - async with session.get(endpoint, timeout=aiohttp.ClientTimeout(total=5)) as resp: - if resp.status == 200: - content_type = resp.headers.get('Content-Type', '') - - # Skip HTML responses - if 'text/html' in content_type: - continue - - try: - data = await resp.json() - print(f"✓ Fetched JSON from {endpoint}") - data_found = True - - # Process based on response structure - if isinstance(data, list): - # List of attacks/events - for item in data: - if isinstance(item, dict): - await process_attack_event(item) - elif isinstance(data, dict): - # Try common keys for attack data - for key in ['attacks', 'stolen_flags', 'flags', 'events', 'data', 'rows']: - if key in data: - items = data[key] - if isinstance(items, list): - print(f" Found {len(items)} items in '{key}'") - for item in items: - if isinstance(item, dict): - await process_attack_event(item) - break - else: - # Might be a single event or game state - # Check if it contains attack-like data - if any(k in data for k in ['attacker_id', 'victim_id', 'team_id', 'flag']): - await process_attack_event(data) - - if data_found: - break # Found valid data, no need to try other endpoints - - except (json.JSONDecodeError, aiohttp.ContentTypeError): - # Not JSON, skip - continue - - except aiohttp.ClientError as e: - continue - except Exception as e: - print(f"Error fetching {endpoint}: {e}") - continue - - if not data_found: - print(f"No valid JSON data found from scoreboard API (last check: {datetime.utcnow().strftime('%H:%M:%S')})") - - except Exception as e: - print(f"HTTP polling error: {e}") - - await asyncio.sleep(POLLING_INTERVAL) - async def socketio_listener(): """Listen to ForcAD scoreboard using Socket.IO""" sio = socketio.AsyncClient(logger=False, engineio_logger=False) @@ -340,87 +250,17 @@ async def socketio_listener(): print("Reconnecting in 5 seconds...") await asyncio.sleep(5) -async def websocket_listener(): - """Listen to scoreboard WebSocket for events""" - reconnect_delay = 5 - max_reconnect_delay = 60 - - while True: - try: - print(f"Attempting to connect to scoreboard WebSocket: {SCOREBOARD_WS_URL}") - - async with aiohttp.ClientSession() as session: - try: - async with session.ws_connect( - SCOREBOARD_WS_URL, - timeout=aiohttp.ClientTimeout(total=10) - ) as ws: - print(f"✓ Connected to scoreboard WebSocket") - reconnect_delay = 5 # Reset delay on successful connection - - async for msg in ws: - if msg.type == aiohttp.WSMsgType.TEXT: - try: - event = json.loads(msg.data) - event_type = event.get('type', 'unknown') - - # Process attack-related events - # ForcAD can send: 'attack', 'flag_stolen', 'stolen_flag', 'flag_submit' - if event_type in ['attack', 'flag_stolen', 'stolen_flag', 'flag_submit', 'flag']: - await process_attack_event(event) - # Optionally log other event types for debugging - # else: - # print(f"Received event type: {event_type}") - - except json.JSONDecodeError as e: - print(f"Failed to decode WebSocket message: {msg.data[:200]}") - except Exception as e: - print(f"Error processing event: {e}") - - elif msg.type == aiohttp.WSMsgType.CLOSED: - print("WebSocket connection closed by server") - break - elif msg.type == aiohttp.WSMsgType.ERROR: - print(f"WebSocket error: {ws.exception()}") - break - - except aiohttp.ClientError as e: - print(f"WebSocket client error: {e}") - except asyncio.TimeoutError: - print(f"Connection timeout to {SCOREBOARD_WS_URL}") - - except Exception as e: - print(f"WebSocket connection error: {e}") - - print(f"Reconnecting in {reconnect_delay} seconds...") - await asyncio.sleep(reconnect_delay) - - # Exponential backoff - reconnect_delay = min(reconnect_delay * 1.5, max_reconnect_delay) - # Lifespan context @asynccontextmanager async def lifespan(app: FastAPI): global db_pool, ws_task db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10) + print(f"Starting Socket.IO listener") + print(f"Scoreboard URL: {SCOREBOARD_URL}") print(f"Our team ID: {OUR_TEAM_ID}") - # Start listener based on configuration - if USE_SOCKETIO: - print(f"Starting Socket.IO mode") - print(f"Scoreboard URL: {SCOREBOARD_URL}") - ws_task = asyncio.create_task(socketio_listener()) - elif USE_HTTP_POLLING: - print(f"Starting HTTP polling mode (interval: {POLLING_INTERVAL}s)") - print(f"Scoreboard API: {SCOREBOARD_API_URL}") - ws_task = asyncio.create_task(http_polling_listener()) - else: - print(f"Starting WebSocket listener mode") - print(f"Scoreboard WS: {SCOREBOARD_WS_URL}") - ws_task = asyncio.create_task(websocket_listener()) - - print(f"Our team ID: {OUR_TEAM_ID}") + ws_task = asyncio.create_task(socketio_listener()) yield @@ -443,8 +283,8 @@ async def health_check(): "status": "ok", "timestamp": datetime.utcnow().isoformat(), "team_id": OUR_TEAM_ID, - "mode": "http_polling" if USE_HTTP_POLLING else "websocket", - "scoreboard_url": SCOREBOARD_API_URL if USE_HTTP_POLLING else SCOREBOARD_WS_URL + "mode": "socketio", + "scoreboard_url": SCOREBOARD_URL } @app.get("/stats", dependencies=[Depends(verify_token)]) @@ -588,72 +428,73 @@ async def inject_test_attack(attacker_id: int, victim_id: int, service: str = "t @app.get("/debug/scoreboard", dependencies=[Depends(verify_token)]) async def debug_scoreboard(): - """Check if scoreboard is reachable and show raw data""" + """Check if scoreboard is reachable and show connection info""" + import aiohttp + results = { - "mode": "http_polling" if USE_HTTP_POLLING else "websocket", + "mode": "socketio", + "config": { + "scoreboard_url": SCOREBOARD_URL, + "our_team_id": OUR_TEAM_ID + }, "endpoints_tested": [] } try: async with aiohttp.ClientSession() as session: - # Test WebSocket - results["websocket_url"] = SCOREBOARD_WS_URL + # Test Socket.IO endpoint + socketio_url = f"{SCOREBOARD_URL}/socket.io/?EIO=4&transport=polling" try: - async with session.ws_connect(SCOREBOARD_WS_URL, timeout=aiohttp.ClientTimeout(total=5)) as ws: - results["websocket_status"] = "reachable" + async with session.get(socketio_url, timeout=aiohttp.ClientTimeout(total=5)) as resp: + results["socketio_status"] = { + "url": socketio_url, + "status": resp.status, + "reachable": resp.status == 200, + "response_preview": (await resp.text())[:200] if resp.status == 200 else None + } except Exception as e: - results["websocket_status"] = f"unreachable: {str(e)}" + results["socketio_status"] = { + "url": socketio_url, + "reachable": False, + "error": str(e) + } - # Test HTTP endpoints - both HTML and API paths - base_url = SCOREBOARD_API_URL.rstrip('/api').rstrip('/') - endpoints = [ - f"{base_url}/flags", - f"{base_url}/stolen_flags", - f"{base_url}/flag_stats", - f"{base_url}/game_state", - f"{base_url}/scoreboard", - f"{base_url}/api/flags/", - f"{base_url}/api/stolen_flags/", - f"{base_url}/api/game_state/", - f"{base_url}/api/client/attack_data", - ] + # Test base scoreboard URL + try: + async with session.get(SCOREBOARD_URL, timeout=aiohttp.ClientTimeout(total=5)) as resp: + results["base_url_status"] = { + "url": SCOREBOARD_URL, + "status": resp.status, + "reachable": resp.status == 200 + } + except Exception as e: + results["base_url_status"] = { + "url": SCOREBOARD_URL, + "reachable": False, + "error": str(e) + } - for endpoint in endpoints: - try: - async with session.get(endpoint, timeout=aiohttp.ClientTimeout(total=5)) as resp: - content_type = resp.headers.get('Content-Type', '') - is_json = 'application/json' in content_type - is_html = 'text/html' in content_type - - result = { - "url": endpoint, - "status": resp.status, - "reachable": resp.status == 200, - "content_type": content_type, - "is_json": is_json, - "is_html": is_html - } - - if resp.status == 200: - if is_json: - try: - data = await resp.json() - result["json_preview"] = str(data)[:500] - result["json_keys"] = list(data.keys()) if isinstance(data, dict) else "list" - except: - pass - else: - text = await resp.text() - result["data_preview"] = text[:200] - - results["endpoints_tested"].append(result) - - except Exception as e: - results["endpoints_tested"].append({ - "url": endpoint, - "reachable": False, - "error": str(e) - }) + # Test attack_data endpoint (for reference only) + attack_data_url = f"{SCOREBOARD_URL}/api/client/attack_data" + try: + async with session.get(attack_data_url, timeout=aiohttp.ClientTimeout(total=5)) as resp: + result = { + "url": attack_data_url, + "status": resp.status, + "reachable": resp.status == 200, + "content_type": resp.headers.get('Content-Type', ''), + "note": "Contains exploit credentials, not attack events" + } + if resp.status == 200 and 'application/json' in resp.headers.get('Content-Type', ''): + data = await resp.json() + result["services"] = list(data.keys()) if isinstance(data, dict) else None + results["endpoints_tested"].append(result) + except Exception as e: + results["endpoints_tested"].append({ + "url": attack_data_url, + "reachable": False, + "error": str(e) + }) except Exception as e: results["error"] = str(e)