통합과 배포 · 우리가 받는 쪽일 때 · 실습
주문 상태가 가끔 뒤로 간다 — 받는 쪽을 만든다
목표
남의 시스템이 밀어 넣는 웹훅을 받는 쪽을 직접 만든다. HMAC 서명 검증과 상수시간 비교, 재생을 막는 시각 창, 배달 번호와 이벤트 번호 두 열쇠의 중복 제거, 판 비교로 순서 뒤집힘 흡수, 그리고 빨리 200 을 주고 나중에 처리하는 구조까지 붙이고, 하루치 배달을 다시 흘려보내 각 갈래를 집계한다.
왜 중요한가
웹훅은 우리가 부르는 것이 아니라 받는 것이라 통제권이 반대편에 있다. 수신 종단은 열려 있어야 하므로 아무나 POST 할 수 있고, 우리 응답이 늦으면 상대는 재전송하고, 순서는 보장되지 않는다.
그래서 받는 쪽은 네 가지를 스스로 판단해야 한다. 누가 보냈는가(서명), 언제 보낸 것인가(시각 창), 이미 본 것인가(중복), 지금 것보다 새것인가(판). 이 중 하나라도 빠지면 가짜 이벤트가 원장에 들어가거나 주문 상태가 뒤로 간다.
중복의 열쇠가 둘이라는 점이 특히 함정이다. 배달 번호는 한 번의 전송을, 이벤트 번호는 벌어진 사건 하나를 가리킨다. 막아야 하는 것은 사건의 중복 적용이므로 배달 번호만으로 거르면 절반만 막힌다.
채점기는 여러분의 문장을 믿지 않는다. 여러분이 만든 발신기와 수신기를 채점기가 고른 포트에 직접 띄우고, 채점기가 만든 비밀과 배달로 검증기·원장을 다시 실행해 답을 맞춰 본다.
단계
1. /root/wh/sender.py 를 만들어 포트 8012 에 띄우고, /deliveries 를 /root/wh/deliveries.json 에, 공유 비밀을 /root/wh/secret.txt 에 저장하세요.
2. /root/wh/verify.py 를 만들어 서명을 상수시간으로 비교하고 {"ok": ..., "reason": ...} 를 내게 하세요.
3. verify.py 에 시각 창을 붙여 창 밖의 배달을 stale 로 거절하게 하세요.
4. /root/wh/ledger.py 를 만들어 배달 번호와 이벤트 번호 두 열쇠로 중복을 거르게 하세요.
5. ledger.py 가 지금 저장된 판보다 새것일 때만 적용하고, 뒤늦게 온 옛 판은 stale_version 으로 남기게 하세요.
6. /root/wh/receiver.py 를 만들어 검증만 하고 큐에 넣은 뒤 곧바로 200 을 주게 하고, --drain 으로 큐를 원장에 흘려보내게 하세요.
7. /root/wh/replay_day.py 로 하루치 배달을 모두 흘려보내 /root/wh/day.db 와 /root/wh/result.json 을 만드세요.
8. /root/wh/wh_report.md 에 네 절로 보고하세요.
참고
- 발신기 실행 계약:
python3 /root/wh/sender.py --port <포트> [--secret <비밀>]./health는{"ok": true, "events": 30, "deliveries": 41},/deliveries는{"now": <기준 시각>, "tolerance": 300, "deliveries": [...]}를 냅니다. 배달 하나는{"delivery_id": ..., "signature": ..., "body": <원본 문자열>}입니다. - 서명 형식:
t=<epoch>,v1=<hex>. 서명 재료는"<t>.<body>"이고 HMAC-SHA256 입니다. body 는 받은 문자열 그대로 씁니다. 다시 파싱해 직렬화하면 서명이 어긋납니다. - 이 하루치는 사건 30건(주문 10건 × 판 3개)에 재전송 4건, 같은 사건의 새 배달 3건, 오래된 재생 2건, 가짜 서명 2건을 더한 41건입니다. 판이 도착하는 순서는 주문마다 다릅니다.
- 검증기 실행 계약:
python3 verify.py --secret <파일> --delivery <파일> [--now <epoch>] [--tolerance <초>]는{"ok": true|false, "reason": "ok"|"bad_signature"|"stale"|"malformed"}를 냅니다.--now를 안 주면 지금 시각을 씁니다. 2번 단계에서도 네 인자를 모두 받아 두세요(창은 3번에서 붙입니다). 서명 모양이 아니면 malformed, 서명이 틀리면 bad_signature, 서명은 맞고 시각이 창 밖이면 stale 입니다. - 원장 실행 계약:
python3 ledger.py --db <sqlite> --delivery <파일>은{"stored": ..., "applied": ..., "reason": ...}를 냅니다. reason 은 new · duplicate_delivery · duplicate_event · stale_version 입니다. 표는order_state(order_id, version, status)를 반드시 포함합니다. - 수신 종단 실행 계약:
python3 receiver.py --port <포트> --db <sqlite> --secret <파일> [--tolerance <초>]는GET /health와POST /webhook을 냅니다. 배달 번호는X-Delivery-Id, 서명은X-Signature헤더로 옵니다. 통과하면 200{"queued": true}, 서명이나 시각 창에서 걸리면 400 입니다.--drain을 주면 서버를 띄우지 않고 큐를 원장에 흘려보낸 뒤 집계를 냅니다. 큐 표 이름은inbox입니다. - 재현기 실행 계약:
python3 replay_day.py --deliveries <파일> --db <sqlite> --out <파일>. 집계 칸은 deliveries · accepted · rejected_signature · rejected_stale · duplicate_delivery · duplicate_event · stored · applied · stale_version · orders 열 개입니다.accepted는 서명과 시각 창을 통과한 배달,stored는 중복이 아니어서 원장에 들어간 사건의 수입니다. - 흔한 실수: 본문을 파싱했다가 다시 직렬화해 서명 계산하기,
==로 서명 비교하기, 배달 번호로만 중복 거르기, 받는 자리에서 원장까지 처리해 200 이 늦어지기. - 서버는 백그라운드로 띄우고
/health가 200 이 될 때까지 기다린 뒤 다음으로 갑니다. 채점기는 여러분이 띄워 둔 프로세스를 보지 않고 스크립트를 직접 다시 띄웁니다.
단계 8개
- 하루치 배달 손에 쥐기
- 누가 보냈는지 서명으로 가리기
- 오래된 것을 다시 밀어 넣는 손
- 열쇠가 둘이다
- 주문 상태가 뒤로 가지 않게
- 빨리 200 을 주고 나중에 처리하기
- 하루치를 하나도 빼지 않고 다시
- 받는 쪽 점검 보고서