maleo-foundation 0.2.78__tar.gz → 0.2.81__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.
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/PKG-INFO +1 -1
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/service.py +3 -2
- maleo_foundation-0.2.81/maleo_foundation/middlewares/base.py +457 -0
- maleo_foundation-0.2.81/maleo_foundation/models/transfers/general/__init__.py +35 -0
- maleo_foundation-0.2.81/maleo_foundation/utils/dependencies/context.py +9 -0
- maleo_foundation-0.2.81/maleo_foundation/utils/extractor.py +55 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/PKG-INFO +1 -1
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/SOURCES.txt +1 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/pyproject.toml +1 -1
- maleo_foundation-0.2.78/maleo_foundation/middlewares/base.py +0 -366
- maleo_foundation-0.2.78/maleo_foundation/models/transfers/general/__init__.py +0 -11
- maleo_foundation-0.2.78/maleo_foundation/utils/extractor.py +0 -20
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/README.md +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/authentication.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/authorization.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/manager.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/encryption/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/encryption/aes.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/encryption/rsa.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/bcrypt.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/hmac.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/sha256.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/key.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/signature.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/token.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/constants.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/enums.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/client.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/encryption/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/encryption/aes.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/encryption/rsa.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/general.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/hash.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/key.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/repository.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/service.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/signature.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/token.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/extended_types.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/cache/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/cache/base.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/cache/redis.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/base.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/base.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/parameter.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/secret.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/storage.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/maleo.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/db.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/middleware.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/middlewares/authentication.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/middlewares/cors.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/responses.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/encryption.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/general.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/hash.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/key.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/parameter.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/result.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/signature.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/token.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/table.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/general/key.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/general/signature.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/general/token.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/client.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/encryption/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/encryption/aes.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/encryption/rsa.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/general.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/bcrypt.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/hmac.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/sha256.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/key.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/service.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/signature.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/token.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/controllers/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/controllers/http.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/service.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/encryption/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/encryption/aes.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/encryption/rsa.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/hash.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/key.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/controllers/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/controllers/rest.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/general.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/repository.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/signature.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/token.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/rest_controller_result.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/types.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/client.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/controller.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/dependencies/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/dependencies/auth.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/exceptions.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/formatter/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/formatter/case.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/credential/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/credential/google.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/json.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/key/__init__.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/key/rsa.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/yaml.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/logging.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/merger.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/query.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/repository.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/searcher.py +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/dependency_links.txt +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/requires.txt +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/top_level.txt +0 -0
- {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/setup.cfg +0 -0
@@ -90,6 +90,7 @@ class MaleoClientConfiguration(BaseModel):
|
|
90
90
|
url:str = Field(..., description="Client's URL")
|
91
91
|
|
92
92
|
class MaleoClientConfigurations(BaseModel):
|
93
|
+
telemetry:MaleoClientConfiguration = Field(..., description="MaleoTelemetry client's configuration")
|
93
94
|
metadata:MaleoClientConfiguration = Field(..., description="MaleoMetadata client's configuration")
|
94
95
|
identity:MaleoClientConfiguration = Field(..., description="MaleoIdentity client's configuration")
|
95
96
|
access:MaleoClientConfiguration = Field(..., description="MaleoAccess client's configuration")
|
@@ -415,7 +416,7 @@ class ServiceManager:
|
|
415
416
|
maleo_foundation=self._foundation
|
416
417
|
)
|
417
418
|
self._middleware.add_all()
|
418
|
-
self._loggers.application.info("Middlewares
|
419
|
+
self._loggers.application.info("Middlewares added successfully")
|
419
420
|
|
420
421
|
#* Add exception handler(s)
|
421
422
|
self._loggers.application.info("Adding exception handlers")
|
@@ -427,7 +428,7 @@ class ServiceManager:
|
|
427
428
|
exc_class_or_status_code=HTTPException,
|
428
429
|
handler=BaseExceptions.http_exception_handler
|
429
430
|
)
|
430
|
-
self._loggers.application.info("Exception handlers
|
431
|
+
self._loggers.application.info("Exception handlers added successfully")
|
431
432
|
|
432
433
|
#* Include router
|
433
434
|
self._loggers.application.info("Including routers")
|
@@ -0,0 +1,457 @@
|
|
1
|
+
import json
|
2
|
+
import threading
|
3
|
+
import time
|
4
|
+
import traceback
|
5
|
+
from collections import defaultdict
|
6
|
+
from datetime import datetime, timedelta, timezone
|
7
|
+
from typing import Awaitable, Callable, Optional, Sequence, Dict, List
|
8
|
+
|
9
|
+
from fastapi import FastAPI, Request, Response, status
|
10
|
+
from fastapi.responses import JSONResponse
|
11
|
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
12
|
+
|
13
|
+
from maleo_foundation.authentication import Authentication
|
14
|
+
from maleo_foundation.enums import BaseEnums
|
15
|
+
from maleo_foundation.client.manager import MaleoFoundationClientManager
|
16
|
+
from maleo_foundation.models.schemas import BaseGeneralSchemas
|
17
|
+
from maleo_foundation.models.responses import BaseResponses
|
18
|
+
from maleo_foundation.models.transfers.general.token import MaleoFoundationTokenGeneralTransfers
|
19
|
+
from maleo_foundation.models.transfers.parameters.token import MaleoFoundationTokenParametersTransfers
|
20
|
+
from maleo_foundation.models.transfers.parameters.signature import MaleoFoundationSignatureParametersTransfers
|
21
|
+
from maleo_foundation.models.transfers.general import RequestContextTransfers
|
22
|
+
from maleo_foundation.utils.extractor import extract_request_context
|
23
|
+
from maleo_foundation.utils.logging import MiddlewareLogger
|
24
|
+
|
25
|
+
RequestProcessor = Callable[[Request], Awaitable[Optional[Response]]]
|
26
|
+
ResponseProcessor = Callable[[Response], Awaitable[Response]]
|
27
|
+
|
28
|
+
class RateLimiter:
|
29
|
+
"""Thread-safe rate limiter with automatic cleanup."""
|
30
|
+
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
limit:int,
|
34
|
+
window:timedelta,
|
35
|
+
ip_timeout:timedelta,
|
36
|
+
cleanup_interval:timedelta
|
37
|
+
):
|
38
|
+
self.limit = limit
|
39
|
+
self.window = window
|
40
|
+
self.ip_timeout = ip_timeout
|
41
|
+
self.cleanup_interval = cleanup_interval
|
42
|
+
self._requests:Dict[str, List[datetime]] = defaultdict(list)
|
43
|
+
self._last_seen:Dict[str, datetime] = {}
|
44
|
+
self._last_cleanup = datetime.now()
|
45
|
+
self._lock = threading.RLock()
|
46
|
+
|
47
|
+
def is_rate_limited(
|
48
|
+
self,
|
49
|
+
request_context:RequestContextTransfers
|
50
|
+
) -> bool:
|
51
|
+
"""Check if client IP is rate limited and record the request."""
|
52
|
+
with self._lock:
|
53
|
+
now = datetime.now()
|
54
|
+
client_ip = request_context.ip_address
|
55
|
+
self._last_seen[client_ip] = now
|
56
|
+
|
57
|
+
# Remove old requests outside the window
|
58
|
+
self._requests[client_ip] = [
|
59
|
+
timestamp for timestamp in self._requests[client_ip]
|
60
|
+
if now - timestamp <= self.window
|
61
|
+
]
|
62
|
+
|
63
|
+
# Check rate limit
|
64
|
+
if len(self._requests[client_ip]) >= self.limit:
|
65
|
+
return True
|
66
|
+
|
67
|
+
# Record this request
|
68
|
+
self._requests[client_ip].append(now)
|
69
|
+
return False
|
70
|
+
|
71
|
+
def cleanup_old_data(
|
72
|
+
self,
|
73
|
+
logger:MiddlewareLogger
|
74
|
+
) -> None:
|
75
|
+
"""Clean up old request data to prevent memory growth."""
|
76
|
+
now = datetime.now()
|
77
|
+
if now - self._last_cleanup <= self.cleanup_interval:
|
78
|
+
return
|
79
|
+
|
80
|
+
with self._lock:
|
81
|
+
inactive_ips = []
|
82
|
+
|
83
|
+
for ip in list(self._requests.keys()):
|
84
|
+
# Remove IPs with empty request lists
|
85
|
+
if not self._requests[ip]:
|
86
|
+
inactive_ips.append(ip)
|
87
|
+
continue
|
88
|
+
|
89
|
+
# Remove IPs that haven't been active recently
|
90
|
+
last_active = self._last_seen.get(ip, datetime.min)
|
91
|
+
if now - last_active > self.ip_timeout:
|
92
|
+
inactive_ips.append(ip)
|
93
|
+
|
94
|
+
# Clean up inactive IPs
|
95
|
+
for ip in inactive_ips:
|
96
|
+
self._requests.pop(ip, None)
|
97
|
+
self._last_seen.pop(ip, None)
|
98
|
+
|
99
|
+
self._last_cleanup = now
|
100
|
+
logger.debug(
|
101
|
+
f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. "
|
102
|
+
f"Current tracked IPs: {len(self._requests)}"
|
103
|
+
)
|
104
|
+
|
105
|
+
class ResponseBuilder:
|
106
|
+
"""Handles response building and header management."""
|
107
|
+
|
108
|
+
def __init__(
|
109
|
+
self,
|
110
|
+
keys:BaseGeneralSchemas.RSAKeys,
|
111
|
+
maleo_foundation:MaleoFoundationClientManager
|
112
|
+
):
|
113
|
+
self.keys = keys
|
114
|
+
self.maleo_foundation = maleo_foundation
|
115
|
+
|
116
|
+
def add_response_headers(
|
117
|
+
self,
|
118
|
+
authentication:Authentication,
|
119
|
+
response:Response,
|
120
|
+
request_context:RequestContextTransfers,
|
121
|
+
responded_at:datetime,
|
122
|
+
process_time:float
|
123
|
+
) -> Response:
|
124
|
+
"""Add custom headers to response."""
|
125
|
+
# Basic headers
|
126
|
+
response.headers["X-Request-ID"] = str(request_context.request_id)
|
127
|
+
response.headers["X-Process-Time"] = str(process_time)
|
128
|
+
response.headers["X-Requested-At"] = request_context.requested_at.isoformat()
|
129
|
+
response.headers["X-Responded-At"] = responded_at.isoformat()
|
130
|
+
|
131
|
+
# Add signature header
|
132
|
+
self._add_signature_header(response, request_context, responded_at, process_time)
|
133
|
+
|
134
|
+
# Add new authorization header if needed
|
135
|
+
self._add_new_authorization_header(request_context, authentication, response)
|
136
|
+
|
137
|
+
return response
|
138
|
+
|
139
|
+
def _add_signature_header(
|
140
|
+
self,
|
141
|
+
response:Response,
|
142
|
+
request_context:RequestContextTransfers,
|
143
|
+
responded_at:datetime,
|
144
|
+
process_time:float
|
145
|
+
) -> None:
|
146
|
+
"""Generate and add signature header."""
|
147
|
+
message = (
|
148
|
+
f"{request_context.method}|{request_context.url}|{request_context.requested_at.isoformat()}|"
|
149
|
+
f"{responded_at.isoformat()}|{str(process_time)}|{str(request_context.request_id)}"
|
150
|
+
)
|
151
|
+
|
152
|
+
sign_parameters = MaleoFoundationSignatureParametersTransfers.Sign(
|
153
|
+
key=self.keys.private,
|
154
|
+
password=self.keys.password,
|
155
|
+
message=message
|
156
|
+
)
|
157
|
+
|
158
|
+
sign_result = self.maleo_foundation.services.signature.sign(parameters=sign_parameters)
|
159
|
+
if sign_result.success:
|
160
|
+
response.headers["X-Signature"] = sign_result.data.signature
|
161
|
+
|
162
|
+
def _add_new_authorization_header(
|
163
|
+
self,
|
164
|
+
request_context:RequestContextTransfers,
|
165
|
+
authentication:Authentication,
|
166
|
+
response:Response
|
167
|
+
) -> None:
|
168
|
+
"""Add new authorization header for refresh tokens."""
|
169
|
+
if not self._should_regenerate_auth(request_context, authentication, response):
|
170
|
+
return
|
171
|
+
|
172
|
+
payload = MaleoFoundationTokenGeneralTransfers.BaseEncodePayload.model_validate(
|
173
|
+
authentication.credentials.token.payload.model_dump()
|
174
|
+
)
|
175
|
+
|
176
|
+
parameters = MaleoFoundationTokenParametersTransfers.Encode(
|
177
|
+
key=self.keys.private,
|
178
|
+
password=self.keys.password,
|
179
|
+
payload=payload
|
180
|
+
)
|
181
|
+
|
182
|
+
result = self.maleo_foundation.services.token.encode(parameters=parameters)
|
183
|
+
if result.success:
|
184
|
+
response.headers["X-New-Authorization"] = result.data.token
|
185
|
+
|
186
|
+
def _should_regenerate_auth(
|
187
|
+
self,
|
188
|
+
request_context:RequestContextTransfers,
|
189
|
+
authentication:Authentication,
|
190
|
+
response:Response
|
191
|
+
) -> bool:
|
192
|
+
"""Check if authorization should be regenerated."""
|
193
|
+
return (
|
194
|
+
authentication.user.is_authenticated
|
195
|
+
and authentication.credentials.token.type == BaseEnums.TokenType.REFRESH
|
196
|
+
and 200 <= response.status_code < 300
|
197
|
+
and "logout" not in request_context.url
|
198
|
+
)
|
199
|
+
|
200
|
+
class RequestLogger:
|
201
|
+
"""Handles request/response logging."""
|
202
|
+
|
203
|
+
def __init__(self, logger:MiddlewareLogger):
|
204
|
+
self.logger = logger
|
205
|
+
|
206
|
+
def log_request_response(
|
207
|
+
self,
|
208
|
+
authentication:Authentication,
|
209
|
+
response:Response,
|
210
|
+
request_context:RequestContextTransfers,
|
211
|
+
log_level:str = "info"
|
212
|
+
) -> None:
|
213
|
+
"""Log request and response details."""
|
214
|
+
authentication_info = self._get_authentication_info(authentication)
|
215
|
+
|
216
|
+
log_func = getattr(self.logger, log_level)
|
217
|
+
log_func(
|
218
|
+
f"Request | ID: {request_context.request_id} {authentication_info} | "
|
219
|
+
f"IP: {request_context.ip_address} | Host: {request_context.host} | "
|
220
|
+
f"Method: {request_context.method} | URL: {request_context.url} - "
|
221
|
+
f"Response | Status: {response.status_code}"
|
222
|
+
)
|
223
|
+
|
224
|
+
def log_exception(
|
225
|
+
self,
|
226
|
+
authentication:Authentication,
|
227
|
+
error:Exception,
|
228
|
+
request_context:RequestContextTransfers
|
229
|
+
) -> None:
|
230
|
+
"""Log exception details."""
|
231
|
+
authentication_info = self._get_authentication_info(authentication)
|
232
|
+
|
233
|
+
error_details = {
|
234
|
+
"request_context": request_context.model_dump(mode="json"),
|
235
|
+
"error": str(error),
|
236
|
+
"traceback": traceback.format_exc().split("\n")
|
237
|
+
}
|
238
|
+
|
239
|
+
self.logger.error(
|
240
|
+
f"Request | ID: {request_context.request_id} {authentication_info} | "
|
241
|
+
f"IP: {request_context.ip_address} | Host: {request_context.host} | "
|
242
|
+
f"Method: {request_context.method} | URL: {request_context.url} - "
|
243
|
+
f"Response | Status: 500 | Exception:\n{json.dumps(error_details, indent=4)}"
|
244
|
+
)
|
245
|
+
|
246
|
+
def _get_authentication_info(self, authentication:Authentication) -> str:
|
247
|
+
"""Get authentication info string."""
|
248
|
+
if not authentication.user.is_authenticated:
|
249
|
+
return "| Unauthenticated"
|
250
|
+
|
251
|
+
return (
|
252
|
+
f"| Token type: {authentication.credentials.token.type} | "
|
253
|
+
f"Username: {authentication.user.display_name} | "
|
254
|
+
f"Email: {authentication.user.identity}"
|
255
|
+
)
|
256
|
+
|
257
|
+
|
258
|
+
class BaseMiddleware(BaseHTTPMiddleware):
|
259
|
+
"""Base middleware with rate limiting, logging, and response enhancement."""
|
260
|
+
|
261
|
+
def __init__(
|
262
|
+
self,
|
263
|
+
app:FastAPI,
|
264
|
+
keys:BaseGeneralSchemas.RSAKeys,
|
265
|
+
logger:MiddlewareLogger,
|
266
|
+
maleo_foundation:MaleoFoundationClientManager,
|
267
|
+
allow_origins:Sequence[str] = (),
|
268
|
+
allow_methods:Sequence[str] = ("GET",),
|
269
|
+
allow_headers:Sequence[str] = (),
|
270
|
+
allow_credentials:bool = False,
|
271
|
+
limit:int = 10,
|
272
|
+
window:int = 1,
|
273
|
+
cleanup_interval:int = 60,
|
274
|
+
ip_timeout:int = 300
|
275
|
+
):
|
276
|
+
super().__init__(app)
|
277
|
+
|
278
|
+
# Core components
|
279
|
+
self.rate_limiter = RateLimiter(
|
280
|
+
limit=limit,
|
281
|
+
window=timedelta(seconds=window),
|
282
|
+
ip_timeout=timedelta(seconds=ip_timeout),
|
283
|
+
cleanup_interval=timedelta(seconds=cleanup_interval)
|
284
|
+
)
|
285
|
+
self.response_builder = ResponseBuilder(keys, maleo_foundation)
|
286
|
+
self.request_logger = RequestLogger(logger)
|
287
|
+
|
288
|
+
# CORS settings (if needed)
|
289
|
+
self.cors_config = {
|
290
|
+
'allow_origins': allow_origins,
|
291
|
+
'allow_methods': allow_methods,
|
292
|
+
'allow_headers': allow_headers,
|
293
|
+
'allow_credentials': allow_credentials,
|
294
|
+
}
|
295
|
+
|
296
|
+
async def dispatch(self, request:Request, call_next:RequestResponseEndpoint) -> Response:
|
297
|
+
"""Main middleware dispatch method."""
|
298
|
+
# Setup
|
299
|
+
self.rate_limiter.cleanup_old_data(self.request_logger.logger)
|
300
|
+
request_context = extract_request_context(request)
|
301
|
+
request.state.request_context = request_context
|
302
|
+
start_time = time.perf_counter()
|
303
|
+
authentication = Authentication(credentials=request.auth, user=request.user)
|
304
|
+
|
305
|
+
try:
|
306
|
+
# Rate limiting check
|
307
|
+
if self.rate_limiter.is_rate_limited(request_context):
|
308
|
+
return self._create_rate_limit_response(
|
309
|
+
authentication, request_context, start_time
|
310
|
+
)
|
311
|
+
|
312
|
+
# Optional preprocessing
|
313
|
+
pre_response = await self._request_processor(request)
|
314
|
+
if pre_response is not None:
|
315
|
+
return self._build_final_response(
|
316
|
+
authentication, pre_response, request_context, start_time
|
317
|
+
)
|
318
|
+
|
319
|
+
# Main request processing
|
320
|
+
response = await call_next(request)
|
321
|
+
return self._build_final_response(
|
322
|
+
authentication, response, request_context, start_time
|
323
|
+
)
|
324
|
+
|
325
|
+
except Exception as e:
|
326
|
+
return self._handle_exception(
|
327
|
+
request, authentication, e, request_context, start_time
|
328
|
+
)
|
329
|
+
|
330
|
+
async def _request_processor(self, request:Request) -> Optional[Response]:
|
331
|
+
"""Override this method for custom request preprocessing."""
|
332
|
+
return None
|
333
|
+
|
334
|
+
def _create_rate_limit_response(
|
335
|
+
self,
|
336
|
+
authentication:Authentication,
|
337
|
+
request_context:RequestContextTransfers,
|
338
|
+
start_time:float
|
339
|
+
) -> Response:
|
340
|
+
"""Create rate limit exceeded response."""
|
341
|
+
response = JSONResponse(
|
342
|
+
content=BaseResponses.RateLimitExceeded().model_dump(),
|
343
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
344
|
+
)
|
345
|
+
|
346
|
+
return self._build_final_response(
|
347
|
+
authentication, response, request_context, start_time, log_level="warning"
|
348
|
+
)
|
349
|
+
|
350
|
+
def _build_final_response(
|
351
|
+
self,
|
352
|
+
authentication:Authentication,
|
353
|
+
response:Response,
|
354
|
+
request_context:RequestContextTransfers,
|
355
|
+
start_time:float,
|
356
|
+
log_level:str = "info"
|
357
|
+
) -> Response:
|
358
|
+
"""Build final response with headers and logging."""
|
359
|
+
responded_at = datetime.now(tz=timezone.utc)
|
360
|
+
process_time = time.perf_counter() - start_time
|
361
|
+
|
362
|
+
# Add headers
|
363
|
+
response = self.response_builder.add_response_headers(
|
364
|
+
authentication, response, request_context, responded_at, process_time
|
365
|
+
)
|
366
|
+
|
367
|
+
# Log request/response
|
368
|
+
self.request_logger.log_request_response(
|
369
|
+
authentication, response, request_context, log_level
|
370
|
+
)
|
371
|
+
|
372
|
+
return response
|
373
|
+
|
374
|
+
def _handle_exception(
|
375
|
+
self,
|
376
|
+
authentication:Authentication,
|
377
|
+
error:Exception,
|
378
|
+
request_context:RequestContextTransfers,
|
379
|
+
start_time:float
|
380
|
+
) -> Response:
|
381
|
+
"""Handle exceptions and create error response."""
|
382
|
+
responded_at = datetime.now(tz=timezone.utc)
|
383
|
+
process_time = time.perf_counter() - start_time
|
384
|
+
|
385
|
+
response = JSONResponse(
|
386
|
+
content=BaseResponses.ServerError().model_dump(),
|
387
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
388
|
+
)
|
389
|
+
|
390
|
+
# Log exception
|
391
|
+
self.request_logger.log_exception(authentication, error, request_context)
|
392
|
+
|
393
|
+
# Add headers
|
394
|
+
return self.response_builder.add_response_headers(
|
395
|
+
authentication, response, request_context, responded_at, process_time
|
396
|
+
)
|
397
|
+
|
398
|
+
|
399
|
+
def add_base_middleware(
|
400
|
+
app:FastAPI,
|
401
|
+
keys:BaseGeneralSchemas.RSAKeys,
|
402
|
+
logger:MiddlewareLogger,
|
403
|
+
maleo_foundation:MaleoFoundationClientManager,
|
404
|
+
allow_origins:Sequence[str] = (),
|
405
|
+
allow_methods:Sequence[str] = ("GET",),
|
406
|
+
allow_headers:Sequence[str] = (),
|
407
|
+
allow_credentials:bool = False,
|
408
|
+
limit:int = 10,
|
409
|
+
window:int = 1,
|
410
|
+
cleanup_interval:int = 60,
|
411
|
+
ip_timeout:int = 300
|
412
|
+
) -> None:
|
413
|
+
"""
|
414
|
+
Add Base middleware to the FastAPI application.
|
415
|
+
|
416
|
+
Args:
|
417
|
+
app:FastAPI application instance
|
418
|
+
keys:RSA keys for signing and token generation
|
419
|
+
logger:Middleware logger instance
|
420
|
+
maleo_foundation:Client manager for foundation services
|
421
|
+
allow_origins:CORS allowed origins
|
422
|
+
allow_methods:CORS allowed methods
|
423
|
+
allow_headers:CORS allowed headers
|
424
|
+
allow_credentials:CORS allow credentials flag
|
425
|
+
limit:Request count limit per window
|
426
|
+
window:Time window for rate limiting (seconds)
|
427
|
+
cleanup_interval:Cleanup interval for old IP data (seconds)
|
428
|
+
ip_timeout:IP timeout after last activity (seconds)
|
429
|
+
|
430
|
+
Example:
|
431
|
+
```python
|
432
|
+
add_base_middleware(
|
433
|
+
app=app,
|
434
|
+
keys=rsa_keys,
|
435
|
+
logger=middleware_logger,
|
436
|
+
maleo_foundation=client_manager,
|
437
|
+
limit=10,
|
438
|
+
window=1,
|
439
|
+
cleanup_interval=60,
|
440
|
+
ip_timeout=300
|
441
|
+
)
|
442
|
+
```
|
443
|
+
"""
|
444
|
+
app.add_middleware(
|
445
|
+
BaseMiddleware,
|
446
|
+
keys=keys,
|
447
|
+
logger=logger,
|
448
|
+
maleo_foundation=maleo_foundation,
|
449
|
+
allow_origins=allow_origins,
|
450
|
+
allow_methods=allow_methods,
|
451
|
+
allow_headers=allow_headers,
|
452
|
+
allow_credentials=allow_credentials,
|
453
|
+
limit=limit,
|
454
|
+
window=window,
|
455
|
+
cleanup_interval=cleanup_interval,
|
456
|
+
ip_timeout=ip_timeout
|
457
|
+
)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from datetime import datetime, timezone
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
from typing import Dict
|
5
|
+
from uuid import UUID
|
6
|
+
from maleo_foundation.models.schemas.general import BaseGeneralSchemas
|
7
|
+
from maleo_foundation.types import BaseTypes
|
8
|
+
from .token import MaleoFoundationTokenGeneralTransfers
|
9
|
+
|
10
|
+
class BaseGeneralTransfers:
|
11
|
+
Token = MaleoFoundationTokenGeneralTransfers
|
12
|
+
|
13
|
+
class AccessTransfers(
|
14
|
+
BaseGeneralSchemas.AccessedBy,
|
15
|
+
BaseGeneralSchemas.AccessedAt
|
16
|
+
): pass
|
17
|
+
|
18
|
+
class RequestContextTransfers(BaseModel):
|
19
|
+
request_id:UUID = Field(..., description="Unique identifier for tracing the request")
|
20
|
+
requested_at:datetime = Field(datetime.now(tz=timezone.utc), description="Request timestamp")
|
21
|
+
method:str = Field(..., description="Request's method")
|
22
|
+
url:str = Field(..., description="Request's URL")
|
23
|
+
path_params:Dict = Field(..., description="Request's path parameters")
|
24
|
+
query_params:Dict = Field(..., description="Request's query parameters")
|
25
|
+
ip_address:str = Field("unknown", description="Client's IP address")
|
26
|
+
is_internal:BaseTypes.OptionalBoolean = Field(None, description="True if IP is internal")
|
27
|
+
user_agent:BaseTypes.OptionalString = Field(None, description="User-Agent string")
|
28
|
+
ua_browser:BaseTypes.OptionalString = Field(None, description="Browser info from sec-ch-ua")
|
29
|
+
ua_mobile:BaseTypes.OptionalString = Field(None, description="Is mobile device?")
|
30
|
+
platform:BaseTypes.OptionalString = Field(None, description="Client platform or OS")
|
31
|
+
referer:BaseTypes.OptionalString = Field(None, description="Referrer URL")
|
32
|
+
origin:BaseTypes.OptionalString = Field(None, description="Origin of the request")
|
33
|
+
host:BaseTypes.OptionalString = Field(None, description="Host header from request")
|
34
|
+
forwarded_proto:BaseTypes.OptionalString = Field(None, description="Forwarded protocol (http/https)")
|
35
|
+
language:BaseTypes.OptionalString = Field(None, description="Accepted languages from client")
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from fastapi.requests import Request
|
2
|
+
from maleo_foundation.models.transfers.general import RequestContextTransfers
|
3
|
+
|
4
|
+
class ContextDependencies:
|
5
|
+
@staticmethod
|
6
|
+
def get_request_context(
|
7
|
+
request:Request
|
8
|
+
) -> RequestContextTransfers:
|
9
|
+
return request.state.request_context
|
@@ -0,0 +1,55 @@
|
|
1
|
+
from datetime import datetime, timezone
|
2
|
+
from fastapi import Request
|
3
|
+
from starlette.requests import HTTPConnection
|
4
|
+
from uuid import uuid4
|
5
|
+
from maleo_foundation.models.transfers.general import RequestContextTransfers
|
6
|
+
|
7
|
+
def extract_client_ip(conn:HTTPConnection) -> str:
|
8
|
+
"""Extract client IP with more robust handling of proxies"""
|
9
|
+
#* Check for X-Forwarded-For header (common when behind proxy/load balancer)
|
10
|
+
x_forwarded_for = conn.headers.get("X-Forwarded-For")
|
11
|
+
if x_forwarded_for:
|
12
|
+
#* The client's IP is the first one in the list
|
13
|
+
ips = [ip.strip() for ip in x_forwarded_for.split(",")]
|
14
|
+
return ips[0]
|
15
|
+
|
16
|
+
#* Check for X-Real-IP header (used by some proxies)
|
17
|
+
x_real_ip = conn.headers.get("X-Real-IP")
|
18
|
+
if x_real_ip:
|
19
|
+
return x_real_ip
|
20
|
+
|
21
|
+
#* Fall back to direct client connection
|
22
|
+
return conn.client.host if conn.client else "unknown"
|
23
|
+
|
24
|
+
def extract_request_context(request:Request) -> RequestContextTransfers:
|
25
|
+
headers = request.headers
|
26
|
+
|
27
|
+
request_id = headers.get("x-request-id")
|
28
|
+
if request_id is None:
|
29
|
+
request_id = uuid4()
|
30
|
+
|
31
|
+
ip_address = extract_client_ip(request)
|
32
|
+
|
33
|
+
ua_browser = headers.get("sec-ch-ua", "")
|
34
|
+
if ua_browser:
|
35
|
+
ua_browser = ua_browser.replace('"', "").split(",")[0].strip()
|
36
|
+
|
37
|
+
return RequestContextTransfers(
|
38
|
+
request_id=request_id,
|
39
|
+
requested_at=datetime.now(tz=timezone.utc),
|
40
|
+
method=request.method,
|
41
|
+
url=request.url.path,
|
42
|
+
path_params=dict(request.path_params),
|
43
|
+
query_params=dict(request.query_params),
|
44
|
+
ip_address=ip_address,
|
45
|
+
is_internal=ip_address.startswith("10.") or ip_address.startswith("192.168.") or ip_address.startswith("172."),
|
46
|
+
user_agent=headers.get("user-agent"),
|
47
|
+
ua_browser=ua_browser,
|
48
|
+
ua_mobile=headers.get("sec-ch-ua-mobile"),
|
49
|
+
platform=headers.get("sec-ch-ua-platform"),
|
50
|
+
referer=headers.get("referer"),
|
51
|
+
origin=headers.get("origin"),
|
52
|
+
host=headers.get("host"),
|
53
|
+
forwarded_proto=headers.get("x-forwarded-proto"),
|
54
|
+
language=headers.get("accept-language"),
|
55
|
+
)
|
@@ -116,6 +116,7 @@ maleo_foundation/utils/repository.py
|
|
116
116
|
maleo_foundation/utils/searcher.py
|
117
117
|
maleo_foundation/utils/dependencies/__init__.py
|
118
118
|
maleo_foundation/utils/dependencies/auth.py
|
119
|
+
maleo_foundation/utils/dependencies/context.py
|
119
120
|
maleo_foundation/utils/formatter/__init__.py
|
120
121
|
maleo_foundation/utils/formatter/case.py
|
121
122
|
maleo_foundation/utils/loaders/__init__.py
|