mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Trust Cloudflare headers for YooKassa webhooks
This commit is contained in:
30
app/external/yookassa_webhook.py
vendored
30
app/external/yookassa_webhook.py
vendored
@@ -33,6 +33,32 @@ YOOKASSA_ALLOWED_IP_NETWORKS: tuple[IPNetwork, ...] = (
|
||||
)
|
||||
|
||||
|
||||
CLOUDFLARE_TRUSTED_NETWORKS: tuple[IPNetwork, ...] = (
|
||||
ip_network("173.245.48.0/20"),
|
||||
ip_network("103.21.244.0/22"),
|
||||
ip_network("103.22.200.0/22"),
|
||||
ip_network("103.31.4.0/22"),
|
||||
ip_network("141.101.64.0/18"),
|
||||
ip_network("108.162.192.0/18"),
|
||||
ip_network("190.93.240.0/20"),
|
||||
ip_network("188.114.96.0/20"),
|
||||
ip_network("197.234.240.0/22"),
|
||||
ip_network("198.41.128.0/17"),
|
||||
ip_network("162.158.0.0/15"),
|
||||
ip_network("104.16.0.0/13"),
|
||||
ip_network("104.24.0.0/14"),
|
||||
ip_network("172.64.0.0/13"),
|
||||
ip_network("131.0.72.0/22"),
|
||||
ip_network("2400:cb00::/32"),
|
||||
ip_network("2606:4700::/32"),
|
||||
ip_network("2803:f800::/32"),
|
||||
ip_network("2405:b500::/32"),
|
||||
ip_network("2405:8100::/32"),
|
||||
ip_network("2a06:98c0::/29"),
|
||||
ip_network("2c0f:f248::/32"),
|
||||
)
|
||||
|
||||
|
||||
YOOKASSA_ALLOWED_EVENTS: tuple[str, ...] = (
|
||||
"payment.succeeded",
|
||||
"payment.waiting_for_capture",
|
||||
@@ -122,6 +148,9 @@ def _is_trusted_proxy_ip(ip_object: IPAddress) -> bool:
|
||||
):
|
||||
return True
|
||||
|
||||
if any(ip_object in network for network in CLOUDFLARE_TRUSTED_NETWORKS):
|
||||
return True
|
||||
|
||||
return any(ip_object in network for network in _get_trusted_proxy_networks())
|
||||
|
||||
|
||||
@@ -180,6 +209,7 @@ class YooKassaWebhookHandler:
|
||||
header_ip_candidates = collect_yookassa_ip_candidates(
|
||||
request.headers.get("X-Forwarded-For"),
|
||||
request.headers.get("X-Real-IP"),
|
||||
request.headers.get("Cf-Connecting-Ip"),
|
||||
)
|
||||
client_ip = resolve_yookassa_ip(
|
||||
header_ip_candidates,
|
||||
|
||||
@@ -349,6 +349,7 @@ def create_payment_router(bot: Bot, payment_service: PaymentService) -> APIRoute
|
||||
header_ip_candidates = yookassa_webhook_module.collect_yookassa_ip_candidates(
|
||||
request.headers.get("X-Forwarded-For"),
|
||||
request.headers.get("X-Real-IP"),
|
||||
request.headers.get("Cf-Connecting-Ip"),
|
||||
)
|
||||
remote_ip = request.client.host if request.client else None
|
||||
client_ip = yookassa_webhook_module.resolve_yookassa_ip(
|
||||
|
||||
27
tests/external/test_yookassa_webhook.py
vendored
27
tests/external/test_yookassa_webhook.py
vendored
@@ -32,6 +32,7 @@ def _build_headers(**overrides: str) -> dict[str, str]:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Forwarded-For": ALLOWED_IP,
|
||||
"Cf-Connecting-Ip": ALLOWED_IP,
|
||||
}
|
||||
headers.update(overrides)
|
||||
return headers
|
||||
@@ -138,6 +139,32 @@ async def test_handle_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_webhook_trusts_cf_connecting_ip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_patch_get_db(monkeypatch)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
app = create_yookassa_webhook_app(service)
|
||||
async with TestClient(TestServer(app)) as client:
|
||||
payload = {"event": "payment.succeeded"}
|
||||
body = json.dumps(payload, ensure_ascii=False)
|
||||
headers = _build_headers()
|
||||
headers.pop("X-Forwarded-For")
|
||||
response = await client.post(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
data=body.encode("utf-8"),
|
||||
headers=headers,
|
||||
)
|
||||
status = response.status
|
||||
text = await response.text()
|
||||
|
||||
assert status == 200
|
||||
assert text == "OK"
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_webhook_with_optional_signature(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_patch_get_db(monkeypatch)
|
||||
|
||||
@@ -263,6 +263,37 @@ async def test_yookassa_allowed_via_forwarded_header_when_proxy(monkeypatch: pyt
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_allowed_via_cf_connecting_ip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
async def fake_get_db():
|
||||
yield SimpleNamespace()
|
||||
|
||||
monkeypatch.setattr("app.webserver.payments.get_db", fake_get_db)
|
||||
|
||||
process_mock = AsyncMock(return_value=True)
|
||||
service = SimpleNamespace(process_yookassa_webhook=process_mock)
|
||||
|
||||
router = create_payment_router(DummyBot(), service)
|
||||
assert router is not None
|
||||
|
||||
route = _get_route(router, settings.YOOKASSA_WEBHOOK_PATH)
|
||||
request = _build_request(
|
||||
settings.YOOKASSA_WEBHOOK_PATH,
|
||||
body=json.dumps({"event": "payment.succeeded"}).encode("utf-8"),
|
||||
headers={"Cf-Connecting-Ip": "185.71.76.10"},
|
||||
client_ip="172.64.223.133",
|
||||
)
|
||||
|
||||
response = await route.endpoint(request)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.body.decode("utf-8"))
|
||||
assert payload["status"] == "ok"
|
||||
process_mock.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_yookassa_allowed_via_trusted_forwarded_chain(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "YOOKASSA_ENABLED", True, raising=False)
|
||||
|
||||
Reference in New Issue
Block a user