import asyncio import hashlib import hmac import json import time from collections import defaultdict, deque from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse import threading # Configuration SHARED_SECRET = b"my_secret_key" DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1234567890/test" MAX_DISCORD_RATE = 5 # events per second RATE_LIMIT_WINDOW = 1.0 # seconds # In-memory stores idempotency_store = {} # event_id -> timestamp discord_forward_count = 0 discord_forward_times = deque(maxlen=MAX_DISCORD_RATE) # timestamps of forwards # Token bucket for rate limiting bucket_tokens = MAX_DISCORD_RATE last_refill = time.time() bucket_lock = threading.Lock() def verify_signature(body, signature): """Verify HMAC signature of the request body.""" expected = hmac.new(SHARED_SECRET, body, hashlib.sha256).hexdigest() return hmac.compare_digest(signature, expected) def is_idempotent(event_id): """Check if event has been seen recently.""" now = time.time() if event_id in idempotency_store: if now - idempotency_store[event_id] < 300: # 5 minutes return True idempotency_store[event_id] = now return False def rate_limit(): """Check if we can make a Discord forward.""" global bucket_tokens, last_refill with bucket_lock: now = time.time() # Refill tokens based on time passed elapsed = now - last_refill if elapsed > RATE_LIMIT_WINDOW: bucket_tokens = min(MAX_DISCORD_RATE, bucket_tokens + elapsed / RATE_LIMIT_WINDOW * MAX_DISCORD_RATE) last_refill = now if bucket_tokens >= 1: bucket_tokens -= 1 return True return False async def discord_send(payload): """Simulate sending to Discord webhook.""" # Simulate occasional 429 import random if random.random() < 0.05: # 5% chance of 429 retry_after = random.randint(1, 3) raise Exception(f"HTTP 429: Retry-After {retry_after}") # Simulate success return True class WebhookHandler(BaseHTTPRequestHandler): def do_POST(self): if self.path != "/webhook": self.send_response(404) self.end_headers() return # Read body content_length = int(self.headers.get('Content-Length', 0)) if not content_length: self.send_response(400) self.end_headers() return body = self.rfile.read(content_length) signature = self.headers.get('X-Signature') if not signature: self.send_response(401) self.end_headers() return # Verify signature if not verify_signature(body, signature): self.send_response(401) self.end_headers() return try: data = json.loads(body) except json.JSONDecodeError: self.send_response(400) self.end_headers() return event_id = data.get("event_id") if not event_id: self.send_response(400) self.end_headers() return # Check idempotency if is_idempotent(event_id): self.send_response(200) self.end_headers() return # Forward to Discord asyncio.run(self.forward_to_discord(data)) self.send_response(200) self.end_headers() async def forward_to_discord(self, data): """Forward event to Discord with rate limiting and retries.""" # Rate limit check while not rate_limit(): await asyncio.sleep(0.1) # Prepare payload for Discord summary = { "content": f"Event {data['type']}: {data.get('data', {}).get('message', 'No message')}" } retry_after = 0 while True: try: await discord_send(summary) break except Exception as e: if "HTTP 429" in str(e): # Extract retry-after import re match = re.search(r"Retry-After (\d+)", str(e)) if match: retry_after = int(match.group(1)) else: retry_after = 1 await asyncio.sleep(retry_after) else: # Log error but don't fail silently print(f"Discord send failed: {e}") break if __name__ == "__main__": # Run tests import unittest import threading import time class TestWebhookBridge(unittest.TestCase): def setUp(self): self.handler = WebhookHandler.__new__(WebhookHandler) self.handler.path = "/webhook" self.handler.headers = {} self.handler.rfile = None self.handler.wfile = None def test_signature_verification(self): body = b'{"event_id": "123", "type": "chat", "data": {}}' signature = hmac.new(SHARED_SECRET, body, hashlib.sha256).hexdigest() self.assertTrue(verify_signature(body, signature)) # Tampered signature self.assertFalse(verify_signature(body, "wrong_signature")) def test_idempotency(self): event_id = "test_event" # First time - should not be idempotent self.assertFalse(is_idempotent(event_id)) # Second time - should be idempotent self.assertTrue(is_idempotent(event_id)) def test_rate_limiting(self): # Fill bucket for _ in range(MAX_DISCORD_RATE): self.assertTrue(rate_limit()) # Next should fail self.assertFalse(rate_limit()) # Run tests unittest.main(argv=[''], exit=False, verbosity=2) # Start HTTP server server = HTTPServer(('localhost', 8080), WebhookHandler) print("Starting server on http://localhost:8080") try: server.serve_forever() except KeyboardInterrupt: print("\nShutting down...")