devlogs 2.3.2__tar.gz → 2.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devlogs-2.3.2/src/devlogs.egg-info → devlogs-2.3.5}/PKG-INFO +1 -1
- {devlogs-2.3.2 → devlogs-2.3.5}/pyproject.toml +1 -1
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/_version_static.py +1 -1
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/cli.py +14 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/server.py +24 -21
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/config.py +18 -2
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/web/server.py +4 -2
- {devlogs-2.3.2 → devlogs-2.3.5/src/devlogs.egg-info}/PKG-INFO +1 -1
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_cli.py +22 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_collector_server.py +58 -5
- {devlogs-2.3.2 → devlogs-2.3.5}/LICENSE +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/MANIFEST.in +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/README.md +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/setup.cfg +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/__main__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/build_info.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/auth.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/cli.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/errors.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/forwarder.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/ingestor.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/loki_plugin.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/plugins.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/collector/schema.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/context.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/demo.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/devlogs_client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/formatting.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/handler.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/jenkins/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/jenkins/cli.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/jenkins/core.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/levels.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/loki/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/loki/queries.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/mcp/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/mcp/server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/opensearch/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/opensearch/client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/opensearch/indexing.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/opensearch/mappings.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/opensearch/queries.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/proxy/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/proxy/server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/retention.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/scrub.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/time_utils.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/version.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/web/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/web/static/devlogs.css +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/web/static/devlogs.js +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/web/static/index.html +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs/wrapper.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs.egg-info/SOURCES.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs.egg-info/dependency_links.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs.egg-info/entry_points.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs.egg-info/requires.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/src/devlogs.egg-info/top_level.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_build_info.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_collector_auth.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_collector_config.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_collector_plugins.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_collector_schema.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_config.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_context.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_devlogs_client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_formatting.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_handler.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_indexing.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_levels.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_mappings.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_mcp_server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_opensearch_client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_opensearch_queries.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_proxy_server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_retention.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_scrub.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_time_utils.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_url_parsing.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.5}/tests/test_web.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# AUTO-GENERATED at build time — do not edit or commit
|
|
2
|
-
__version__ = "2.3.
|
|
2
|
+
__version__ = "2.3.5"
|
|
@@ -926,6 +926,13 @@ def tail(
|
|
|
926
926
|
_apply_common_options(env, url)
|
|
927
927
|
|
|
928
928
|
cfg = load_config()
|
|
929
|
+
if cfg.url_mode == "collector":
|
|
930
|
+
typer.echo(typer.style(
|
|
931
|
+
"Error: the provided URL is a collector (ingest) endpoint and cannot be used for querying.\n"
|
|
932
|
+
"For Loki backends, use lokis://TOKEN@host/path instead of https://TOKEN@host/path",
|
|
933
|
+
fg=typer.colors.RED
|
|
934
|
+
), err=True)
|
|
935
|
+
raise typer.Exit(1)
|
|
929
936
|
if cfg.is_loki:
|
|
930
937
|
_tail_loki(cfg, application=application, operation_id=operation_id, area=area,
|
|
931
938
|
component=component, level=level, since=since, limit=limit, follow=follow,
|
|
@@ -1115,6 +1122,13 @@ def search(
|
|
|
1115
1122
|
_apply_common_options(env, url)
|
|
1116
1123
|
|
|
1117
1124
|
cfg = load_config()
|
|
1125
|
+
if cfg.url_mode == "collector":
|
|
1126
|
+
typer.echo(typer.style(
|
|
1127
|
+
"Error: the provided URL is a collector (ingest) endpoint and cannot be used for querying.\n"
|
|
1128
|
+
"For Loki backends, use lokis://TOKEN@host/path instead of https://TOKEN@host/path",
|
|
1129
|
+
fg=typer.colors.RED
|
|
1130
|
+
), err=True)
|
|
1131
|
+
raise typer.Exit(1)
|
|
1118
1132
|
if cfg.is_loki:
|
|
1119
1133
|
_search_loki(cfg, q=q, application=application, area=area,
|
|
1120
1134
|
component=component, level=level, operation_id=operation_id,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# - Forward mode: proxy to upstream collector
|
|
5
5
|
# - Ingest mode: write directly to OpenSearch
|
|
6
6
|
|
|
7
|
+
import hmac
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
9
10
|
import platform
|
|
@@ -127,27 +128,28 @@ app.add_middleware(
|
|
|
127
128
|
)
|
|
128
129
|
|
|
129
130
|
|
|
130
|
-
def get_client_ip(request: Request) -> str:
|
|
131
|
+
def get_client_ip(request: Request, trusted_proxy_token: str = "") -> str:
|
|
131
132
|
"""Extract client IP from request.
|
|
132
133
|
|
|
133
134
|
Returns a bare IPv4 or IPv6 address string (e.g. "192.168.1.5", "::1").
|
|
134
135
|
No port, no brackets, no CIDR suffix.
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
Proxy headers (X-Forwarded-For, X-Real-IP) are only honored when
|
|
138
|
+
DEVLOGS_TRUSTED_PROXY_TOKEN is configured and the request presents
|
|
139
|
+
a matching X-Trusted-Proxy-Token header. This prevents clients from
|
|
140
|
+
spoofing their IP address.
|
|
138
141
|
"""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# Fall back to direct client
|
|
142
|
+
if trusted_proxy_token:
|
|
143
|
+
presented = request.headers.get("X-Trusted-Proxy-Token", "")
|
|
144
|
+
if presented and hmac.compare_digest(presented, trusted_proxy_token):
|
|
145
|
+
forwarded_for = request.headers.get("X-Forwarded-For")
|
|
146
|
+
if forwarded_for:
|
|
147
|
+
return forwarded_for.split(",")[0].strip()
|
|
148
|
+
|
|
149
|
+
real_ip = request.headers.get("X-Real-IP")
|
|
150
|
+
if real_ip:
|
|
151
|
+
return real_ip.strip()
|
|
152
|
+
|
|
151
153
|
if request.client:
|
|
152
154
|
return request.client.host
|
|
153
155
|
|
|
@@ -157,7 +159,8 @@ def get_client_ip(request: Request) -> str:
|
|
|
157
159
|
@app.exception_handler(CollectorError)
|
|
158
160
|
async def collector_error_handler(request: Request, exc: CollectorError):
|
|
159
161
|
"""Handle CollectorError exceptions with structured response."""
|
|
160
|
-
|
|
162
|
+
cfg = load_config()
|
|
163
|
+
client_ip = get_client_ip(request, cfg.trusted_proxy_token)
|
|
161
164
|
logger.warning("%s %d %s: %s", client_ip, exc.status_code, exc.subcode, exc.message)
|
|
162
165
|
return JSONResponse(
|
|
163
166
|
status_code=exc.status_code,
|
|
@@ -202,7 +205,7 @@ async def ingest_logs(request: Request):
|
|
|
202
205
|
|
|
203
206
|
# Determine operating mode
|
|
204
207
|
mode = cfg.get_collector_mode()
|
|
205
|
-
client_ip = get_client_ip(request)
|
|
208
|
+
client_ip = get_client_ip(request, cfg.trusted_proxy_token)
|
|
206
209
|
logger.info("%s POST / (%d bytes)", client_ip, len(body))
|
|
207
210
|
|
|
208
211
|
if mode == "forward":
|
|
@@ -240,7 +243,7 @@ async def _handle_forward_mode(request: Request, cfg, body: bytes) -> Response:
|
|
|
240
243
|
timeout=cfg.opensearch_timeout,
|
|
241
244
|
)
|
|
242
245
|
|
|
243
|
-
client_ip = get_client_ip(request)
|
|
246
|
+
client_ip = get_client_ip(request, cfg.trusted_proxy_token)
|
|
244
247
|
|
|
245
248
|
# If upstream returned 2xx, return 202
|
|
246
249
|
if 200 <= status < 300:
|
|
@@ -297,7 +300,7 @@ def _validate_and_enrich_records(request: Request, cfg, body: bytes):
|
|
|
297
300
|
raise
|
|
298
301
|
|
|
299
302
|
# Get client info
|
|
300
|
-
client_ip = get_client_ip(request)
|
|
303
|
+
client_ip = get_client_ip(request, cfg.trusted_proxy_token)
|
|
301
304
|
|
|
302
305
|
# Extract token (precedence: Bearer header → X-Devlogs-Token → URL userinfo → ?token=)
|
|
303
306
|
authorization = request.headers.get("Authorization")
|
|
@@ -342,7 +345,7 @@ async def _handle_ingest_mode(request: Request, cfg, body: bytes) -> Response:
|
|
|
342
345
|
index_map = parse_forward_index_map_kv(cfg.forward_index_map_kv)
|
|
343
346
|
|
|
344
347
|
result = ingest_records(client, cfg.index, enriched_records, index_map)
|
|
345
|
-
client_ip = get_client_ip(request)
|
|
348
|
+
client_ip = get_client_ip(request, cfg.trusted_proxy_token)
|
|
346
349
|
logger.info("%s 202 ingested %d record(s)", client_ip, result["ingested"])
|
|
347
350
|
|
|
348
351
|
return Response(
|
|
@@ -377,7 +380,7 @@ async def _handle_plugin_mode(request: Request, cfg, body: bytes, plugin) -> Res
|
|
|
377
380
|
result = {}
|
|
378
381
|
|
|
379
382
|
ingested = result.get("ingested", len(enriched_records))
|
|
380
|
-
client_ip = get_client_ip(request)
|
|
383
|
+
client_ip = get_client_ip(request, cfg.trusted_proxy_token)
|
|
381
384
|
logger.info("%s 202 plugin '%s' ingested %d record(s)", client_ip, plugin.name, ingested)
|
|
382
385
|
|
|
383
386
|
return Response(
|
|
@@ -49,6 +49,9 @@ _DEVLOGS_CONFIG_KEYS = (
|
|
|
49
49
|
"DEVLOGS_TOKEN_MAP_KV",
|
|
50
50
|
# Legacy: Auth token header name (default: Authorization)
|
|
51
51
|
"DEVLOGS_AUTH_HEADER",
|
|
52
|
+
# Proxy trust: shared secret that a reverse proxy must present to have
|
|
53
|
+
# X-Forwarded-For / X-Real-IP headers honored
|
|
54
|
+
"DEVLOGS_TRUSTED_PROXY_TOKEN",
|
|
52
55
|
)
|
|
53
56
|
|
|
54
57
|
|
|
@@ -411,8 +414,9 @@ class DevlogsConfig:
|
|
|
411
414
|
# Treat unparseable DEVLOGS_URL as legacy OpenSearch
|
|
412
415
|
opensearch_url_config = _parse_opensearch_url(devlogs_url)
|
|
413
416
|
|
|
414
|
-
# Legacy: DEVLOGS_OPENSEARCH_URL overrides OpenSearch settings from DEVLOGS_URL
|
|
415
|
-
|
|
417
|
+
# Legacy: DEVLOGS_OPENSEARCH_URL overrides OpenSearch settings from DEVLOGS_URL,
|
|
418
|
+
# but NOT when the URL was explicitly set via --url flag.
|
|
419
|
+
if legacy_opensearch_url and not _url_set_explicitly:
|
|
416
420
|
opensearch_url_config = _parse_opensearch_url(legacy_opensearch_url)
|
|
417
421
|
# Parse application filter from opensearch:// URL (second path segment)
|
|
418
422
|
if legacy_opensearch_url.startswith("opensearchs://") or legacy_opensearch_url.startswith("opensearch://"):
|
|
@@ -465,6 +469,11 @@ class DevlogsConfig:
|
|
|
465
469
|
# Token-to-identity mapping (KV format)
|
|
466
470
|
self.token_map_kv = _getenv("DEVLOGS_TOKEN_MAP_KV", "")
|
|
467
471
|
|
|
472
|
+
# Trusted proxy token: if set, X-Forwarded-For / X-Real-IP headers are
|
|
473
|
+
# only honored when the request includes this token in X-Trusted-Proxy-Token.
|
|
474
|
+
# If empty, forwarded headers are never trusted.
|
|
475
|
+
self.trusted_proxy_token = _getenv("DEVLOGS_TRUSTED_PROXY_TOKEN", "")
|
|
476
|
+
|
|
468
477
|
# Forward mode: per-application index routing (KV format)
|
|
469
478
|
self.forward_index_map_kv = _getenv("DEVLOGS_FORWARD_INDEX_MAP_KV", "")
|
|
470
479
|
|
|
@@ -531,15 +540,22 @@ def set_dotenv_path(path: str):
|
|
|
531
540
|
_dotenv_loaded = False # Reset to force reload with new path
|
|
532
541
|
|
|
533
542
|
|
|
543
|
+
_url_set_explicitly = False
|
|
544
|
+
|
|
534
545
|
def set_url(url: str):
|
|
535
546
|
"""Set the URL, auto-detecting whether it's a collector or OpenSearch URL.
|
|
536
547
|
|
|
537
548
|
Uses parse_url() to detect the URL type:
|
|
538
549
|
- Collector URLs → sets DEVLOGS_URL
|
|
539
550
|
- OpenSearch URLs → sets DEVLOGS_OPENSEARCH_URL
|
|
551
|
+
|
|
552
|
+
When called (i.e. via --url flag), marks the URL as explicitly set so that
|
|
553
|
+
dotenv values cannot silently override it.
|
|
540
554
|
"""
|
|
555
|
+
global _url_set_explicitly
|
|
541
556
|
if not url:
|
|
542
557
|
return
|
|
558
|
+
_url_set_explicitly = True
|
|
543
559
|
try:
|
|
544
560
|
parsed = parse_url(url)
|
|
545
561
|
except URLParseError:
|
|
@@ -78,8 +78,10 @@ def tail(operation_id: Optional[str] = None, area: Optional[str] = None, compone
|
|
|
78
78
|
|
|
79
79
|
@app.get("/ui/{path:path}")
|
|
80
80
|
def serve_ui(path: str):
|
|
81
|
-
static_dir = os.path.join(os.path.dirname(__file__), "static")
|
|
82
|
-
file_path = os.path.join(static_dir, path)
|
|
81
|
+
static_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "static"))
|
|
82
|
+
file_path = os.path.realpath(os.path.join(static_dir, path))
|
|
83
|
+
if not file_path.startswith(static_dir + os.sep) and file_path != static_dir:
|
|
84
|
+
file_path = os.path.join(static_dir, "index.html")
|
|
83
85
|
if not os.path.isfile(file_path):
|
|
84
86
|
file_path = os.path.join(static_dir, "index.html")
|
|
85
87
|
return FileResponse(file_path)
|
|
@@ -919,3 +919,25 @@ class TestLokiCLI:
|
|
|
919
919
|
result = runner.invoke(cli.app, ["--url", "lokis://token@host.example.io/query", "last-error"])
|
|
920
920
|
assert result.exit_code == 1
|
|
921
921
|
assert "--application is required" in result.output
|
|
922
|
+
|
|
923
|
+
def test_tail_rejects_collector_url(self, monkeypatch):
|
|
924
|
+
"""Test that tail fails with a clear error for collector (https) URLs."""
|
|
925
|
+
monkeypatch.setattr(config, "_dotenv_loaded", True)
|
|
926
|
+
monkeypatch.delenv("DEVLOGS_OPENSEARCH_URL", raising=False)
|
|
927
|
+
monkeypatch.delenv("DEVLOGS_OPENSEARCH_HOST", raising=False)
|
|
928
|
+
runner = CliRunner()
|
|
929
|
+
result = runner.invoke(cli.app, ["--url", "https://token@host.example.io/ingest", "tail"])
|
|
930
|
+
assert result.exit_code == 1
|
|
931
|
+
assert "collector" in result.output
|
|
932
|
+
assert "lokis://" in result.output
|
|
933
|
+
|
|
934
|
+
def test_search_rejects_collector_url(self, monkeypatch):
|
|
935
|
+
"""Test that search fails with a clear error for collector (https) URLs."""
|
|
936
|
+
monkeypatch.setattr(config, "_dotenv_loaded", True)
|
|
937
|
+
monkeypatch.delenv("DEVLOGS_OPENSEARCH_URL", raising=False)
|
|
938
|
+
monkeypatch.delenv("DEVLOGS_OPENSEARCH_HOST", raising=False)
|
|
939
|
+
runner = CliRunner()
|
|
940
|
+
result = runner.invoke(cli.app, ["--url", "https://token@host.example.io/ingest", "search"])
|
|
941
|
+
assert result.exit_code == 1
|
|
942
|
+
assert "collector" in result.output
|
|
943
|
+
assert "lokis://" in result.output
|
|
@@ -166,7 +166,7 @@ class TestIngestEndpoint:
|
|
|
166
166
|
# TestClient uses testclient as host
|
|
167
167
|
assert body["client_ip"] is not None
|
|
168
168
|
|
|
169
|
-
def
|
|
169
|
+
def test_ignores_forwarded_ip_without_proxy_token(self, client, reset_config, monkeypatch):
|
|
170
170
|
monkeypatch.setenv("DEVLOGS_OPENSEARCH_HOST", "localhost")
|
|
171
171
|
monkeypatch.setenv("DEVLOGS_INDEX", "test-index")
|
|
172
172
|
|
|
@@ -184,6 +184,32 @@ class TestIngestEndpoint:
|
|
|
184
184
|
headers={"X-Forwarded-For": "192.168.1.100, 10.0.0.1"}
|
|
185
185
|
)
|
|
186
186
|
|
|
187
|
+
assert response.status_code == 202
|
|
188
|
+
body = mock_client.index.call_args.kwargs["body"]
|
|
189
|
+
assert body["client_ip"] != "192.168.1.100"
|
|
190
|
+
|
|
191
|
+
def test_captures_forwarded_ip_with_proxy_token(self, client, reset_config, monkeypatch):
|
|
192
|
+
monkeypatch.setenv("DEVLOGS_OPENSEARCH_HOST", "localhost")
|
|
193
|
+
monkeypatch.setenv("DEVLOGS_INDEX", "test-index")
|
|
194
|
+
monkeypatch.setenv("DEVLOGS_TRUSTED_PROXY_TOKEN", "my-proxy-secret")
|
|
195
|
+
|
|
196
|
+
mock_client = Mock()
|
|
197
|
+
mock_client.index = Mock(return_value={})
|
|
198
|
+
|
|
199
|
+
with patch("devlogs.collector.server.get_opensearch_client", return_value=mock_client):
|
|
200
|
+
response = client.post(
|
|
201
|
+
"/",
|
|
202
|
+
json={
|
|
203
|
+
"application": "test-app",
|
|
204
|
+
"component": "api",
|
|
205
|
+
"timestamp": "2024-01-15T10:30:00Z"
|
|
206
|
+
},
|
|
207
|
+
headers={
|
|
208
|
+
"X-Forwarded-For": "192.168.1.100, 10.0.0.1",
|
|
209
|
+
"X-Trusted-Proxy-Token": "my-proxy-secret",
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
187
213
|
assert response.status_code == 202
|
|
188
214
|
body = mock_client.index.call_args.kwargs["body"]
|
|
189
215
|
assert body["client_ip"] == "192.168.1.100"
|
|
@@ -575,17 +601,44 @@ class TestForwardMode:
|
|
|
575
601
|
class TestGetClientIp:
|
|
576
602
|
"""Tests for client IP extraction."""
|
|
577
603
|
|
|
578
|
-
def
|
|
604
|
+
def test_ignores_forwarded_for_without_token(self):
|
|
579
605
|
mock_request = Mock()
|
|
580
606
|
mock_request.headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
|
581
607
|
mock_request.client = Mock(host="127.0.0.1")
|
|
582
|
-
assert get_client_ip(mock_request) == "
|
|
608
|
+
assert get_client_ip(mock_request) == "127.0.0.1"
|
|
583
609
|
|
|
584
|
-
def
|
|
610
|
+
def test_ignores_real_ip_without_token(self):
|
|
585
611
|
mock_request = Mock()
|
|
586
612
|
mock_request.headers = {"X-Real-IP": "192.168.1.1"}
|
|
587
613
|
mock_request.client = Mock(host="127.0.0.1")
|
|
588
|
-
assert get_client_ip(mock_request) == "
|
|
614
|
+
assert get_client_ip(mock_request) == "127.0.0.1"
|
|
615
|
+
|
|
616
|
+
def test_extracts_forwarded_for_with_proxy_token(self):
|
|
617
|
+
mock_request = Mock()
|
|
618
|
+
mock_request.headers = {
|
|
619
|
+
"X-Forwarded-For": "192.168.1.1, 10.0.0.1",
|
|
620
|
+
"X-Trusted-Proxy-Token": "secret",
|
|
621
|
+
}
|
|
622
|
+
mock_request.client = Mock(host="127.0.0.1")
|
|
623
|
+
assert get_client_ip(mock_request, "secret") == "192.168.1.1"
|
|
624
|
+
|
|
625
|
+
def test_extracts_real_ip_with_proxy_token(self):
|
|
626
|
+
mock_request = Mock()
|
|
627
|
+
mock_request.headers = {
|
|
628
|
+
"X-Real-IP": "192.168.1.1",
|
|
629
|
+
"X-Trusted-Proxy-Token": "secret",
|
|
630
|
+
}
|
|
631
|
+
mock_request.client = Mock(host="127.0.0.1")
|
|
632
|
+
assert get_client_ip(mock_request, "secret") == "192.168.1.1"
|
|
633
|
+
|
|
634
|
+
def test_ignores_forwarded_for_with_wrong_token(self):
|
|
635
|
+
mock_request = Mock()
|
|
636
|
+
mock_request.headers = {
|
|
637
|
+
"X-Forwarded-For": "192.168.1.1, 10.0.0.1",
|
|
638
|
+
"X-Trusted-Proxy-Token": "wrong",
|
|
639
|
+
}
|
|
640
|
+
mock_request.client = Mock(host="127.0.0.1")
|
|
641
|
+
assert get_client_ip(mock_request, "secret") == "127.0.0.1"
|
|
589
642
|
|
|
590
643
|
def test_falls_back_to_client(self):
|
|
591
644
|
mock_request = Mock()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|