#!/usr/bin/python3

"""
Original code (tornado based) by Matthew May - mcmay.web@gmail.com
Adjusted code for asyncio, aiohttp and redis (asynchronous support) by t3chn0m4g3
Remote T-Pot support by honeypot.es
"""

import asyncio
import os

import redis.asyncio as redis
from aiohttp import web

# Configuration via environment variables
redis_host = os.getenv("REDIS_HOST", "localhost")
redis_url = f'redis://{redis_host}:6379'
web_port = int(os.getenv("WEB_PORT", "8080"))
version = 'Attack Map Server 3.0.0'


async def redis_subscriber(websockets):
    was_disconnected = False
    while True:
        try:
            r = redis.Redis.from_url(redis_url)
            pubsub = r.pubsub()
            channel = "attack-map-production"
            await pubsub.subscribe(channel)

            if was_disconnected:
                print("[*] Redis connection re-established")
                was_disconnected = False

            while True:
                message = await pubsub.get_message(ignore_subscribe_messages=True)
                if message:
                    try:
                        json_data = message['data'].decode('utf-8')
                        await asyncio.gather(*[ws.send_str(json_data) for ws in websockets], return_exceptions=True)
                    except Exception:
                        print("Something went wrong while sending JSON data.")
                else:
                    await asyncio.sleep(0.1)
        except redis.RedisError as e:
            print(f"[ ] Connection lost to Redis ({type(e).__name__}), retrying...")
            was_disconnected = True
            await asyncio.sleep(5)

async def my_websocket_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)
    request.app['websockets'].append(ws)
    print(f"[*] New WebSocket connection opened. Clients active: {len(request.app['websockets'])}")
    async for msg in ws:
        if msg.type == web.WSMsgType.TEXT:
            await ws.send_str(msg.data)
        elif msg.type == web.WSMsgType.ERROR:
            print(f'WebSocket connection closed with exception {ws.exception()}')
    request.app['websockets'].remove(ws)
    print(f"[-] WebSocket connection closed. Clients active: {len(request.app['websockets'])}")
    return ws

async def my_index_handler(request):
    return web.FileResponse('static/index.html')

async def start_background_tasks(app):
    app['websockets'] = []
    app['redis_subscriber'] = asyncio.create_task(redis_subscriber(app['websockets']))

async def cleanup_background_tasks(app):
    app['redis_subscriber'].cancel()
    await app['redis_subscriber']

async def check_redis_connection():
    """Check Redis connection on startup and wait until available."""
    print(f"[*] Checking Redis connection at {redis_url}...")
    waiting_printed = False

    while True:
        try:
            r = redis.Redis.from_url(redis_url)
            await r.ping()
            await r.aclose()
            print("[*] Redis connection established")
            return True
        except Exception as e:
            if not waiting_printed:
                print(f"[...] Waiting for Redis... (Error: {type(e).__name__})")
                waiting_printed = True
            await asyncio.sleep(5)

async def make_webapp():
    app = web.Application()
    app.add_routes([
        web.get('/', my_index_handler),
        web.get('/websocket', my_websocket_handler),
        web.static('/static/', 'static'),
        web.static('/images/', 'static/images'),
        web.static('/flags/', 'static/flags')
    ])
    app.on_startup.append(start_background_tasks)
    app.on_cleanup.append(cleanup_background_tasks)
    return app

if __name__ == '__main__':
    print(version)
    asyncio.run(check_redis_connection())
    print(f"[*] Starting web server on port {web_port}...\n")
    web.run_app(make_webapp(), port=web_port)
