Skip to content

Commit 9368d17

Browse files
authored
Merge pull request #295 from cmu-delphi/development
Test logging
2 parents 5868520 + 01a1bb1 commit 9368d17

File tree

3 files changed

+45
-69
lines changed

3 files changed

+45
-69
lines changed

src/epiportal/middleware.py

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from typing import Any
1010

1111
from django.utils.deprecation import MiddlewareMixin
12-
from django.conf import settings
12+
13+
from epiportal.utils import get_client_ip
1314

1415
logger = logging.getLogger("epiportal.requests")
1516

@@ -32,9 +33,9 @@
3233

3334
# Path segments to exclude from request logging (matched anywhere in path)
3435
LOG_EXCLUDE_PATH_PATTERNS = (
35-
"get_table_stats_info",
36-
"get_related_indicators",
37-
"get_available_geos",
36+
# "get_table_stats_info",
37+
# "get_related_indicators",
38+
# "get_available_geos",
3839
)
3940

4041

@@ -44,37 +45,6 @@ def _should_log_request(request) -> bool:
4445
return not any(pattern in path for pattern in LOG_EXCLUDE_PATH_PATTERNS)
4546

4647

47-
def _get_client_ip(req) -> str:
48-
"""Extract client IP, respecting X-Forwarded-For when behind proxies."""
49-
if settings.REVERSE_PROXY_DEPTH:
50-
# we only expect/trust (up to) "REVERSE_PROXY_DEPTH" number of proxies between this server and the outside world.
51-
# a REVERSE_PROXY_DEPTH of 0 means not proxied, i.e. server is globally directly reachable.
52-
# a negative proxy depth is a special case to trust the whole chain -- not generally recommended unless the
53-
# most-external proxy is configured to disregard "X-Forwarded-For" from outside.
54-
# really, ONLY trust the following headers if reverse proxied!!!
55-
x_forwarded_for = req.META.get("HTTP_X_FORWARDED_FOR")
56-
57-
if x_forwarded_for:
58-
full_proxy_chain = x_forwarded_for.split(",")
59-
# eliminate any extra addresses at the front of this list, as they could be spoofed.
60-
if settings.REVERSE_PROXY_DEPTH > 0:
61-
depth = settings.REVERSE_PROXY_DEPTH
62-
else:
63-
# special case for -1/negative: setting `depth` to 0 will not strip any items from the chain
64-
depth = 0
65-
trusted_proxy_chain = full_proxy_chain[-depth:]
66-
# accept the first (or only) address in the remaining trusted part of the chain as the actual remote address
67-
return trusted_proxy_chain[0].strip()
68-
69-
# fall back to "X-Real-Ip" if "X-Forwarded-For" isnt present
70-
x_real_ip = req.META.get("HTTP_X_REAL_IP")
71-
if x_real_ip:
72-
return x_real_ip
73-
74-
# if we are not proxied (or we are proxied but the headers werent present and we fell through to here), just use the remote ip addr as the true client address
75-
return req.META.get("REMOTE_ADDR")
76-
77-
7848
def _sanitize_headers(meta: dict) -> dict[str, str]:
7949
"""Extract HTTP headers from META, redacting sensitive ones."""
8050
headers = {}
@@ -134,7 +104,7 @@ def process_response(self, request, response):
134104
"full_uri": request.build_absolute_uri(),
135105
"query_string": request.META.get("QUERY_STRING") or "",
136106
"query_params": dict(request.GET) if request.GET else {},
137-
"client_ip": _get_client_ip(request),
107+
"client_ip": get_client_ip(request),
138108
"referer": request.META.get("HTTP_REFERER", ""),
139109
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
140110
"content_type": request.content_type or "",

src/epiportal/utils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Shared utilities for epiportal.
3+
"""
4+
5+
from django.conf import settings
6+
7+
8+
def get_client_ip(request) -> str:
9+
"""
10+
Extract the real client IP from a request, respecting X-Forwarded-For when behind proxies.
11+
12+
Use this everywhere you need the client IP (logging, rate limiting, etc.) to ensure
13+
consistent behavior. Requires PROXY_DEPTH to be set correctly for your production
14+
proxy chain (e.g. 2 for AWS ALB + nginx).
15+
"""
16+
if not settings.REVERSE_PROXY_DEPTH:
17+
return request.META.get("REMOTE_ADDR", "")
18+
19+
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
20+
if x_forwarded_for:
21+
full_proxy_chain = [s.strip() for s in x_forwarded_for.split(",")]
22+
depth = (
23+
settings.REVERSE_PROXY_DEPTH
24+
if settings.REVERSE_PROXY_DEPTH > 0
25+
else len(full_proxy_chain)
26+
)
27+
trusted = full_proxy_chain[-depth:] if depth else full_proxy_chain
28+
if trusted:
29+
return trusted[0]
30+
31+
x_real_ip = request.META.get("HTTP_X_REAL_IP")
32+
if x_real_ip:
33+
return x_real_ip.strip()
34+
35+
return request.META.get("REMOTE_ADDR", "")

src/indicatorsets/utils.py

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import requests
88
from django.conf import settings
9-
from django.http import JsonResponse
109
from django.core.cache import cache
10+
from django.http import JsonResponse
11+
from epiportal.utils import get_client_ip
1112
from epiweeks import Week
1213
from delphi_utils import get_structured_logger
1314

@@ -681,36 +682,6 @@ def generate_query_code_flusurv(flusurv_geos, start_date, end_date):
681682
return python_code_blocks, r_code_blocks
682683

683684

684-
def get_real_ip_addr(req): # `req` should be a Flask.request object
685-
if settings.REVERSE_PROXY_DEPTH:
686-
# we only expect/trust (up to) "REVERSE_PROXY_DEPTH" number of proxies between this server and the outside world.
687-
# a REVERSE_PROXY_DEPTH of 0 means not proxied, i.e. server is globally directly reachable.
688-
# a negative proxy depth is a special case to trust the whole chain -- not generally recommended unless the
689-
# most-external proxy is configured to disregard "X-Forwarded-For" from outside.
690-
# really, ONLY trust the following headers if reverse proxied!!!
691-
x_forwarded_for = req.META.get("HTTP_X_FORWARDED_FOR")
692-
693-
if x_forwarded_for:
694-
full_proxy_chain = x_forwarded_for.split(",")
695-
# eliminate any extra addresses at the front of this list, as they could be spoofed.
696-
if settings.REVERSE_PROXY_DEPTH > 0:
697-
depth = settings.REVERSE_PROXY_DEPTH
698-
else:
699-
# special case for -1/negative: setting `depth` to 0 will not strip any items from the chain
700-
depth = 0
701-
trusted_proxy_chain = full_proxy_chain[-depth:]
702-
# accept the first (or only) address in the remaining trusted part of the chain as the actual remote address
703-
return trusted_proxy_chain[0].strip()
704-
705-
# fall back to "X-Real-Ip" if "X-Forwarded-For" isnt present
706-
x_real_ip = req.META.get("HTTP_X_REAL_IP")
707-
if x_real_ip:
708-
return x_real_ip
709-
710-
# if we are not proxied (or we are proxied but the headers werent present and we fell through to here), just use the remote ip addr as the true client address
711-
return req.META.get("REMOTE_ADDR")
712-
713-
714685
def log_form_stats(request, data, form_mode):
715686
log_data = {
716687
"form_mode": form_mode,
@@ -729,7 +700,7 @@ def log_form_stats(request, data, form_mode):
729700
),
730701
"api_key_used": bool(data.get("api_key")),
731702
"api_key": data.get("api_key", "Not provided"),
732-
"user_ip": get_real_ip_addr(request),
703+
"user_ip": get_client_ip(request),
733704
"user_ga_id": data.get("clientId", "Not available"),
734705
}
735706

@@ -805,7 +776,7 @@ def log_form_data(request, data, form_mode):
805776
"epiweeks": get_epiweek(data.get("start_date", ""), data.get("end_date", "")) if data.get("start_date") and data.get("end_date") else [], # fmt: skip
806777
"api_key_used": bool(data.get("apiKey")),
807778
"api_key": data.get("apiKey", "Not provided"),
808-
"user_ip": get_real_ip_addr(request),
779+
"user_ip": get_client_ip(request),
809780
"user_ga_id": data.get("clientId", "Not available"),
810781
}
811782
form_data_logger.info("form_data", **log_data)

0 commit comments

Comments
 (0)