Log trusted forwarded client IPs
This commit is contained in:
@@ -1,13 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import functools
|
||||||
import hmac
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp.web_log import AccessLogger, KeyMethod
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from bot.payment_providers import iter_provider_specs, iter_service_keys
|
from bot.payment_providers import iter_provider_specs, iter_service_keys
|
||||||
|
from bot.utils.request_security import request_client_ip
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +21,39 @@ class SecureSimpleRequestHandler(SimpleRequestHandler):
|
|||||||
return hmac.compare_digest(telegram_secret_token, self.secret_token)
|
return hmac.compare_digest(telegram_secret_token, self.secret_token)
|
||||||
|
|
||||||
|
|
||||||
|
class TrustedProxyAccessLogger(AccessLogger):
|
||||||
|
"""Aiohttp access logger that respects trusted X-Forwarded-For headers."""
|
||||||
|
|
||||||
|
def compile_format(self, log_format):
|
||||||
|
methods = []
|
||||||
|
for atom in self.FORMAT_RE.findall(log_format):
|
||||||
|
if atom[1] == "":
|
||||||
|
format_key = self.LOG_FORMAT_MAP[atom[0]]
|
||||||
|
method = getattr(type(self), f"_format_{atom[0]}", None)
|
||||||
|
if method is None:
|
||||||
|
method = getattr(AccessLogger, f"_format_{atom[0]}")
|
||||||
|
methods.append(KeyMethod(format_key, method))
|
||||||
|
else:
|
||||||
|
format_key = (self.LOG_FORMAT_MAP[atom[2]], atom[1])
|
||||||
|
method = getattr(type(self), f"_format_{atom[2]}", None)
|
||||||
|
if method is None:
|
||||||
|
method = getattr(AccessLogger, f"_format_{atom[2]}")
|
||||||
|
methods.append(KeyMethod(format_key, functools.partial(method, atom[1])))
|
||||||
|
|
||||||
|
compiled = self.FORMAT_RE.sub(r"%s", log_format)
|
||||||
|
compiled = self.CLEANUP_RE.sub(r"%\1", compiled)
|
||||||
|
return compiled, methods
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_a(request, response, time):
|
||||||
|
if request is None:
|
||||||
|
return "-"
|
||||||
|
settings = request.app.get("settings") if hasattr(request, "app") else None
|
||||||
|
trusted_proxies = getattr(settings, "trusted_proxies", None)
|
||||||
|
client_ip = request_client_ip(request, trusted_proxies=trusted_proxies)
|
||||||
|
return client_ip or "-"
|
||||||
|
|
||||||
|
|
||||||
def _inject_shared_instances(
|
def _inject_shared_instances(
|
||||||
app: web.Application,
|
app: web.Application,
|
||||||
dp: Dispatcher,
|
dp: Dispatcher,
|
||||||
@@ -110,7 +146,7 @@ async def build_and_start_web_app(
|
|||||||
|
|
||||||
runners = []
|
runners = []
|
||||||
|
|
||||||
webhooks_runner = web.AppRunner(app)
|
webhooks_runner = web.AppRunner(app, access_log_class=TrustedProxyAccessLogger)
|
||||||
await webhooks_runner.setup()
|
await webhooks_runner.setup()
|
||||||
runners.append(webhooks_runner)
|
runners.append(webhooks_runner)
|
||||||
site = web.TCPSite(
|
site = web.TCPSite(
|
||||||
@@ -133,7 +169,10 @@ async def build_and_start_web_app(
|
|||||||
settings,
|
settings,
|
||||||
async_session_factory,
|
async_session_factory,
|
||||||
)
|
)
|
||||||
subscription_runner = web.AppRunner(subscription_app)
|
subscription_runner = web.AppRunner(
|
||||||
|
subscription_app,
|
||||||
|
access_log_class=TrustedProxyAccessLogger,
|
||||||
|
)
|
||||||
await subscription_runner.setup()
|
await subscription_runner.setup()
|
||||||
runners.append(subscription_runner)
|
runners.append(subscription_runner)
|
||||||
subscription_site = web.TCPSite(
|
subscription_site = web.TCPSite(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from aiohttp import web
|
|||||||
|
|
||||||
from bot.app.web import admin_api, subscription_webapp
|
from bot.app.web import admin_api, subscription_webapp
|
||||||
from bot.app.web.admin_api_impl import settings as admin_settings_routes
|
from bot.app.web.admin_api_impl import settings as admin_settings_routes
|
||||||
|
from bot.app.web.web_server import TrustedProxyAccessLogger
|
||||||
from bot.app.web.webapp import account as account_routes
|
from bot.app.web.webapp import account as account_routes
|
||||||
from bot.app.web.webapp_auth import (
|
from bot.app.web.webapp_auth import (
|
||||||
create_telegram_oauth_nonce,
|
create_telegram_oauth_nonce,
|
||||||
@@ -47,6 +48,27 @@ class RequestSecurityTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"198.51.100.7",
|
"198.51.100.7",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def test_access_logger_uses_forwarded_ip_only_for_trusted_proxy(self):
|
||||||
|
trusted_request = SimpleNamespace(
|
||||||
|
remote="172.19.0.7",
|
||||||
|
headers={"X-Forwarded-For": "203.0.113.10"},
|
||||||
|
app={"settings": SimpleNamespace(trusted_proxies=["172.19.0.0/16"])},
|
||||||
|
)
|
||||||
|
untrusted_request = SimpleNamespace(
|
||||||
|
remote="172.19.0.7",
|
||||||
|
headers={"X-Forwarded-For": "203.0.113.10"},
|
||||||
|
app={"settings": SimpleNamespace(trusted_proxies=["127.0.0.1"])},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
TrustedProxyAccessLogger._format_a(trusted_request, object(), 0),
|
||||||
|
"203.0.113.10",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
TrustedProxyAccessLogger._format_a(untrusted_request, object(), 0),
|
||||||
|
"172.19.0.7",
|
||||||
|
)
|
||||||
|
|
||||||
async def test_yookassa_webhook_rejects_untrusted_ip_before_reading_body(self):
|
async def test_yookassa_webhook_rejects_untrusted_ip_before_reading_body(self):
|
||||||
request = SimpleNamespace(
|
request = SimpleNamespace(
|
||||||
app={
|
app={
|
||||||
|
|||||||
Reference in New Issue
Block a user