mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 08:52:45 +00:00
169 lines
6.3 KiB
Python
169 lines
6.3 KiB
Python
"""
|
|
Webhook Handler.
|
|
S2: ChatOps/webhook auth + least privilege.
|
|
S17: Rate limiting.
|
|
|
|
POST /moltbot/webhook
|
|
- Requires auth (deny-by-default)
|
|
- Accepts strict JobSpec
|
|
- Returns normalized internal request
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
|
|
from aiohttp import web
|
|
|
|
try:
|
|
from ..models.schemas import MAX_BODY_SIZE, WebhookJobRequest
|
|
from ..services.metrics import metrics
|
|
from ..services.rate_limit import check_rate_limit
|
|
from ..services.trace import get_effective_trace_id
|
|
from ..services.webhook_auth import get_auth_summary, require_auth
|
|
except ImportError:
|
|
from models.schemas import MAX_BODY_SIZE, WebhookJobRequest
|
|
from services.metrics import metrics
|
|
from services.rate_limit import check_rate_limit
|
|
from services.trace import get_effective_trace_id
|
|
from services.webhook_auth import get_auth_summary, require_auth
|
|
|
|
try:
|
|
from ..services.diagnostics_flags import diagnostics
|
|
except ImportError:
|
|
from services.diagnostics_flags import diagnostics
|
|
|
|
# R46: Scoped logger for safe-by-default redaction
|
|
logger = diagnostics.get_logger("ComfyUI-OpenClaw.api.webhook", "webhook")
|
|
|
|
|
|
def safe_error_response(status: int, error: str, detail: str = "") -> web.Response:
|
|
"""
|
|
Return a safe error response (no secrets, no stack traces).
|
|
"""
|
|
body = {"ok": False, "error": error}
|
|
if detail:
|
|
body["detail"] = detail
|
|
return web.json_response(body, status=status)
|
|
|
|
|
|
async def webhook_handler(request: web.Request) -> web.Response:
|
|
"""
|
|
POST /moltbot/webhook
|
|
|
|
Authenticated endpoint for external job requests.
|
|
"""
|
|
# S17: Rate Limit
|
|
if not check_rate_limit(request, "webhook"):
|
|
metrics.inc("webhook_denied")
|
|
return web.json_response(
|
|
{"ok": False, "error": "rate_limit_exceeded"},
|
|
status=429,
|
|
headers={"Retry-After": "60"},
|
|
)
|
|
|
|
try:
|
|
# Check content-type
|
|
content_type = request.headers.get("Content-Type", "")
|
|
if not content_type.startswith("application/json"):
|
|
metrics.inc("webhook_denied")
|
|
return safe_error_response(
|
|
415, "unsupported_media_type", "Content-Type must be application/json"
|
|
)
|
|
|
|
# Read raw body with size limit
|
|
try:
|
|
raw_body = await request.content.read(MAX_BODY_SIZE + 1)
|
|
if len(raw_body) > MAX_BODY_SIZE:
|
|
metrics.inc("webhook_denied")
|
|
return safe_error_response(
|
|
413, "payload_too_large", f"Max body size: {MAX_BODY_SIZE} bytes"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to read request body: {e}")
|
|
metrics.inc("errors")
|
|
return safe_error_response(400, "read_error")
|
|
|
|
# Require auth
|
|
valid, error = require_auth(request, raw_body)
|
|
if not valid:
|
|
# R46: Use debug log for details (safe redaction), warning for summary
|
|
logger.debug(f"Webhook auth failed details", data={"error": error})
|
|
logger.warning(f"Webhook auth failed: {error}")
|
|
metrics.inc("webhook_denied")
|
|
|
|
# Map error to appropriate status code
|
|
if error in (
|
|
"auth_not_configured",
|
|
"bearer_not_configured",
|
|
"hmac_not_configured",
|
|
):
|
|
return safe_error_response(403, error)
|
|
else:
|
|
return safe_error_response(401, error)
|
|
|
|
# Parse JSON
|
|
try:
|
|
data = json.loads(raw_body.decode("utf-8"))
|
|
# R46: Log payload if validation diagnostics enabled
|
|
if diagnostics.is_enabled("webhook.validate"):
|
|
# Use a separate logger for validation if we want specific granularity,
|
|
# or just reuse the main one but check the flag dynamically?
|
|
# Since logger is scoped to "webhook", we can check specific sub-flag here manually.
|
|
# Actually, let's just log to the main scoped logger, but with a clear prefix.
|
|
# Or create a sub-scope logger?
|
|
# For simplicity, reuse main logger but only call debug if specific intent matches?
|
|
# The 'diagnostics.get_logger' wraps 'debug' with 'is_enabled("webhook")'.
|
|
# If we want detailed validation logs only on "webhook.validate", we can do:
|
|
diagnostics.get_logger(
|
|
"ComfyUI-OpenClaw.api.webhook.validate", "webhook.validate"
|
|
).debug("Incoming Payload", data=data)
|
|
|
|
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
metrics.inc("webhook_denied")
|
|
return safe_error_response(400, "invalid_json")
|
|
|
|
# Validate schema
|
|
try:
|
|
job_request = WebhookJobRequest.from_dict(data)
|
|
except ValueError as e:
|
|
metrics.inc("webhook_denied")
|
|
return safe_error_response(400, "validation_error", str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected validation error: {e}")
|
|
metrics.inc("errors")
|
|
return safe_error_response(400, "validation_error")
|
|
|
|
# R25: Trace Context Extraction
|
|
trace_id = get_effective_trace_id(request.headers, data)
|
|
|
|
# Inject trace_id into flattened normalization if applicable,
|
|
# or just ensure it's returned so caller can track it.
|
|
# The job_request object *has* a trace_id field (we checked schemas.py).
|
|
# But if it wasn't in input, it might be None.
|
|
# We should set it on the object so to_normalized() includes it?
|
|
if trace_id:
|
|
job_request.trace_id = trace_id
|
|
|
|
# Success - return normalized request
|
|
metrics.inc("webhook_requests")
|
|
|
|
normalized_data = job_request.to_normalized()
|
|
# Ensure trace_id is in normalized data if not already
|
|
if "trace_id" not in normalized_data or not normalized_data["trace_id"]:
|
|
normalized_data["trace_id"] = trace_id
|
|
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"accepted": True,
|
|
"trace_id": trace_id,
|
|
"normalized": normalized_data,
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
# Catch-all for unexpected errors - log but don't expose details
|
|
logger.exception(f"Unexpected webhook error: {e}")
|
|
metrics.inc("errors")
|
|
return safe_error_response(500, "internal_error")
|