""" Simple reverse proxy + static file server for Monica Proxy with admin endpoints for cookie management """ import asyncio import subprocess import os import signal from aiohttp import web, ClientSession, ClientTimeout BACKEND_PORT = 8080 FRONTEND_PORT = 7860 # Global reference to backend process backend_process = None async def proxy_handler(request: web.Request): """Proxy requests to monica-proxy backend""" backend_url = f"http://127.0.0.1:{BACKEND_PORT}{request.path_qs}" async with ClientSession(timeout=ClientTimeout(total=300)) as session: try: headers = {k: v for k, v in request.headers.items() if k.lower() not in ('host', 'content-length')} body = await request.read() if request.can_read_body else None async with session.request( method=request.method, url=backend_url, headers=headers, data=body, ) as resp: response_body = await resp.read() return web.Response( status=resp.status, headers={k: v for k, v in resp.headers.items() if k.lower() not in ('content-encoding', 'transfer-encoding', 'content-length')}, body=response_body ) except Exception as e: return web.json_response( {"error": str(e), "message": "Backend connection failed"}, status=502 ) async def index_handler(request: web.Request): """Serve the status page""" return web.FileResponse('index.html') async def update_cookie_handler(request: web.Request): """Update MONICA_COOKIE and restart backend""" global backend_process # Verify authorization auth_header = request.headers.get('Authorization', '') expected_token = os.environ.get('BEARER_TOKEN', '') if not expected_token: return web.json_response( {"error": "BEARER_TOKEN not configured on server"}, status=500 ) if not auth_header.startswith('Bearer ') or auth_header[7:] != expected_token: return web.json_response( {"error": "Invalid authorization"}, status=401 ) try: data = await request.json() new_cookie = data.get('cookie', '').strip() if not new_cookie: return web.json_response( {"error": "Cookie value is required"}, status=400 ) # Update environment variable os.environ['MONICA_COOKIE'] = new_cookie print(f"[admin] MONICA_COOKIE updated, length: {len(new_cookie)}") # Restart backend if backend_process: print("[admin] Restarting backend...") backend_process.terminate() await asyncio.sleep(1) backend_process = await start_backend() await asyncio.sleep(2) print("[admin] Backend restarted") return web.json_response({ "success": True, "message": "Cookie updated and backend restarted" }) except Exception as e: return web.json_response( {"error": str(e)}, status=500 ) async def get_cookie_status_handler(request: web.Request): """Get current cookie status (masked)""" auth_header = request.headers.get('Authorization', '') expected_token = os.environ.get('BEARER_TOKEN', '') if not auth_header.startswith('Bearer ') or auth_header[7:] != expected_token: return web.json_response( {"error": "Invalid authorization"}, status=401 ) cookie = os.environ.get('MONICA_COOKIE', '') if cookie: # Mask the cookie, show only first and last 10 chars if len(cookie) > 30: masked = cookie[:10] + '...' + cookie[-10:] else: masked = cookie[:5] + '...' else: masked = '(not set)' return web.json_response({ "cookie_set": bool(cookie), "cookie_length": len(cookie), "cookie_preview": masked }) async def start_backend(): """Start monica-proxy in background""" env = os.environ.copy() env['SERVER_PORT'] = str(BACKEND_PORT) env['SERVER_HOST'] = '0.0.0.0' process = await asyncio.create_subprocess_exec( './monica-proxy', env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) # Log backend output async def log_output(): while True: line = await process.stdout.readline() if not line: break print(f"[backend] {line.decode().strip()}") asyncio.create_task(log_output()) return process async def main(): global backend_process # Start backend print(f"Starting monica-proxy on port {BACKEND_PORT}...") backend_process = await start_backend() # Give backend time to start await asyncio.sleep(2) # Setup web server app = web.Application() # Static routes app.router.add_get('/', index_handler) # Admin routes app.router.add_post('/admin/update-cookie', update_cookie_handler) app.router.add_get('/admin/cookie-status', get_cookie_status_handler) # Proxy routes (must be last due to catch-all) app.router.add_route('*', '/v1/{path:.*}', proxy_handler) app.router.add_route('*', '/{path:.*}', proxy_handler) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, '0.0.0.0', FRONTEND_PORT) print(f"Starting frontend on port {FRONTEND_PORT}...") await site.start() print(f"Server ready!") print(f" Status page: http://0.0.0.0:{FRONTEND_PORT}/") print(f" API: http://0.0.0.0:{FRONTEND_PORT}/v1/...") print(f" Admin: http://0.0.0.0:{FRONTEND_PORT}/admin/...") # Keep running try: await asyncio.Event().wait() finally: if backend_process: backend_process.terminate() if __name__ == '__main__': asyncio.run(main())