from __future__ import annotations import hashlib import time from collections import defaultdict WEBHOOK_MAX_BODY_BYTES = 10 * 1024 * 1024 WEBHOOK_RATE_WINDOW_S = 60 WEBHOOK_RATE_MAX_REQUESTS = 1000 _rate_counters: defaultdict[str, list[float]] = defaultdict(list) def verify_feishu_signature(headers: dict, body: bytes, encrypt_key: str = "") -> bool: if len(body) > WEBHOOK_MAX_BODY_BYTES: return False timestamp = headers.get("X-Lark-Request-Timestamp", "") nonce = headers.get("X-Lark-Request-Nonce", "") signature = headers.get("X-Lark-Signature", "") if not timestamp or not nonce or not signature: return False raw = f"{timestamp}{nonce}{encrypt_key}".encode() + body expected = hashlib.sha256(raw).hexdigest() return signature == expected def check_webhook_rate_limit(source_ip: str = "default") -> bool: now = time.monotonic() window = _rate_counters[source_ip] window[:] = [t for t in window if now - t < WEBHOOK_RATE_WINDOW_S] if len(window) >= WEBHOOK_RATE_MAX_REQUESTS: return False window.append(now) return True def decrypt_feishu_body(encrypted_body: bytes, encrypt_key: str) -> bytes | None: if not encrypt_key: return None import base64 try: from Crypto.Cipher import AES from Crypto.Util.Padding import unpad except ImportError: try: from Cryptodome.Cipher import AES from Cryptodome.Util.Padding import unpad except ImportError: return None try: key = hashlib.sha256(encrypt_key.encode()).digest() raw = base64.b64decode(encrypted_body) iv = raw[:16] ciphertext = raw[16:] cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size) return plaintext except Exception: return None def verify_and_decrypt_webhook( headers: dict, body: bytes, encrypt_key: str = "", source_ip: str = "default", ) -> tuple[bool, bytes | None, str]: if not check_webhook_rate_limit(source_ip): return False, None, "rate_limited" if not verify_feishu_signature(headers, body, encrypt_key): return False, None, "signature_mismatch" encrypted = headers.get("X-Lark-Request-Encrypted", "") if encrypted and encrypt_key: decrypted = decrypt_feishu_body(body, encrypt_key) if decrypted is None: return False, None, "decrypt_failed" return True, decrypted, "ok" return True, body, "ok"