refactor(extension): 모듈 분리 + Hub 통합 테스트 #task-395
- extension.ts 3,446→1,289줄 (-63%) - step-probe.ts (1,435줄): setupMonitor, processResponseFile, tryApprovalStrategies - observer-script.ts (687줄): DOM observer script - ws-client.ts (390줄): WSBridgeClient - step-utils.ts (114줄): step 파싱 유틸 - auth.py (115줄): JWT + registration code - hub.py (581줄): WSHub + per-client queue - Hub WS 연동 테스트 통과 (auth, chat, register) - VSIX v0.4.0 빌드
This commit is contained in:
47
gateway.py
47
gateway.py
@@ -1,10 +1,12 @@
|
||||
"""Gateway HTTP API — receives data from remote Collectors and routes to Discord bot.
|
||||
"""Gateway HTTP API + WebSocket Hub — receives data from Collectors and Extensions.
|
||||
|
||||
Runs alongside the Discord bot in the server Docker container.
|
||||
Collectors (local PCs) push pending approvals, chat snapshots, and registrations
|
||||
to this API, and poll for responses.
|
||||
Supports both:
|
||||
- REST API: for legacy Collectors (HTTP polling)
|
||||
- WebSocket: for direct Extension connections (real-time)
|
||||
|
||||
Endpoints:
|
||||
GET /ws — WebSocket endpoint (Extension direct connection)
|
||||
POST /api/pending — Collector pushes a new approval request
|
||||
GET /api/pending — List all pending requests (for diagnostics)
|
||||
POST /api/response/{rid} — Collector polls for response (or Gateway pushes)
|
||||
@@ -14,6 +16,7 @@ Endpoints:
|
||||
POST /api/command — Gateway pushes command to specific collector
|
||||
GET /api/commands/{project} — Collector polls for commands
|
||||
GET /health — Health check
|
||||
GET /hub/status — WebSocket Hub diagnostics
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -34,13 +37,15 @@ COMMAND_TTL = 1800 # 30 min — stale commands auto-deleted
|
||||
|
||||
|
||||
class GatewayAPI:
|
||||
"""HTTP API server for Collector ↔ Gateway communication."""
|
||||
"""HTTP API + WebSocket Hub server."""
|
||||
|
||||
def __init__(self, bot, host: str = "0.0.0.0", port: int = 8585, api_key: str = ""):
|
||||
def __init__(self, bot, host: str = "0.0.0.0", port: int = 8585,
|
||||
api_key: str = "", hub=None):
|
||||
self.bot = bot
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.api_key = api_key
|
||||
self.hub = hub # WSHub instance (None = WS disabled)
|
||||
self.app = web.Application(
|
||||
middlewares=[self._auth_middleware],
|
||||
client_max_size=1024 * 1024, # Security: 1MB max request body
|
||||
@@ -52,6 +57,10 @@ class GatewayAPI:
|
||||
self._rate_limits: dict[str, list[float]] = defaultdict(list) # IP → [timestamps]
|
||||
|
||||
def _setup_routes(self):
|
||||
# WebSocket endpoint (no auth middleware — Hub handles its own auth)
|
||||
self.app.router.add_get("/ws", self._ws_handler)
|
||||
self.app.router.add_get("/hub/status", self._hub_status)
|
||||
# Legacy REST endpoints (Collector compatibility)
|
||||
self.app.router.add_get("/health", self._health)
|
||||
self.app.router.add_post("/api/pending", self._post_pending)
|
||||
self.app.router.add_get("/api/pending", self._list_pending)
|
||||
@@ -61,13 +70,29 @@ class GatewayAPI:
|
||||
self.app.router.add_get("/api/commands/{project}", self._get_commands)
|
||||
self.app.router.add_post("/api/event", self._post_event)
|
||||
|
||||
# ─── WebSocket Handler ───
|
||||
|
||||
async def _ws_handler(self, request: web.Request) -> web.WebSocketResponse:
|
||||
"""WebSocket endpoint for direct Extension connections."""
|
||||
if not self.hub:
|
||||
return web.json_response(
|
||||
{"error": "WebSocket Hub not enabled"}, status=503
|
||||
)
|
||||
return await self.hub.handle_ws(request)
|
||||
|
||||
async def _hub_status(self, request: web.Request) -> web.Response:
|
||||
"""WebSocket Hub diagnostics."""
|
||||
if not self.hub:
|
||||
return web.json_response({"hub": "disabled"})
|
||||
return web.json_response(self.hub.get_status())
|
||||
|
||||
# ─── Auth Middleware ───
|
||||
|
||||
@web.middleware
|
||||
async def _auth_middleware(self, request: web.Request, handler):
|
||||
"""Reject requests without valid API key on /api/* routes."""
|
||||
# Health endpoint is public
|
||||
if request.path == "/health":
|
||||
# WebSocket and public endpoints skip API key auth
|
||||
if request.path in ("/health", "/ws", "/hub/status"):
|
||||
return await handler(request)
|
||||
|
||||
# All /api/* routes require auth + rate limit
|
||||
@@ -109,11 +134,15 @@ class GatewayAPI:
|
||||
# ─── Health ───
|
||||
|
||||
async def _health(self, request: web.Request) -> web.Response:
|
||||
return web.json_response({
|
||||
status = {
|
||||
"status": "ok",
|
||||
"bot_ready": self.bot.is_ready() if self.bot else False,
|
||||
"timestamp": time.time(),
|
||||
})
|
||||
"hub_enabled": self.hub is not None,
|
||||
}
|
||||
if self.hub:
|
||||
status["hub_connections"] = len(self.hub.connections)
|
||||
return web.json_response(status)
|
||||
|
||||
# ─── Pending Approvals (Collector → Gateway → Discord) ───
|
||||
|
||||
|
||||
Reference in New Issue
Block a user