Files
BLIGHT--CUE/app.py
Spencer 3ee1d55584 Add debug logging to signature verification
Temporarily logs received vs expected signatures to diagnose
webhook secret mismatches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 22:47:09 -05:00

118 lines
3.7 KiB
Python

import hashlib
import hmac
import json
import logging
import subprocess
import threading
from flask import Flask, request, abort
import config
import gitea_client
import processor
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
def _verify_signature(payload: bytes, signature_header: str | None) -> bool:
"""Validate the Gitea webhook HMAC-SHA256 signature.
Gitea sends X-Gitea-Signature as a raw hex digest (no scheme prefix).
"""
if not signature_header:
logger.warning("Signature verification failed: no signature header received")
return False
expected = hmac.new(
config.WEBHOOK_SECRET.encode(), payload, hashlib.sha256
).hexdigest()
logger.debug("Received signature: %s", signature_header.strip())
logger.debug("Expected signature: %s", expected)
match = hmac.compare_digest(expected, signature_header.strip())
if not match:
logger.warning("Signature mismatch — check WEBHOOK_SECRET matches the secret set in Gitea")
return match
def _handle_push(owner: str, repo: str, changed_files: list[str]) -> None:
"""Process all changed markdown files in a push event."""
for file_path in changed_files:
if not file_path.endswith(".md"):
continue
logger.info("Checking %s/%s: %s", owner, repo, file_path)
try:
content, sha = gitea_client.get_file(owner, repo, file_path)
updated, changed = processor.process_document(content)
if changed:
gitea_client.update_file(owner, repo, file_path, updated, sha)
logger.info("Updated %s", file_path)
else:
logger.info("No BLIGHT triggers found in %s", file_path)
except Exception as exc:
logger.error("Failed processing %s: %s", file_path, exc)
@app.post("/webhook")
def webhook():
payload = request.get_data()
if not _verify_signature(payload, request.headers.get("X-Gitea-Signature")):
logger.warning("Rejected webhook: invalid signature")
abort(403)
event = request.headers.get("X-Gitea-Event")
if event != "push":
return {"status": "ignored", "event": event}, 200
data = json.loads(payload)
owner = data["repository"]["owner"]["login"]
repo = data["repository"]["name"]
# Collect unique file paths from all commits in the push
seen: set[str] = set()
changed_files: list[str] = []
for commit in data.get("commits", []):
for path in commit.get("added", []) + commit.get("modified", []):
if path not in seen:
seen.add(path)
changed_files.append(path)
if not changed_files:
return {"status": "no files"}, 200
# Process in background so we return 200 to Gitea immediately
thread = threading.Thread(
target=_handle_push,
args=(owner, repo, changed_files),
daemon=True,
)
thread.start()
return {"status": "processing", "files": len(changed_files)}, 200
def _self_update() -> None:
"""Pull the latest code from origin before starting."""
try:
result = subprocess.run(
["git", "pull", "--ff-only"],
capture_output=True,
text=True,
)
if result.returncode == 0:
logger.info("Self-update: %s", result.stdout.strip())
else:
logger.warning("Self-update failed: %s", result.stderr.strip())
except Exception as exc:
logger.warning("Self-update error: %s", exc)
if __name__ == "__main__":
_self_update()
logger.info("BLIGHT: CUE starting on port %d", config.WEBHOOK_PORT)
app.run(host="0.0.0.0", port=config.WEBHOOK_PORT)