devlogs 2.3.2__tar.gz → 2.3.4__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.4}/PKG-INFO +1 -1
- {devlogs-2.3.2 → devlogs-2.3.4}/pyproject.toml +1 -1
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/_version_static.py +1 -1
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/server.py +24 -21
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/config.py +8 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/web/server.py +4 -2
- {devlogs-2.3.2 → devlogs-2.3.4/src/devlogs.egg-info}/PKG-INFO +1 -1
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_collector_server.py +58 -5
- {devlogs-2.3.2 → devlogs-2.3.4}/LICENSE +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/MANIFEST.in +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/README.md +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/setup.cfg +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/__main__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/build_info.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/cli.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/auth.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/cli.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/errors.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/forwarder.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/ingestor.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/loki_plugin.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/plugins.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/collector/schema.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/context.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/demo.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/devlogs_client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/formatting.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/handler.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/jenkins/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/jenkins/cli.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/jenkins/core.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/levels.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/loki/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/loki/queries.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/mcp/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/mcp/server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/opensearch/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/opensearch/client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/opensearch/indexing.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/opensearch/mappings.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/opensearch/queries.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/proxy/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/proxy/server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/retention.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/scrub.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/time_utils.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/version.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/web/__init__.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/web/static/devlogs.css +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/web/static/devlogs.js +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/web/static/index.html +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs/wrapper.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs.egg-info/SOURCES.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs.egg-info/dependency_links.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs.egg-info/entry_points.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs.egg-info/requires.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/src/devlogs.egg-info/top_level.txt +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_build_info.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_cli.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_collector_auth.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_collector_config.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_collector_plugins.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_collector_schema.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_config.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_context.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_devlogs_client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_formatting.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_handler.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_indexing.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_levels.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_mappings.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_mcp_server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_opensearch_client.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_opensearch_queries.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_proxy_server.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_retention.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_scrub.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_time_utils.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/tests/test_url_parsing.py +0 -0
- {devlogs-2.3.2 → devlogs-2.3.4}/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.4"
|
|
@@ -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
|
|
|
@@ -465,6 +468,11 @@ class DevlogsConfig:
|
|
|
465
468
|
# Token-to-identity mapping (KV format)
|
|
466
469
|
self.token_map_kv = _getenv("DEVLOGS_TOKEN_MAP_KV", "")
|
|
467
470
|
|
|
471
|
+
# Trusted proxy token: if set, X-Forwarded-For / X-Real-IP headers are
|
|
472
|
+
# only honored when the request includes this token in X-Trusted-Proxy-Token.
|
|
473
|
+
# If empty, forwarded headers are never trusted.
|
|
474
|
+
self.trusted_proxy_token = _getenv("DEVLOGS_TRUSTED_PROXY_TOKEN", "")
|
|
475
|
+
|
|
468
476
|
# Forward mode: per-application index routing (KV format)
|
|
469
477
|
self.forward_index_map_kv = _getenv("DEVLOGS_FORWARD_INDEX_MAP_KV", "")
|
|
470
478
|
|
|
@@ -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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|