monica-proxy / server.py
asemxin
Add full model list display and cookie management UI
cc8eb0f
"""
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())