测试入站 Webhook 的麻烦程度与其实际问题并不成正比。
一个提供商——Stripe、GitHub、Shopify 等——会将 JSON 正文 POST 到您拥有的 URL。您需要准确看到他们发送的内容,并在修改处理器后再次发送。两者都不难,但通常的设置是:
启动隧道,以便提供商能够访问您的笔记本电脑
将生成的 URL 粘贴到提供商的仪表板中
在他们的 UI 中触发事件
阅读您的应用程序日志,并希望您记录得足够详细
修改您的处理器
返回步骤 3,因为您无法重放已到达的内容
步骤 6 是成本最高的一步。通过第三方的 UI 重新触发真实事件来测试一行代码的更改是一个缓慢的循环,而且有些事件根本不容易按需触发。
您真正需要的部分
剥离后,只有三个要求:
捕获到达的任何内容:方法、路径、标头、正文——不做任何修改
检查它,包括标头(签名通常位于此处)
根据需要多次将其重放到您的处理器
这些都不需要隧道。隧道解决的是不同的可达性问题——来自公共互联网的可达性——一旦捕获了有效负载,您就不再需要提供商再次参与。
使用标准库实现
Python 的 http.server 就足够了。一个捕获所有请求并返回 200 的处理器:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
CAPTURED = []
class Handler(BaseHTTPRequestHandler):
def _capture(self):
length = int(self.headers.get("Content-Length", 0))
CAPTURED.append({
"method": self.command,
"path": self.path,
"headers": dict(self.headers),
"body": self.rfile.read(length).decode("utf-8", "replace"),
})
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"status":"captured"}')
do_POST = do_PUT = do_PATCH = do_GET = do_DELETE = _capture
def log_message(self, *args):
pass
ThreadingHTTPServer(("127.0.0.1", 8000), Handler).serve_forever()
Enter fullscreen mode Exit fullscreen mode
将任何内容指向 http://127.0.0.1:8000/hook/test,它就会被记录。
重放同样简单——获取捕获的请求并将其发送到其他地方:
import json, urllib.request
def replay(captured, target_url):
req = urllib.request.Request(
target_url,
data=captured["body"].encode(),
headers={k: v for k, v in captured["headers"].items()
if k.lower() not in ("host", "content-length")},
method=captured["method"],
)
with urllib.request.urlopen(req, timeout=10) as resp:
return resp.status, resp.read().decode()
Enter fullscreen mode Exit fullscreen mode
删除 Host 和 Content-Length 很重要——两者都引用原始请求,要么是错误的,要么会与 urllib 自己设置的内容冲突。
这就是全部的想法。此后的所有内容都是便利功能:存储多个请求、用于读取请求的 UI、按端点匹配、跨重启持久化。
会咬你的事
重放时签名验证会失败。提供商对原始正文进行签名,通常带有时间戳以防止这种重放。如果您的处理器验证签名,您需要在开发中跳过验证。这是签名方案的属性,而不是捕获工具可以修复的。
只读取正文一次。self.rfile.read() 会消耗流。先将其读取到变量中,然后从该变量进行操作。
不要盲目解码。并非每个 Webhook 正文都是 UTF-8 JSON。errors="replace" 可防止二进制有效负载导致处理器崩溃。
绑定到 127.0.0.1。许多示例代码中的默认值是 0.0.0.0,这会在机器的每个接口上放置一个未经身份验证的端点,该端点会记录完整的请求正文——包括身份验证标头。
如果您不想自己维护
我厌倦了将这些粘贴到项目中,所以我正确地构建了它,并将其作为 HookTrap 出售——49 美元,一次性付费,包含源代码,无订阅。
它与相同的想法相同,但去除了尖锐的边缘:将任何方法捕获到 /hook/{endpoint_id},在 / 处有一个内置的 Web UI,用于读取请求和触发重放,一个 JSON API(GET /api/requests、POST /api/replay/{id}、DELETE /api/requests),以及使用 --file storage.json 的可选持久化。仍然仅使用 Python 3.9+ 标准库——无需 pip install,无需账户,任何东西都不会离开您的机器。
它故意不做的事情:没有自己的 HTTPS(如果需要 TLS,请在代理后面运行它),UI 上没有身份验证,重放同步运行,因此慢速目标会阻塞调用。这些都在您购买前的 README 中,而不是之后。
免责声明:我构建了 HookTrap,它是一个付费产品。本文中的代码仅使用标准库,您可以随意使用——不购买任何东西,文章也能正常工作。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.