maleo-foundation 0.2.78__py3-none-any.whl → 0.2.80__py3-none-any.whl
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/managers/service.py +1 -0
- maleo_foundation/middlewares/base.py +366 -241
- {maleo_foundation-0.2.78.dist-info → maleo_foundation-0.2.80.dist-info}/METADATA +1 -1
- {maleo_foundation-0.2.78.dist-info → maleo_foundation-0.2.80.dist-info}/RECORD +6 -6
- {maleo_foundation-0.2.78.dist-info → maleo_foundation-0.2.80.dist-info}/WHEEL +0 -0
- {maleo_foundation-0.2.78.dist-info → maleo_foundation-0.2.80.dist-info}/top_level.txt +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")
|
@@ -4,303 +4,432 @@ import time
|
|
4
4
|
import traceback
|
5
5
|
from collections import defaultdict
|
6
6
|
from datetime import datetime, timedelta, timezone
|
7
|
+
from typing import Awaitable, Callable, Optional, Sequence, Dict, List
|
8
|
+
from uuid import UUID, uuid4
|
9
|
+
|
7
10
|
from fastapi import FastAPI, Request, Response, status
|
8
11
|
from fastapi.responses import JSONResponse
|
9
12
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
10
|
-
|
13
|
+
|
11
14
|
from maleo_foundation.authentication import Authentication
|
12
15
|
from maleo_foundation.enums import BaseEnums
|
13
16
|
from maleo_foundation.client.manager import MaleoFoundationClientManager
|
14
17
|
from maleo_foundation.models.schemas import BaseGeneralSchemas
|
15
18
|
from maleo_foundation.models.responses import BaseResponses
|
16
|
-
from maleo_foundation.models.transfers.general.token
|
17
|
-
|
18
|
-
from maleo_foundation.models.transfers.parameters.
|
19
|
-
import MaleoFoundationTokenParametersTransfers
|
20
|
-
from maleo_foundation.models.transfers.parameters.signature \
|
21
|
-
import MaleoFoundationSignatureParametersTransfers
|
19
|
+
from maleo_foundation.models.transfers.general.token import MaleoFoundationTokenGeneralTransfers
|
20
|
+
from maleo_foundation.models.transfers.parameters.token import MaleoFoundationTokenParametersTransfers
|
21
|
+
from maleo_foundation.models.transfers.parameters.signature import MaleoFoundationSignatureParametersTransfers
|
22
22
|
from maleo_foundation.utils.extractor import BaseExtractors
|
23
23
|
from maleo_foundation.utils.logging import MiddlewareLogger
|
24
24
|
|
25
25
|
RequestProcessor = Callable[[Request], Awaitable[Optional[Response]]]
|
26
26
|
ResponseProcessor = Callable[[Response], Awaitable[Response]]
|
27
27
|
|
28
|
-
class
|
28
|
+
class RateLimiter:
|
29
|
+
"""Thread-safe rate limiter with automatic cleanup."""
|
30
|
+
|
29
31
|
def __init__(
|
30
32
|
self,
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
allow_origins:Sequence[str] = (),
|
36
|
-
allow_methods:Sequence[str] = ("GET",),
|
37
|
-
allow_headers:Sequence[str] = (),
|
38
|
-
allow_credentials:bool = False,
|
39
|
-
limit:int = 10,
|
40
|
-
window:int = 1,
|
41
|
-
cleanup_interval:int = 60,
|
42
|
-
ip_timeout:int = 300
|
33
|
+
limit:int,
|
34
|
+
window:timedelta,
|
35
|
+
ip_timeout:timedelta,
|
36
|
+
cleanup_interval:timedelta
|
43
37
|
):
|
44
|
-
|
45
|
-
self.
|
46
|
-
self.
|
47
|
-
self.
|
48
|
-
self.
|
49
|
-
self.
|
50
|
-
self._allow_headers = allow_headers
|
51
|
-
self._allow_credentials = allow_credentials
|
52
|
-
self._limit = limit
|
53
|
-
self._window = timedelta(seconds=window)
|
54
|
-
self._cleanup_interval = timedelta(seconds=cleanup_interval)
|
55
|
-
self._ip_timeout = timedelta(seconds=ip_timeout)
|
56
|
-
self._requests:dict[str, list[datetime]] = defaultdict(list)
|
57
|
-
self._last_seen: dict[str, datetime] = {}
|
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] = {}
|
58
44
|
self._last_cleanup = datetime.now()
|
59
|
-
self._lock = threading.RLock()
|
60
|
-
|
61
|
-
def
|
62
|
-
"""
|
63
|
-
Periodically clean up old request data to prevent memory growth.
|
64
|
-
Removes:
|
65
|
-
1. IPs with empty request lists
|
66
|
-
2. IPs that haven't been seen in ip_timeout period
|
67
|
-
"""
|
68
|
-
now = datetime.now()
|
69
|
-
if now - self._last_cleanup > self._cleanup_interval:
|
70
|
-
with self._lock:
|
71
|
-
#* Remove inactive IPs (not seen recently) and empty lists
|
72
|
-
inactive_ips = []
|
73
|
-
for ip in list(self._requests.keys()):
|
74
|
-
#* Remove IPs with empty timestamp lists
|
75
|
-
if not self._requests[ip]:
|
76
|
-
inactive_ips.append(ip)
|
77
|
-
continue
|
78
|
-
|
79
|
-
#* Remove IPs that haven't been active recently
|
80
|
-
last_active = self._last_seen.get(ip, datetime.min)
|
81
|
-
if now - last_active > self._ip_timeout:
|
82
|
-
inactive_ips.append(ip)
|
83
|
-
|
84
|
-
#* Remove the inactive IPs
|
85
|
-
for ip in inactive_ips:
|
86
|
-
if ip in self._requests:
|
87
|
-
del self._requests[ip]
|
88
|
-
if ip in self._last_seen:
|
89
|
-
del self._last_seen[ip]
|
90
|
-
|
91
|
-
# Update last cleanup time
|
92
|
-
self._last_cleanup = now
|
93
|
-
self._logger.debug(f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. Current tracked IPs: {len(self._requests)}")
|
94
|
-
|
95
|
-
def _check_rate_limit(
|
45
|
+
self._lock = threading.RLock()
|
46
|
+
|
47
|
+
def is_rate_limited(
|
96
48
|
self,
|
97
49
|
client_ip:str
|
98
50
|
) -> bool:
|
99
|
-
"""Check if
|
51
|
+
"""Check if client IP is rate limited and record the request."""
|
100
52
|
with self._lock:
|
101
|
-
now = datetime.now()
|
102
|
-
self._last_seen[client_ip] = now
|
53
|
+
now = datetime.now()
|
54
|
+
self._last_seen[client_ip] = now
|
103
55
|
|
104
|
-
|
56
|
+
# Remove old requests outside the window
|
105
57
|
self._requests[client_ip] = [
|
106
58
|
timestamp for timestamp in self._requests[client_ip]
|
107
|
-
if now - timestamp <= self.
|
59
|
+
if now - timestamp <= self.window
|
108
60
|
]
|
109
61
|
|
110
|
-
|
111
|
-
if len(self._requests[client_ip]) >= self.
|
62
|
+
# Check rate limit
|
63
|
+
if len(self._requests[client_ip]) >= self.limit:
|
112
64
|
return True
|
113
65
|
|
114
|
-
|
66
|
+
# Record this request
|
115
67
|
self._requests[client_ip].append(now)
|
116
68
|
return False
|
117
69
|
|
118
|
-
def
|
70
|
+
def cleanup_old_data(
|
71
|
+
self,
|
72
|
+
logger:MiddlewareLogger
|
73
|
+
) -> None:
|
74
|
+
"""Clean up old request data to prevent memory growth."""
|
75
|
+
now = datetime.now()
|
76
|
+
if now - self._last_cleanup <= self.cleanup_interval:
|
77
|
+
return
|
78
|
+
|
79
|
+
with self._lock:
|
80
|
+
inactive_ips = []
|
81
|
+
|
82
|
+
for ip in list(self._requests.keys()):
|
83
|
+
# Remove IPs with empty request lists
|
84
|
+
if not self._requests[ip]:
|
85
|
+
inactive_ips.append(ip)
|
86
|
+
continue
|
87
|
+
|
88
|
+
# Remove IPs that haven't been active recently
|
89
|
+
last_active = self._last_seen.get(ip, datetime.min)
|
90
|
+
if now - last_active > self.ip_timeout:
|
91
|
+
inactive_ips.append(ip)
|
92
|
+
|
93
|
+
# Clean up inactive IPs
|
94
|
+
for ip in inactive_ips:
|
95
|
+
self._requests.pop(ip, None)
|
96
|
+
self._last_seen.pop(ip, None)
|
97
|
+
|
98
|
+
self._last_cleanup = now
|
99
|
+
logger.debug(
|
100
|
+
f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. "
|
101
|
+
f"Current tracked IPs: {len(self._requests)}"
|
102
|
+
)
|
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(
|
119
117
|
self,
|
120
118
|
request:Request,
|
121
119
|
authentication:Authentication,
|
122
120
|
response:Response,
|
123
121
|
request_timestamp:datetime,
|
124
122
|
response_timestamp:datetime,
|
125
|
-
process_time:
|
123
|
+
process_time:float,
|
124
|
+
request_id:UUID
|
126
125
|
) -> Response:
|
127
|
-
|
128
|
-
|
129
|
-
response.headers["X-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
126
|
+
"""Add custom headers to response."""
|
127
|
+
# Basic headers
|
128
|
+
response.headers["X-Request-ID"] = str(request_id)
|
129
|
+
response.headers["X-Process-Time"] = str(process_time)
|
130
|
+
response.headers["X-Request-Timestamp"] = request_timestamp.isoformat()
|
131
|
+
response.headers["X-Response-Timestamp"] = response_timestamp.isoformat()
|
132
|
+
|
133
|
+
# Add signature header
|
134
|
+
self._add_signature_header(request, response, request_timestamp, response_timestamp, process_time, request_id)
|
135
|
+
|
136
|
+
# Add new authorization header if needed
|
137
|
+
self._add_new_authorization_header(request, authentication, response)
|
138
|
+
|
139
|
+
return response
|
140
|
+
|
141
|
+
def _add_signature_header(
|
142
|
+
self,
|
143
|
+
request:Request,
|
144
|
+
response:Response,
|
145
|
+
request_timestamp:datetime,
|
146
|
+
response_timestamp:datetime,
|
147
|
+
process_time:float,
|
148
|
+
request_id:UUID
|
149
|
+
) -> None:
|
150
|
+
"""Generate and add signature header."""
|
151
|
+
message = (
|
152
|
+
f"{request.method}|{request.url.path}|{request_timestamp.isoformat()}|"
|
153
|
+
f"{response_timestamp.isoformat()}|{str(process_time)}|{str(request_id)}"
|
135
154
|
)
|
136
|
-
|
155
|
+
|
156
|
+
sign_parameters = MaleoFoundationSignatureParametersTransfers.Sign(
|
157
|
+
key=self.keys.private,
|
158
|
+
password=self.keys.password,
|
159
|
+
message=message
|
160
|
+
)
|
161
|
+
|
162
|
+
sign_result = self.maleo_foundation.services.signature.sign(parameters=sign_parameters)
|
137
163
|
if sign_result.success:
|
138
164
|
response.headers["X-Signature"] = sign_result.data.signature
|
139
|
-
|
165
|
+
|
166
|
+
def _add_new_authorization_header(
|
167
|
+
self,
|
168
|
+
request:Request,
|
169
|
+
authentication:Authentication,
|
170
|
+
response:Response
|
171
|
+
) -> None:
|
172
|
+
"""Add new authorization header for refresh tokens."""
|
173
|
+
if not self._should_regenerate_auth(request, authentication, response):
|
174
|
+
return
|
175
|
+
|
176
|
+
payload = MaleoFoundationTokenGeneralTransfers.BaseEncodePayload.model_validate(
|
177
|
+
authentication.credentials.token.payload.model_dump()
|
178
|
+
)
|
179
|
+
|
180
|
+
parameters = MaleoFoundationTokenParametersTransfers.Encode(
|
181
|
+
key=self.keys.private,
|
182
|
+
password=self.keys.password,
|
183
|
+
payload=payload
|
184
|
+
)
|
185
|
+
|
186
|
+
result = self.maleo_foundation.services.token.encode(parameters=parameters)
|
187
|
+
if result.success:
|
188
|
+
response.headers["X-New-Authorization"] = result.data.token
|
189
|
+
|
190
|
+
def _should_regenerate_auth(
|
191
|
+
self,
|
192
|
+
request:Request,
|
193
|
+
authentication:Authentication,
|
194
|
+
response:Response
|
195
|
+
) -> bool:
|
196
|
+
"""Check if authorization should be regenerated."""
|
197
|
+
return (
|
198
|
+
authentication.user.is_authenticated
|
140
199
|
and authentication.credentials.token.type == BaseEnums.TokenType.REFRESH
|
141
|
-
and
|
200
|
+
and 200 <= response.status_code < 300
|
142
201
|
and "logout" not in request.url.path
|
143
|
-
)
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
result = self._maleo_foundation.services.token.encode(parameters=parameters)
|
155
|
-
if result.success:
|
156
|
-
response.headers["X-New-Authorization"] = result.data.token
|
157
|
-
return response
|
202
|
+
)
|
203
|
+
|
204
|
+
|
205
|
+
class RequestLogger:
|
206
|
+
"""Handles request/response logging."""
|
207
|
+
|
208
|
+
def __init__(
|
209
|
+
self,
|
210
|
+
logger:MiddlewareLogger
|
211
|
+
):
|
212
|
+
self.logger = logger
|
158
213
|
|
159
|
-
def
|
214
|
+
def log_request_response(
|
160
215
|
self,
|
161
216
|
request:Request,
|
162
217
|
authentication:Authentication,
|
163
|
-
authentication_info:str,
|
164
218
|
response:Response,
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
authentication,
|
174
|
-
response,
|
175
|
-
request_timestamp,
|
176
|
-
response_timestamp,
|
177
|
-
process_time
|
178
|
-
)
|
179
|
-
log_func = getattr(self._logger, log_level)
|
219
|
+
client_ip:str,
|
220
|
+
request_id:UUID,
|
221
|
+
log_level:str = "info"
|
222
|
+
) -> None:
|
223
|
+
"""Log request and response details."""
|
224
|
+
authentication_info = self._get_authentication_info(authentication)
|
225
|
+
|
226
|
+
log_func = getattr(self.logger, log_level)
|
180
227
|
log_func(
|
181
|
-
f"Request
|
228
|
+
f"Request | ID: {request_id} {authentication_info} | IP: {client_ip} | "
|
229
|
+
f"Host: {request.client.host} | Port: {request.client.port} | "
|
230
|
+
f"Method: {request.method} | URL: {request.url.path} | "
|
182
231
|
f"Headers: {dict(request.headers)} - Response | Status: {response.status_code}"
|
183
232
|
)
|
184
|
-
|
185
|
-
|
186
|
-
def _handle_exception(
|
233
|
+
|
234
|
+
def log_exception(
|
187
235
|
self,
|
188
236
|
request:Request,
|
189
237
|
authentication:Authentication,
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
traceback_str = traceback.format_exc().split("\n")
|
238
|
+
error:Exception,
|
239
|
+
client_ip:str,
|
240
|
+
request_id:UUID
|
241
|
+
) -> None:
|
242
|
+
"""Log exception details."""
|
243
|
+
authentication_info = self._get_authentication_info(authentication)
|
244
|
+
|
198
245
|
error_details = {
|
246
|
+
"request_id": str(request_id),
|
199
247
|
"error": str(error),
|
200
|
-
"traceback":
|
248
|
+
"traceback": traceback.format_exc().split("\n"),
|
201
249
|
"client_ip": client_ip,
|
202
250
|
"method": request.method,
|
203
251
|
"url": request.url.path,
|
204
252
|
"headers": dict(request.headers),
|
205
253
|
}
|
206
254
|
|
207
|
-
|
208
|
-
|
209
|
-
|
255
|
+
self.logger.error(
|
256
|
+
f"Request | ID: {request_id} {authentication_info} | IP: {client_ip} | "
|
257
|
+
f"Host: {request.client.host} | Port: {request.client.port} | "
|
258
|
+
f"Method: {request.method} | URL: {request.url.path} | "
|
259
|
+
f"Headers: {dict(request.headers)} - Response | Status: 500 | "
|
260
|
+
f"Exception:\n{json.dumps(error_details, indent=4)}"
|
210
261
|
)
|
211
262
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
263
|
+
def _get_authentication_info(self, authentication:Authentication) -> str:
|
264
|
+
"""Get authentication info string."""
|
265
|
+
if not authentication.user.is_authenticated:
|
266
|
+
return "| Unauthenticated"
|
216
267
|
|
217
|
-
return
|
218
|
-
|
219
|
-
authentication
|
220
|
-
|
221
|
-
request_timestamp,
|
222
|
-
response_timestamp,
|
223
|
-
process_time
|
268
|
+
return (
|
269
|
+
f"| Token type: {authentication.credentials.token.type} | "
|
270
|
+
f"Username: {authentication.user.display_name} | "
|
271
|
+
f"Email: {authentication.user.identity}"
|
224
272
|
)
|
225
273
|
|
226
|
-
async def _request_processor(self, request:Request) -> Optional[Response]:
|
227
|
-
return None
|
228
274
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
275
|
+
class BaseMiddleware(BaseHTTPMiddleware):
|
276
|
+
"""Base middleware with rate limiting, logging, and response enhancement."""
|
277
|
+
|
278
|
+
def __init__(
|
279
|
+
self,
|
280
|
+
app:FastAPI,
|
281
|
+
keys:BaseGeneralSchemas.RSAKeys,
|
282
|
+
logger:MiddlewareLogger,
|
283
|
+
maleo_foundation:MaleoFoundationClientManager,
|
284
|
+
allow_origins:Sequence[str] = (),
|
285
|
+
allow_methods:Sequence[str] = ("GET",),
|
286
|
+
allow_headers:Sequence[str] = (),
|
287
|
+
allow_credentials:bool = False,
|
288
|
+
limit:int = 10,
|
289
|
+
window:int = 1,
|
290
|
+
cleanup_interval:int = 60,
|
291
|
+
ip_timeout:int = 300
|
292
|
+
):
|
293
|
+
super().__init__(app)
|
294
|
+
|
295
|
+
# Core components
|
296
|
+
self.rate_limiter = RateLimiter(
|
297
|
+
limit=limit,
|
298
|
+
window=timedelta(seconds=window),
|
299
|
+
ip_timeout=timedelta(seconds=ip_timeout),
|
300
|
+
cleanup_interval=timedelta(seconds=cleanup_interval)
|
237
301
|
)
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
302
|
+
self.response_builder = ResponseBuilder(keys, maleo_foundation)
|
303
|
+
self.request_logger = RequestLogger(logger)
|
304
|
+
|
305
|
+
# CORS settings (if needed)
|
306
|
+
self.cors_config = {
|
307
|
+
'allow_origins': allow_origins,
|
308
|
+
'allow_methods': allow_methods,
|
309
|
+
'allow_headers': allow_headers,
|
310
|
+
'allow_credentials': allow_credentials,
|
311
|
+
}
|
312
|
+
|
313
|
+
async def dispatch(self, request:Request, call_next:RequestResponseEndpoint) -> Response:
|
314
|
+
"""Main middleware dispatch method."""
|
315
|
+
# Generate unique request ID
|
316
|
+
request_id = uuid4()
|
317
|
+
request.state.request_id = request_id
|
318
|
+
|
319
|
+
# Setup
|
320
|
+
self.rate_limiter.cleanup_old_data(self.request_logger.logger)
|
321
|
+
request_timestamp = datetime.now(tz=timezone.utc)
|
322
|
+
request.state.request_timestamp = request_timestamp
|
323
|
+
start_time = time.perf_counter()
|
324
|
+
client_ip = BaseExtractors.extract_client_ip(request)
|
325
|
+
authentication = Authentication(credentials=request.auth, user=request.user)
|
242
326
|
|
243
327
|
try:
|
244
|
-
|
245
|
-
if self.
|
246
|
-
return self.
|
247
|
-
request
|
248
|
-
authentication=authentication,
|
249
|
-
authentication_info=authentication_info,
|
250
|
-
response=JSONResponse(
|
251
|
-
content=BaseResponses.RateLimitExceeded().model_dump(),
|
252
|
-
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
253
|
-
),
|
254
|
-
request_timestamp=request_timestamp,
|
255
|
-
response_timestamp=datetime.now(tz=timezone.utc),
|
256
|
-
process_time=time.perf_counter() - start_time,
|
257
|
-
log_level="warning",
|
258
|
-
client_ip=client_ip,
|
328
|
+
# Rate limiting check
|
329
|
+
if self.rate_limiter.is_rate_limited(client_ip):
|
330
|
+
return self._create_rate_limit_response(
|
331
|
+
request, authentication, client_ip, request_timestamp, start_time, request_id
|
259
332
|
)
|
260
333
|
|
261
|
-
|
334
|
+
# Optional preprocessing
|
262
335
|
pre_response = await self._request_processor(request)
|
263
336
|
if pre_response is not None:
|
264
|
-
return self.
|
265
|
-
request
|
266
|
-
|
267
|
-
authentication_info=authentication_info,
|
268
|
-
response=pre_response,
|
269
|
-
request_timestamp=request_timestamp,
|
270
|
-
response_timestamp=datetime.now(tz=timezone.utc),
|
271
|
-
process_time=time.perf_counter() - start_time,
|
272
|
-
log_level="info",
|
273
|
-
client_ip=client_ip,
|
337
|
+
return self._build_final_response(
|
338
|
+
request, authentication, pre_response, client_ip,
|
339
|
+
request_timestamp, start_time, request_id
|
274
340
|
)
|
275
341
|
|
276
|
-
|
342
|
+
# Main request processing
|
277
343
|
response = await call_next(request)
|
278
|
-
|
279
|
-
request
|
280
|
-
|
281
|
-
authentication_info=authentication_info,
|
282
|
-
response=response,
|
283
|
-
request_timestamp=request_timestamp,
|
284
|
-
response_timestamp=datetime.now(tz=timezone.utc),
|
285
|
-
process_time=time.perf_counter() - start_time,
|
286
|
-
log_level="info",
|
287
|
-
client_ip=client_ip,
|
344
|
+
return self._build_final_response(
|
345
|
+
request, authentication, response, client_ip,
|
346
|
+
request_timestamp, start_time, request_id
|
288
347
|
)
|
289
348
|
|
290
|
-
return response
|
291
|
-
|
292
349
|
except Exception as e:
|
293
350
|
return self._handle_exception(
|
294
|
-
request
|
295
|
-
authentication=authentication,
|
296
|
-
authentication_info=authentication_info,
|
297
|
-
error=e,
|
298
|
-
request_timestamp=request_timestamp,
|
299
|
-
response_timestamp=datetime.now(tz=timezone.utc),
|
300
|
-
process_time=time.perf_counter() - start_time,
|
301
|
-
client_ip=client_ip
|
351
|
+
request, authentication, e, client_ip, request_timestamp, start_time, request_id
|
302
352
|
)
|
303
353
|
|
354
|
+
async def _request_processor(self, request:Request) -> Optional[Response]:
|
355
|
+
"""Override this method for custom request preprocessing."""
|
356
|
+
return None
|
357
|
+
|
358
|
+
def _create_rate_limit_response(
|
359
|
+
self,
|
360
|
+
request:Request,
|
361
|
+
authentication:Authentication,
|
362
|
+
client_ip:str,
|
363
|
+
request_timestamp:datetime,
|
364
|
+
start_time:float,
|
365
|
+
request_id:UUID
|
366
|
+
) -> Response:
|
367
|
+
"""Create rate limit exceeded response."""
|
368
|
+
response = JSONResponse(
|
369
|
+
content=BaseResponses.RateLimitExceeded().model_dump(),
|
370
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
371
|
+
)
|
372
|
+
|
373
|
+
return self._build_final_response(
|
374
|
+
request, authentication, response, client_ip,
|
375
|
+
request_timestamp, start_time, request_id, log_level="warning"
|
376
|
+
)
|
377
|
+
|
378
|
+
def _build_final_response(
|
379
|
+
self,
|
380
|
+
request:Request,
|
381
|
+
authentication:Authentication,
|
382
|
+
response:Response,
|
383
|
+
client_ip:str,
|
384
|
+
request_timestamp:datetime,
|
385
|
+
start_time:float,
|
386
|
+
request_id:UUID,
|
387
|
+
log_level:str = "info"
|
388
|
+
) -> Response:
|
389
|
+
"""Build final response with headers and logging."""
|
390
|
+
response_timestamp = datetime.now(tz=timezone.utc)
|
391
|
+
process_time = time.perf_counter() - start_time
|
392
|
+
|
393
|
+
# Add headers
|
394
|
+
response = self.response_builder.add_response_headers(
|
395
|
+
request, authentication, response, request_timestamp, response_timestamp, process_time, request_id
|
396
|
+
)
|
397
|
+
|
398
|
+
# Log request/response
|
399
|
+
self.request_logger.log_request_response(
|
400
|
+
request, authentication, response, client_ip, request_id, log_level
|
401
|
+
)
|
402
|
+
|
403
|
+
return response
|
404
|
+
|
405
|
+
def _handle_exception(
|
406
|
+
self,
|
407
|
+
request:Request,
|
408
|
+
authentication:Authentication,
|
409
|
+
error:Exception,
|
410
|
+
client_ip:str,
|
411
|
+
request_timestamp:datetime,
|
412
|
+
start_time:float,
|
413
|
+
request_id:UUID
|
414
|
+
) -> Response:
|
415
|
+
"""Handle exceptions and create error response."""
|
416
|
+
response_timestamp = datetime.now(tz=timezone.utc)
|
417
|
+
process_time = time.perf_counter() - start_time
|
418
|
+
|
419
|
+
response = JSONResponse(
|
420
|
+
content=BaseResponses.ServerError().model_dump(),
|
421
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
422
|
+
)
|
423
|
+
|
424
|
+
# Log exception
|
425
|
+
self.request_logger.log_exception(request, authentication, error, client_ip, request_id)
|
426
|
+
|
427
|
+
# Add headers
|
428
|
+
return self.response_builder.add_response_headers(
|
429
|
+
request, authentication, response, request_timestamp, response_timestamp, process_time, request_id
|
430
|
+
)
|
431
|
+
|
432
|
+
|
304
433
|
def add_base_middleware(
|
305
434
|
app:FastAPI,
|
306
435
|
keys:BaseGeneralSchemas.RSAKeys,
|
@@ -316,39 +445,35 @@ def add_base_middleware(
|
|
316
445
|
ip_timeout:int = 300
|
317
446
|
) -> None:
|
318
447
|
"""
|
319
|
-
|
448
|
+
Add Base middleware to the FastAPI application.
|
320
449
|
|
321
450
|
Args:
|
322
|
-
app:
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
window:
|
332
|
-
|
333
|
-
|
334
|
-
cleanup_interval: int
|
335
|
-
How often to clean up old IP data (in seconds).
|
336
|
-
|
337
|
-
ip_timeout: int
|
338
|
-
How long to keep an IP in memory after its last activity (in seconds).
|
339
|
-
Default is 300 seconds (5 minutes).
|
340
|
-
|
341
|
-
Returns:
|
342
|
-
None: The function modifies the FastAPI app by adding Base middleware.
|
343
|
-
|
344
|
-
Note:
|
345
|
-
FastAPI applies middleware in reverse order of registration, so this middleware
|
346
|
-
will execute after any middleware added subsequently.
|
451
|
+
app:FastAPI application instance
|
452
|
+
keys:RSA keys for signing and token generation
|
453
|
+
logger:Middleware logger instance
|
454
|
+
maleo_foundation:Client manager for foundation services
|
455
|
+
allow_origins:CORS allowed origins
|
456
|
+
allow_methods:CORS allowed methods
|
457
|
+
allow_headers:CORS allowed headers
|
458
|
+
allow_credentials:CORS allow credentials flag
|
459
|
+
limit:Request count limit per window
|
460
|
+
window:Time window for rate limiting (seconds)
|
461
|
+
cleanup_interval:Cleanup interval for old IP data (seconds)
|
462
|
+
ip_timeout:IP timeout after last activity (seconds)
|
347
463
|
|
348
464
|
Example:
|
349
|
-
|
350
|
-
|
351
|
-
|
465
|
+
```python
|
466
|
+
add_base_middleware(
|
467
|
+
app=app,
|
468
|
+
keys=rsa_keys,
|
469
|
+
logger=middleware_logger,
|
470
|
+
maleo_foundation=client_manager,
|
471
|
+
limit=10,
|
472
|
+
window=1,
|
473
|
+
cleanup_interval=60,
|
474
|
+
ip_timeout=300
|
475
|
+
)
|
476
|
+
```
|
352
477
|
"""
|
353
478
|
app.add_middleware(
|
354
479
|
BaseMiddleware,
|
@@ -34,7 +34,7 @@ maleo_foundation/expanded_types/encryption/rsa.py,sha256=Esf_H8nMz2kOLAWa3M7dlD-
|
|
34
34
|
maleo_foundation/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
35
|
maleo_foundation/managers/db.py,sha256=cpY1IOiUytT9XXYtzS0E9OSYOuB7jBKo0XHe__uI1Jg,5340
|
36
36
|
maleo_foundation/managers/middleware.py,sha256=77wRCC_CWc22nSDL-UJanO3rXmSB7wLzaAIXEFjXq6M,4249
|
37
|
-
maleo_foundation/managers/service.py,sha256=
|
37
|
+
maleo_foundation/managers/service.py,sha256=n2ubW5NoeBBju0t0fLSWa0FxeZMLzmoRcimuCIobgzk,19284
|
38
38
|
maleo_foundation/managers/cache/__init__.py,sha256=CeY0oof2bVl_v5WS-FKXNwn2gf3xrEMfUsHK9cHo59s,471
|
39
39
|
maleo_foundation/managers/cache/base.py,sha256=YyPjde4KTsp2IHV6NdFMysa0ev-1GX1rtX-0jQPuIBU,837
|
40
40
|
maleo_foundation/managers/cache/redis.py,sha256=xLa8QfXdNtghs0eBxIqc04H3XTYmxLEzrqJZAFCigvM,1150
|
@@ -47,7 +47,7 @@ maleo_foundation/managers/client/google/parameter.py,sha256=Lnj7mQgxWQpsQwbmDRK5
|
|
47
47
|
maleo_foundation/managers/client/google/secret.py,sha256=Ski1CHYeA8vjSk2Oc2Pf4CfFrzT_RcA6NEZwza7gM7Y,4464
|
48
48
|
maleo_foundation/managers/client/google/storage.py,sha256=Hq79cVreWR2AGwUzpD57NzziZjqxj__TqYIBRy-A9A0,5340
|
49
49
|
maleo_foundation/middlewares/authentication.py,sha256=UL6kL65SvqrzemlIDopoO9N1C05eWlYMHVR2tiRsVEA,4821
|
50
|
-
maleo_foundation/middlewares/base.py,sha256=
|
50
|
+
maleo_foundation/middlewares/base.py,sha256=R-N8rxUaPt7_mvK6DI8y8Jal8GfzuD7Yyx3VLp9_tQA,17097
|
51
51
|
maleo_foundation/middlewares/cors.py,sha256=9uvBvY2N6Vxa9RP_YtESxcWo6Doi6uS0lzAG9iLY7Uc,2288
|
52
52
|
maleo_foundation/models/__init__.py,sha256=AaKehO7c1HyKhoTGRmNHDddSeBXkW-_YNrpOGBu8Ms8,246
|
53
53
|
maleo_foundation/models/responses.py,sha256=nE5qThK-WgcYB-9J4wHzJltMA3PLmWbMI-dkxAxAdII,5631
|
@@ -118,7 +118,7 @@ maleo_foundation/utils/loaders/credential/__init__.py,sha256=qopTKvcMVoTFwyRijeg
|
|
118
118
|
maleo_foundation/utils/loaders/credential/google.py,sha256=HUcuHD4tXHPt0eHInlFYxA_MDrGSOtbenpd0PX156OM,1255
|
119
119
|
maleo_foundation/utils/loaders/key/__init__.py,sha256=hVygcC2ImHc_aVrSrOmyedR8tMUZokWUKCKOSh5ctbo,106
|
120
120
|
maleo_foundation/utils/loaders/key/rsa.py,sha256=gDhyX6iTFtHiluuhFCozaZ3pOLKU2Y9TlrNMK_GVyGU,3796
|
121
|
-
maleo_foundation-0.2.
|
122
|
-
maleo_foundation-0.2.
|
123
|
-
maleo_foundation-0.2.
|
124
|
-
maleo_foundation-0.2.
|
121
|
+
maleo_foundation-0.2.80.dist-info/METADATA,sha256=dRNIGvejuDcdeeazla9MoD2p0H1LGUATumhyh2XTB00,3598
|
122
|
+
maleo_foundation-0.2.80.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
123
|
+
maleo_foundation-0.2.80.dist-info/top_level.txt,sha256=_iBos3F_bhEOdjOnzeiEYSrCucasc810xXtLBXI8cQc,17
|
124
|
+
maleo_foundation-0.2.80.dist-info/RECORD,,
|
File without changes
|
File without changes
|