pywebguard 0.0.0__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.
- pywebguard/__init__.py +87 -0
- pywebguard/cli.py +1022 -0
- pywebguard/core/__init__.py +0 -0
- pywebguard/core/base.py +792 -0
- pywebguard/core/config.py +455 -0
- pywebguard/filters/__init__.py +0 -0
- pywebguard/filters/base.py +44 -0
- pywebguard/filters/ip_filter.py +233 -0
- pywebguard/filters/user_agent.py +102 -0
- pywebguard/frameworks/__init__.py +0 -0
- pywebguard/frameworks/_fastapi.py +191 -0
- pywebguard/frameworks/_flask.py +182 -0
- pywebguard/limiters/__init__.py +0 -0
- pywebguard/limiters/base.py +46 -0
- pywebguard/limiters/rate_limit.py +415 -0
- pywebguard/logging/__init__.py +0 -0
- pywebguard/logging/backends/__init__.py +17 -0
- pywebguard/logging/backends/_elasticsearch.py +307 -0
- pywebguard/logging/backends/_meilisearch.py +310 -0
- pywebguard/logging/backends/_mongodb.py +303 -0
- pywebguard/logging/base.py +117 -0
- pywebguard/logging/logger.py +408 -0
- pywebguard/security/__init__.py +0 -0
- pywebguard/security/base.py +44 -0
- pywebguard/security/cors.py +204 -0
- pywebguard/security/penetration.py +265 -0
- pywebguard/storage/__init__.py +0 -0
- pywebguard/storage/_mongodb.py +396 -0
- pywebguard/storage/_postgresql.py +539 -0
- pywebguard/storage/_redis.py +314 -0
- pywebguard/storage/_sqlite.py +374 -0
- pywebguard/storage/_tinydb.py +271 -0
- pywebguard/storage/base.py +327 -0
- pywebguard/storage/memory.py +245 -0
- pywebguard/utils/__init__.py +0 -0
- pywebguard/utils/ip.py +84 -0
- pywebguard/utils/request.py +65 -0
- pywebguard-0.0.0.dist-info/LICENSE +21 -0
- pywebguard-0.0.0.dist-info/METADATA +368 -0
- pywebguard-0.0.0.dist-info/RECORD +43 -0
- pywebguard-0.0.0.dist-info/WHEEL +5 -0
- pywebguard-0.0.0.dist-info/entry_points.txt +2 -0
- pywebguard-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IP filtering functionality for PyWebGuard with both sync and async support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional, Union, Any
|
|
6
|
+
import ipaddress
|
|
7
|
+
from pywebguard.core.config import IPFilterConfig
|
|
8
|
+
from pywebguard.storage.base import BaseStorage, AsyncBaseStorage
|
|
9
|
+
from pywebguard.filters.base import BaseFilter, AsyncBaseFilter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IPFilter(BaseFilter):
|
|
13
|
+
"""
|
|
14
|
+
Filter requests based on IP addresses (synchronous).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
config: IPFilterConfig,
|
|
20
|
+
storage: BaseStorage,
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the IP filter.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: IP filter configuration
|
|
27
|
+
storage: Storage backend for persistent data
|
|
28
|
+
"""
|
|
29
|
+
self.config = config
|
|
30
|
+
self.storage = storage
|
|
31
|
+
|
|
32
|
+
# Parse IP networks for efficient matching
|
|
33
|
+
self.whitelist_networks = self._parse_ip_networks(config.whitelist)
|
|
34
|
+
self.blacklist_networks = self._parse_ip_networks(config.blacklist)
|
|
35
|
+
|
|
36
|
+
def _parse_ip_networks(self, ip_list: List[str]) -> List:
|
|
37
|
+
"""
|
|
38
|
+
Parse IP addresses and CIDR ranges into network objects.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
ip_list: List of IP addresses and CIDR ranges
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of network objects
|
|
45
|
+
"""
|
|
46
|
+
networks = []
|
|
47
|
+
for ip in ip_list:
|
|
48
|
+
try:
|
|
49
|
+
# Handle CIDR notation
|
|
50
|
+
if "/" in ip:
|
|
51
|
+
networks.append(ipaddress.ip_network(ip, strict=False))
|
|
52
|
+
else:
|
|
53
|
+
# Convert single IP to network with /32 or /128 mask
|
|
54
|
+
networks.append(ipaddress.ip_address(ip))
|
|
55
|
+
except ValueError:
|
|
56
|
+
# Log invalid IP address
|
|
57
|
+
pass
|
|
58
|
+
return networks
|
|
59
|
+
|
|
60
|
+
def is_allowed(self, ip_address: str) -> Dict[str, Union[bool, str]]:
|
|
61
|
+
"""
|
|
62
|
+
Check if an IP address is allowed.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
ip_address: The IP address to check
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict with allowed status and reason
|
|
69
|
+
"""
|
|
70
|
+
if not self.config.enabled:
|
|
71
|
+
return {"allowed": True, "reason": ""}
|
|
72
|
+
|
|
73
|
+
# Re-parse networks in case config changed at runtime
|
|
74
|
+
self.whitelist_networks = self._parse_ip_networks(self.config.whitelist)
|
|
75
|
+
self.blacklist_networks = self._parse_ip_networks(self.config.blacklist)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
ip = ipaddress.ip_address(ip_address)
|
|
79
|
+
|
|
80
|
+
# Check if IP is banned (highest priority)
|
|
81
|
+
if self.storage.exists(f"banned_ip:{ip_address}"):
|
|
82
|
+
return {"allowed": False, "reason": "IP is banned"}
|
|
83
|
+
|
|
84
|
+
# Check if IP is in blacklist (second priority)
|
|
85
|
+
if self._is_ip_in_networks(ip, self.blacklist_networks):
|
|
86
|
+
return {"allowed": False, "reason": "IP in blacklist"}
|
|
87
|
+
|
|
88
|
+
# Check if IP is in whitelist (lowest priority)
|
|
89
|
+
if self.whitelist_networks and not self._is_ip_in_networks(
|
|
90
|
+
ip, self.whitelist_networks
|
|
91
|
+
):
|
|
92
|
+
return {"allowed": False, "reason": "IP not in whitelist"}
|
|
93
|
+
|
|
94
|
+
# Additional checks for cloud providers, geolocation, etc. would go here
|
|
95
|
+
|
|
96
|
+
return {"allowed": True, "reason": ""}
|
|
97
|
+
|
|
98
|
+
except ValueError:
|
|
99
|
+
# Invalid IP address
|
|
100
|
+
return {"allowed": False, "reason": "Invalid IP address"}
|
|
101
|
+
|
|
102
|
+
def _is_ip_in_networks(self, ip, networks) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Check if an IP is in a list of networks.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
ip: IP address to check
|
|
108
|
+
networks: List of networks to check against
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if IP is in any network, False otherwise
|
|
112
|
+
"""
|
|
113
|
+
for network in networks:
|
|
114
|
+
if isinstance(network, ipaddress.IPv4Network) or isinstance(
|
|
115
|
+
network, ipaddress.IPv6Network
|
|
116
|
+
):
|
|
117
|
+
if ip in network:
|
|
118
|
+
return True
|
|
119
|
+
elif ip == network: # Direct comparison for single IP addresses
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AsyncIPFilter(AsyncBaseFilter):
|
|
125
|
+
"""
|
|
126
|
+
Filter requests based on IP addresses (asynchronous).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
config: IPFilterConfig,
|
|
132
|
+
storage: AsyncBaseStorage,
|
|
133
|
+
):
|
|
134
|
+
"""
|
|
135
|
+
Initialize the async IP filter.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
config: IP filter configuration
|
|
139
|
+
storage: Async storage backend for persistent data
|
|
140
|
+
"""
|
|
141
|
+
self.config = config
|
|
142
|
+
self.storage = storage
|
|
143
|
+
|
|
144
|
+
# Parse IP networks for efficient matching
|
|
145
|
+
self.whitelist_networks = self._parse_ip_networks(config.whitelist)
|
|
146
|
+
self.blacklist_networks = self._parse_ip_networks(config.blacklist)
|
|
147
|
+
|
|
148
|
+
def _parse_ip_networks(self, ip_list: List[str]) -> List:
|
|
149
|
+
"""
|
|
150
|
+
Parse IP addresses and CIDR ranges into network objects.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
ip_list: List of IP addresses and CIDR ranges
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of network objects
|
|
157
|
+
"""
|
|
158
|
+
networks = []
|
|
159
|
+
for ip in ip_list:
|
|
160
|
+
try:
|
|
161
|
+
# Handle CIDR notation
|
|
162
|
+
if "/" in ip:
|
|
163
|
+
networks.append(ipaddress.ip_network(ip, strict=False))
|
|
164
|
+
else:
|
|
165
|
+
# Convert single IP to network with /32 or /128 mask
|
|
166
|
+
networks.append(ipaddress.ip_address(ip))
|
|
167
|
+
except ValueError:
|
|
168
|
+
# Log invalid IP address
|
|
169
|
+
pass
|
|
170
|
+
return networks
|
|
171
|
+
|
|
172
|
+
async def is_allowed(self, ip_address: str) -> Dict[str, Union[bool, str]]:
|
|
173
|
+
"""
|
|
174
|
+
Check if an IP address is allowed asynchronously.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
ip_address: The IP address to check
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dict with allowed status and reason
|
|
181
|
+
"""
|
|
182
|
+
if not self.config.enabled:
|
|
183
|
+
return {"allowed": True, "reason": ""}
|
|
184
|
+
|
|
185
|
+
# Re-parse networks in case config changed at runtime
|
|
186
|
+
self.whitelist_networks = self._parse_ip_networks(self.config.whitelist)
|
|
187
|
+
self.blacklist_networks = self._parse_ip_networks(self.config.blacklist)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
ip = ipaddress.ip_address(ip_address)
|
|
191
|
+
|
|
192
|
+
# Check if IP is banned (highest priority)
|
|
193
|
+
if await self.storage.exists(f"banned_ip:{ip_address}"):
|
|
194
|
+
return {"allowed": False, "reason": "IP is banned"}
|
|
195
|
+
|
|
196
|
+
# Check if IP is in blacklist (second priority)
|
|
197
|
+
if self._is_ip_in_networks(ip, self.blacklist_networks):
|
|
198
|
+
return {"allowed": False, "reason": "IP in blacklist"}
|
|
199
|
+
|
|
200
|
+
# Check if IP is in whitelist (lowest priority)
|
|
201
|
+
if self.whitelist_networks and not self._is_ip_in_networks(
|
|
202
|
+
ip, self.whitelist_networks
|
|
203
|
+
):
|
|
204
|
+
return {"allowed": False, "reason": "IP not in whitelist"}
|
|
205
|
+
|
|
206
|
+
# Additional checks for cloud providers, geolocation, etc. would go here
|
|
207
|
+
|
|
208
|
+
return {"allowed": True, "reason": ""}
|
|
209
|
+
|
|
210
|
+
except ValueError:
|
|
211
|
+
# Invalid IP address
|
|
212
|
+
return {"allowed": False, "reason": "Invalid IP address"}
|
|
213
|
+
|
|
214
|
+
def _is_ip_in_networks(self, ip, networks) -> bool:
|
|
215
|
+
"""
|
|
216
|
+
Check if an IP is in a list of networks.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
ip: IP address to check
|
|
220
|
+
networks: List of networks to check against
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if IP is in any network, False otherwise
|
|
224
|
+
"""
|
|
225
|
+
for network in networks:
|
|
226
|
+
if isinstance(network, ipaddress.IPv4Network) or isinstance(
|
|
227
|
+
network, ipaddress.IPv6Network
|
|
228
|
+
):
|
|
229
|
+
if ip in network:
|
|
230
|
+
return True
|
|
231
|
+
elif ip == network: # Direct comparison for single IP addresses
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User agent filtering functionality for PyWebGuard with both sync and async support.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Union
|
|
6
|
+
from pywebguard.core.config import UserAgentConfig
|
|
7
|
+
from pywebguard.storage.base import BaseStorage, AsyncBaseStorage
|
|
8
|
+
from pywebguard.filters.base import BaseFilter, AsyncBaseFilter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UserAgentFilter(BaseFilter):
|
|
12
|
+
"""
|
|
13
|
+
Filter requests based on user agent (synchronous).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
config: UserAgentConfig,
|
|
19
|
+
storage: BaseStorage,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the user agent filter.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config: User agent filter configuration
|
|
26
|
+
storage: Storage backend for persistent data
|
|
27
|
+
"""
|
|
28
|
+
self.config = config
|
|
29
|
+
self.storage = storage
|
|
30
|
+
|
|
31
|
+
def is_allowed(self, user_agent: str) -> Dict[str, Union[bool, str]]:
|
|
32
|
+
"""
|
|
33
|
+
Check if a user agent is allowed.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
user_agent: The user agent string to check
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dict with allowed status and reason
|
|
40
|
+
"""
|
|
41
|
+
if not self.config.enabled:
|
|
42
|
+
return {"allowed": True, "reason": ""}
|
|
43
|
+
|
|
44
|
+
if not user_agent:
|
|
45
|
+
return {"allowed": False, "reason": "Empty user agent"}
|
|
46
|
+
|
|
47
|
+
# Check if user agent is in blocked list
|
|
48
|
+
for blocked_agent in self.config.blocked_agents:
|
|
49
|
+
if blocked_agent.lower() in user_agent.lower():
|
|
50
|
+
return {
|
|
51
|
+
"allowed": False,
|
|
52
|
+
"reason": f"Blocked user agent: {blocked_agent}",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {"allowed": True, "reason": ""}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AsyncUserAgentFilter(AsyncBaseFilter):
|
|
59
|
+
"""
|
|
60
|
+
Filter requests based on user agent (asynchronous).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
config: UserAgentConfig,
|
|
66
|
+
storage: AsyncBaseStorage,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize the async user agent filter.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
config: User agent filter configuration
|
|
73
|
+
storage: Async storage backend for persistent data
|
|
74
|
+
"""
|
|
75
|
+
self.config = config
|
|
76
|
+
self.storage = storage
|
|
77
|
+
|
|
78
|
+
async def is_allowed(self, user_agent: str) -> Dict[str, Union[bool, str]]:
|
|
79
|
+
"""
|
|
80
|
+
Check if a user agent is allowed asynchronously.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
user_agent: The user agent string to check
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dict with allowed status and reason
|
|
87
|
+
"""
|
|
88
|
+
if not self.config.enabled:
|
|
89
|
+
return {"allowed": True, "reason": ""}
|
|
90
|
+
|
|
91
|
+
if not user_agent:
|
|
92
|
+
return {"allowed": False, "reason": "Empty user agent"}
|
|
93
|
+
|
|
94
|
+
# Check if user agent is in blocked list
|
|
95
|
+
for blocked_agent in self.config.blocked_agents:
|
|
96
|
+
if blocked_agent.lower() in user_agent.lower():
|
|
97
|
+
return {
|
|
98
|
+
"allowed": False,
|
|
99
|
+
"reason": f"Blocked user agent: {blocked_agent}",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {"allowed": True, "reason": ""}
|
|
File without changes
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI integration for PyWebGuard.
|
|
3
|
+
|
|
4
|
+
This module provides middleware for integrating PyWebGuard with FastAPI applications.
|
|
5
|
+
It handles request interception, security checks, and response processing.
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
FastAPIGuard: FastAPI middleware for PyWebGuard
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Callable, Dict, Any, Optional, List, Union
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
# Check if FastAPI is installed
|
|
15
|
+
try:
|
|
16
|
+
from fastapi import FastAPI, Request, Response
|
|
17
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
18
|
+
from fastapi.responses import JSONResponse
|
|
19
|
+
|
|
20
|
+
FASTAPI_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
FASTAPI_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
# Create dummy classes for type checking
|
|
25
|
+
class BaseHTTPMiddleware:
|
|
26
|
+
def __init__(self, app):
|
|
27
|
+
self.app = app
|
|
28
|
+
|
|
29
|
+
class Request:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
class Response:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
class JSONResponse:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
from pywebguard.core.base import Guard, AsyncGuard
|
|
40
|
+
from pywebguard.core.config import GuardConfig
|
|
41
|
+
from pywebguard.storage.base import BaseStorage, AsyncBaseStorage
|
|
42
|
+
from pywebguard.storage.memory import MemoryStorage, AsyncMemoryStorage
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FastAPIGuard(BaseHTTPMiddleware):
|
|
46
|
+
"""
|
|
47
|
+
FastAPI middleware for PyWebGuard supporting both sync and async guards.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
app,
|
|
53
|
+
config: Optional[GuardConfig] = None,
|
|
54
|
+
storage: Optional[Union[AsyncBaseStorage, BaseStorage]] = None,
|
|
55
|
+
route_rate_limits: Optional[List[Dict[str, Any]]] = None,
|
|
56
|
+
custom_response_handler: Optional[Callable] = None,
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Initialize the FastAPI guard.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
app: The FastAPI application
|
|
63
|
+
config: Guard configuration
|
|
64
|
+
storage: Storage backend
|
|
65
|
+
route_rate_limits: Optional list of route-specific rate limits
|
|
66
|
+
custom_response_handler: Optional custom response handler for blocked requests
|
|
67
|
+
"""
|
|
68
|
+
if not FASTAPI_AVAILABLE:
|
|
69
|
+
raise ImportError(
|
|
70
|
+
"FastAPI is not installed. Install it with 'pip install pywebguard[fastapi]' "
|
|
71
|
+
"or 'pip install fastapi>=0.68.0 starlette>=0.14.0'"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
super().__init__(app)
|
|
75
|
+
self.app = app
|
|
76
|
+
|
|
77
|
+
# Use AsyncGuard instead of Guard
|
|
78
|
+
if isinstance(storage, AsyncBaseStorage):
|
|
79
|
+
self.guard = AsyncGuard(config, storage)
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"Storage must be an instance of AsyncBaseStorage for FastAPI integration"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Add route-specific rate limits if provided
|
|
86
|
+
if route_rate_limits:
|
|
87
|
+
for route in route_rate_limits:
|
|
88
|
+
self.guard.rate_limiter.add_route_config(route["endpoint"], route)
|
|
89
|
+
|
|
90
|
+
# Set up default response handler if none provided
|
|
91
|
+
if custom_response_handler is None:
|
|
92
|
+
custom_response_handler = self._default_response_handler
|
|
93
|
+
self.custom_response_handler = custom_response_handler
|
|
94
|
+
|
|
95
|
+
async def _default_response_handler(
|
|
96
|
+
self, request: Request, reason: str
|
|
97
|
+
) -> Response:
|
|
98
|
+
"""
|
|
99
|
+
Default response handler for blocked requests.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
request: The FastAPI request object
|
|
103
|
+
reason: The reason the request was blocked
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A JSON response with details about why the request was blocked
|
|
107
|
+
"""
|
|
108
|
+
status_code = 429 if "rate limit" in reason.lower() else 403
|
|
109
|
+
|
|
110
|
+
return JSONResponse(
|
|
111
|
+
status_code=status_code,
|
|
112
|
+
content={
|
|
113
|
+
"error": "Request blocked",
|
|
114
|
+
"reason": reason,
|
|
115
|
+
"timestamp": time.time(),
|
|
116
|
+
"path": request.url.path,
|
|
117
|
+
"method": request.method,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def dispatch(self, request: Request, call_next):
|
|
122
|
+
"""
|
|
123
|
+
Process the request through the middleware.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
request: The FastAPI request object
|
|
127
|
+
call_next: The next middleware/handler in the chain
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The response from the next middleware/handler
|
|
131
|
+
"""
|
|
132
|
+
# Store the guard instance in the app's state if not already stored
|
|
133
|
+
if not hasattr(request.app.state, "guard"):
|
|
134
|
+
request.app.state.guard = self
|
|
135
|
+
|
|
136
|
+
client_ip = request.client.host
|
|
137
|
+
|
|
138
|
+
# Check if IP is banned first
|
|
139
|
+
is_banned = await self.guard.is_ip_banned(client_ip)
|
|
140
|
+
if is_banned:
|
|
141
|
+
ban_info = await self.guard.storage.get(f"banned_ip:{client_ip}")
|
|
142
|
+
response = await self.custom_response_handler(
|
|
143
|
+
request,
|
|
144
|
+
f"IP is banned: {ban_info.get('reason', 'Unknown reason')}",
|
|
145
|
+
)
|
|
146
|
+
# Log blocked request
|
|
147
|
+
request_info = {
|
|
148
|
+
"ip": client_ip,
|
|
149
|
+
"method": request.method,
|
|
150
|
+
"path": request.url.path,
|
|
151
|
+
"user_agent": request.headers.get("user-agent", "unknown"),
|
|
152
|
+
}
|
|
153
|
+
await self.guard.logger.log_blocked_request(
|
|
154
|
+
request_info,
|
|
155
|
+
"ip_ban",
|
|
156
|
+
f"IP is banned: {ban_info.get('reason', 'Unknown reason')}",
|
|
157
|
+
)
|
|
158
|
+
return response
|
|
159
|
+
|
|
160
|
+
# Check rate limits
|
|
161
|
+
rate_info = await self.guard.rate_limiter.check_limit(
|
|
162
|
+
client_ip, request.url.path
|
|
163
|
+
)
|
|
164
|
+
if not rate_info["allowed"]:
|
|
165
|
+
response = await self.custom_response_handler(request, rate_info["reason"])
|
|
166
|
+
# Log blocked request
|
|
167
|
+
request_info = {
|
|
168
|
+
"ip": client_ip,
|
|
169
|
+
"method": request.method,
|
|
170
|
+
"path": request.url.path,
|
|
171
|
+
"user_agent": request.headers.get("user-agent", "unknown"),
|
|
172
|
+
}
|
|
173
|
+
await self.guard.logger.log_security_event(
|
|
174
|
+
"WARNING",
|
|
175
|
+
f"Blocked request: {request.method} {request.url.path} - {rate_info['reason']}",
|
|
176
|
+
)
|
|
177
|
+
return response
|
|
178
|
+
|
|
179
|
+
# Continue with the request
|
|
180
|
+
response = await call_next(request)
|
|
181
|
+
|
|
182
|
+
# Log successful request
|
|
183
|
+
request_info = {
|
|
184
|
+
"ip": client_ip,
|
|
185
|
+
"method": request.method,
|
|
186
|
+
"path": request.url.path,
|
|
187
|
+
"user_agent": request.headers.get("user-agent", "unknown"),
|
|
188
|
+
}
|
|
189
|
+
await self.guard.logger.log_request(request_info, response)
|
|
190
|
+
|
|
191
|
+
return response
|