Spaces:
Running
Running
| """ | |
| 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()) | |