drogue 0.1.0__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.
- drogue-0.1.0/.gitignore +57 -0
- drogue-0.1.0/LICENSE +21 -0
- drogue-0.1.0/PKG-INFO +278 -0
- drogue-0.1.0/README.md +217 -0
- drogue-0.1.0/_integration_test/__init__.py +0 -0
- drogue-0.1.0/_integration_test/django_settings.py +25 -0
- drogue-0.1.0/_integration_test/django_urls.py +11 -0
- drogue-0.1.0/_integration_test/django_views.py +41 -0
- drogue-0.1.0/_integration_test/fastapi_app.py +152 -0
- drogue-0.1.0/_integration_test/test_integration.py +405 -0
- drogue-0.1.0/docs/advanced/adaptive.md +109 -0
- drogue-0.1.0/docs/advanced/cidr.md +109 -0
- drogue-0.1.0/docs/advanced/honeypots.md +110 -0
- drogue-0.1.0/docs/advanced/probabilistic.md +130 -0
- drogue-0.1.0/docs/advanced/probes.md +107 -0
- drogue-0.1.0/docs/advanced/randomizer.md +82 -0
- drogue-0.1.0/docs/advanced/sentinel.md +107 -0
- drogue-0.1.0/docs/advanced/shadow.md +84 -0
- drogue-0.1.0/docs/advanced/trust.md +127 -0
- drogue-0.1.0/docs/advanced-features.md +228 -0
- drogue-0.1.0/docs/api/adapters.md +208 -0
- drogue-0.1.0/docs/api/core.md +162 -0
- drogue-0.1.0/docs/api/observability.md +218 -0
- drogue-0.1.0/docs/api/protection.md +303 -0
- drogue-0.1.0/docs/api/storage.md +250 -0
- drogue-0.1.0/docs/configuration/algorithms.md +111 -0
- drogue-0.1.0/docs/configuration/overview.md +139 -0
- drogue-0.1.0/docs/configuration/rate-limit-strings.md +76 -0
- drogue-0.1.0/docs/configuration.md +87 -0
- drogue-0.1.0/docs/development/changelog.md +85 -0
- drogue-0.1.0/docs/development/contributing.md +212 -0
- drogue-0.1.0/docs/development/testing.md +346 -0
- drogue-0.1.0/docs/features/observability.md +117 -0
- drogue-0.1.0/docs/features/protection.md +162 -0
- drogue-0.1.0/docs/features/rate-limiting.md +96 -0
- drogue-0.1.0/docs/features/storage.md +132 -0
- drogue-0.1.0/docs/getting-started/django.md +112 -0
- drogue-0.1.0/docs/getting-started/fastapi.md +88 -0
- drogue-0.1.0/docs/getting-started/flask.md +83 -0
- drogue-0.1.0/docs/getting-started/redis.md +91 -0
- drogue-0.1.0/docs/getting-started.md +78 -0
- drogue-0.1.0/docs/index.md +69 -0
- drogue-0.1.0/docs/migration/slowapi.md +126 -0
- drogue-0.1.0/examples/django_example_settings.py +29 -0
- drogue-0.1.0/examples/django_example_urls.py +12 -0
- drogue-0.1.0/examples/django_example_views.py +31 -0
- drogue-0.1.0/examples/fastapi_app.py +40 -0
- drogue-0.1.0/examples/fastapi_redis_app.py +59 -0
- drogue-0.1.0/examples/flask_app.py +43 -0
- drogue-0.1.0/mkdocs.yml +91 -0
- drogue-0.1.0/pyproject.toml +71 -0
- drogue-0.1.0/src/drogue/__init__.py +74 -0
- drogue-0.1.0/src/drogue/adapters/__init__.py +3 -0
- drogue-0.1.0/src/drogue/adapters/django/__init__.py +3 -0
- drogue-0.1.0/src/drogue/adapters/django/limiter.py +266 -0
- drogue-0.1.0/src/drogue/adapters/django/throttle.py +131 -0
- drogue-0.1.0/src/drogue/adapters/fastapi/__init__.py +3 -0
- drogue-0.1.0/src/drogue/adapters/fastapi/limiter.py +541 -0
- drogue-0.1.0/src/drogue/adapters/flask/__init__.py +4 -0
- drogue-0.1.0/src/drogue/adapters/flask/limiter.py +233 -0
- drogue-0.1.0/src/drogue/core/__init__.py +18 -0
- drogue-0.1.0/src/drogue/core/abstracts.py +177 -0
- drogue-0.1.0/src/drogue/core/algorithms/__init__.py +5 -0
- drogue-0.1.0/src/drogue/core/algorithms/fixed_window.py +141 -0
- drogue-0.1.0/src/drogue/core/algorithms/sliding_window.py +171 -0
- drogue-0.1.0/src/drogue/core/algorithms/token_bucket.py +232 -0
- drogue-0.1.0/src/drogue/core/config.py +71 -0
- drogue-0.1.0/src/drogue/core/errors.py +65 -0
- drogue-0.1.0/src/drogue/core/headers.py +56 -0
- drogue-0.1.0/src/drogue/core/identity/__init__.py +17 -0
- drogue-0.1.0/src/drogue/core/identity/key.py +148 -0
- drogue-0.1.0/src/drogue/core/identity/proxy.py +133 -0
- drogue-0.1.0/src/drogue/core/rules/__init__.py +5 -0
- drogue-0.1.0/src/drogue/core/rules/resolver.py +83 -0
- drogue-0.1.0/src/drogue/core/rules/rule.py +117 -0
- drogue-0.1.0/src/drogue/core/storage/__init__.py +3 -0
- drogue-0.1.0/src/drogue/core/storage/memory.py +162 -0
- drogue-0.1.0/src/drogue/core/storage/redis.py +147 -0
- drogue-0.1.0/src/drogue/defense/randomizer.py +318 -0
- drogue-0.1.0/src/drogue/observability/__init__.py +6 -0
- drogue-0.1.0/src/drogue/observability/logging.py +146 -0
- drogue-0.1.0/src/drogue/observability/metrics.py +248 -0
- drogue-0.1.0/src/drogue/observability/opentelemetry.py +204 -0
- drogue-0.1.0/src/drogue/protection/__init__.py +7 -0
- drogue-0.1.0/src/drogue/protection/adaptive.py +180 -0
- drogue-0.1.0/src/drogue/protection/ban.py +175 -0
- drogue-0.1.0/src/drogue/protection/cidr.py +165 -0
- drogue-0.1.0/src/drogue/protection/circuit.py +141 -0
- drogue-0.1.0/src/drogue/protection/ddos.py +312 -0
- drogue-0.1.0/src/drogue/protection/probes.py +289 -0
- drogue-0.1.0/src/drogue/protection/sentinel.py +344 -0
- drogue-0.1.0/src/drogue/protection/trust.py +298 -0
- drogue-0.1.0/src/drogue/storage/probabilistic.py +445 -0
- drogue-0.1.0/src/drogue/utils/__init__.py +3 -0
- drogue-0.1.0/src/drogue/utils/async_utils.py +55 -0
- drogue-0.1.0/src/drogue/utils/time.py +42 -0
- drogue-0.1.0/src/drogue/utils/typing.py +26 -0
- drogue-0.1.0/tests/__init__.py +0 -0
- drogue-0.1.0/tests/adapters/__init__.py +0 -0
- drogue-0.1.0/tests/adapters/conftest.py +18 -0
- drogue-0.1.0/tests/adapters/test_django.py +209 -0
- drogue-0.1.0/tests/adapters/test_fastapi.py +202 -0
- drogue-0.1.0/tests/conftest.py +29 -0
- drogue-0.1.0/tests/core/__init__.py +0 -0
- drogue-0.1.0/tests/core/test_algorithms.py +240 -0
- drogue-0.1.0/tests/core/test_errors.py +57 -0
- drogue-0.1.0/tests/core/test_identity.py +189 -0
- drogue-0.1.0/tests/core/test_rules.py +113 -0
- drogue-0.1.0/tests/core/test_storage.py +94 -0
- drogue-0.1.0/tests/integration/__init__.py +0 -0
- drogue-0.1.0/tests/protection/__init__.py +0 -0
- drogue-0.1.0/tests/protection/test_protection.py +166 -0
- drogue-0.1.0/tests/test_regressions.py +348 -0
drogue-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
venv/
|
|
25
|
+
.venv/
|
|
26
|
+
env/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# IDE
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# Testing
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.coverage
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
|
|
43
|
+
# Type checking
|
|
44
|
+
.mypy_cache/
|
|
45
|
+
.pytype/
|
|
46
|
+
|
|
47
|
+
# Ruff
|
|
48
|
+
.ruff_cache/
|
|
49
|
+
|
|
50
|
+
# OS
|
|
51
|
+
.DS_Store
|
|
52
|
+
Thumbs.db
|
|
53
|
+
|
|
54
|
+
# Project specific
|
|
55
|
+
_integration_test/__pycache__/
|
|
56
|
+
tests/__pycache__/
|
|
57
|
+
src/drogue/__pycache__/
|
drogue-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zlynv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
drogue-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drogue
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Production-ready rate limiting and DDoS protection for Python web frameworks
|
|
5
|
+
Author: Drogue Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ddos,django,fastapi,rate-limiting,security,throttling
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Framework :: Flask
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: adaptive
|
|
25
|
+
Requires-Dist: psutil>=5.0.0; extra == 'adaptive'
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: django>=4.2; extra == 'all'
|
|
28
|
+
Requires-Dist: djangorestframework>=3.14; extra == 'all'
|
|
29
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'all'
|
|
30
|
+
Requires-Dist: flask>=3.0.0; extra == 'all'
|
|
31
|
+
Requires-Dist: opentelemetry-api>=1.0.0; extra == 'all'
|
|
32
|
+
Requires-Dist: opentelemetry-sdk>=1.0.0; extra == 'all'
|
|
33
|
+
Requires-Dist: prometheus-client>=0.17.0; extra == 'all'
|
|
34
|
+
Requires-Dist: psutil>=5.0.0; extra == 'all'
|
|
35
|
+
Requires-Dist: redis[hiredis]>=5.0.0; extra == 'all'
|
|
36
|
+
Requires-Dist: starlette>=0.27.0; extra == 'all'
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
43
|
+
Provides-Extra: django
|
|
44
|
+
Requires-Dist: django>=4.2; extra == 'django'
|
|
45
|
+
Provides-Extra: drf
|
|
46
|
+
Requires-Dist: django>=4.2; extra == 'drf'
|
|
47
|
+
Requires-Dist: djangorestframework>=3.14; extra == 'drf'
|
|
48
|
+
Provides-Extra: fastapi
|
|
49
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
|
|
50
|
+
Requires-Dist: starlette>=0.27.0; extra == 'fastapi'
|
|
51
|
+
Provides-Extra: flask
|
|
52
|
+
Requires-Dist: flask>=3.0.0; extra == 'flask'
|
|
53
|
+
Provides-Extra: opentelemetry
|
|
54
|
+
Requires-Dist: opentelemetry-api>=1.0.0; extra == 'opentelemetry'
|
|
55
|
+
Requires-Dist: opentelemetry-sdk>=1.0.0; extra == 'opentelemetry'
|
|
56
|
+
Provides-Extra: prometheus
|
|
57
|
+
Requires-Dist: prometheus-client>=0.17.0; extra == 'prometheus'
|
|
58
|
+
Provides-Extra: redis
|
|
59
|
+
Requires-Dist: redis[hiredis]>=5.0.0; extra == 'redis'
|
|
60
|
+
Description-Content-Type: text/markdown
|
|
61
|
+
|
|
62
|
+
# drogue -- Rate Limiting and DDoS Protection for Python
|
|
63
|
+
|
|
64
|
+
[](https://www.python.org/downloads/)
|
|
65
|
+
[](LICENSE)
|
|
66
|
+
[](#development)
|
|
67
|
+
|
|
68
|
+
> Drop-in replacement for slowapi with DDoS detection, WebSocket support, and 9x faster trusted-user paths.
|
|
69
|
+
|
|
70
|
+
**Author:** Zlynv
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Why drogue?
|
|
75
|
+
|
|
76
|
+
**slowapi** requires `request: Request` in every endpoint, does not support WebSocket, and has no DDoS detection.
|
|
77
|
+
|
|
78
|
+
**drogue** fixes all three:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# slowapi -- polluted signature, no WebSocket, no DDoS detection
|
|
82
|
+
@app.get("/api/data")
|
|
83
|
+
@limiter.limit("10/minute")
|
|
84
|
+
async def get_data(request: Request): # forced parameter
|
|
85
|
+
return {"data": "value"}
|
|
86
|
+
|
|
87
|
+
# drogue -- clean signature, WebSocket support, DDoS detection
|
|
88
|
+
@app.get("/api/data")
|
|
89
|
+
@limiter.limit("10/minute")
|
|
90
|
+
async def get_data(): # clean
|
|
91
|
+
return {"data": "value"}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**What drogue adds that slowapi does not have:**
|
|
95
|
+
|
|
96
|
+
- Clean signatures -- no `request: Request` needed
|
|
97
|
+
- WebSocket rate limiting -- `@limiter.limit_ws("100/minute")`
|
|
98
|
+
- DDoS detection -- Z-score anomaly detection plus streaming Sentinel Model
|
|
99
|
+
- Early warning -- probe pattern detection 30-120 seconds before attacks
|
|
100
|
+
- Trust caching -- 9x throughput for verified users (5 microseconds vs 43 microseconds)
|
|
101
|
+
- Defense randomization -- attackers cannot learn your thresholds
|
|
102
|
+
- Memory efficient -- 10MB in-process replaces 80MB Redis
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Install
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install drogue
|
|
110
|
+
|
|
111
|
+
# With framework support
|
|
112
|
+
pip install drogue[fastapi] # FastAPI + Starlette
|
|
113
|
+
pip install drogue[django] # Django
|
|
114
|
+
pip install drogue[flask] # Flask
|
|
115
|
+
pip install drogue[drf] # Django REST Framework
|
|
116
|
+
pip install drogue[redis] # Redis backend
|
|
117
|
+
pip install drogue[all] # Everything
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Quick Start
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from fastapi import FastAPI
|
|
126
|
+
from drogue.adapters.fastapi import DrogueLimiter
|
|
127
|
+
|
|
128
|
+
app = FastAPI()
|
|
129
|
+
limiter = DrogueLimiter(app, default_limits=["100/minute"])
|
|
130
|
+
|
|
131
|
+
@app.get("/api/data")
|
|
132
|
+
@limiter.limit("10/minute")
|
|
133
|
+
async def get_data():
|
|
134
|
+
return {"data": "value"}
|
|
135
|
+
|
|
136
|
+
@app.get("/api/heavy")
|
|
137
|
+
@limiter.limit("3/minute") # expensive endpoints get lower limits
|
|
138
|
+
async def heavy_operation():
|
|
139
|
+
return {"result": "computed"}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
That is it. No `request` parameter. No middleware setup. Works with Flask and Django too -- see [docs/getting-started](docs/getting-started.md).
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Features
|
|
147
|
+
|
|
148
|
+
**Rate Limiting** -- Token Bucket (burst-friendly), Sliding Window (most accurate), Fixed Window (simplest), cost-aware limits, blocking mode
|
|
149
|
+
|
|
150
|
+
**Storage** -- In-memory (5 microseconds), Redis (distributed), Count-Min Sketch (10MB for 1M keys)
|
|
151
|
+
|
|
152
|
+
**Frameworks** -- FastAPI (pure ASGI middleware), Flask (decorator plus hook), Django (decorator plus middleware)
|
|
153
|
+
|
|
154
|
+
**Protection** -- DDoS detection (Z-score plus streaming), probe detection (early warning), progressive auto-ban (5 levels), circuit breaker, CIDR filtering, adaptive limits, shadow mode, trust caching, defense randomization, honeypots
|
|
155
|
+
|
|
156
|
+
**Observability** -- Prometheus metrics, OpenTelemetry tracing, structured logging
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Performance
|
|
161
|
+
|
|
162
|
+
| Metric | drogue | Context |
|
|
163
|
+
|--------|--------|---------|
|
|
164
|
+
| Trusted user | ~5 microseconds | Faster than a function call |
|
|
165
|
+
| Standard user | ~43 microseconds | 7-23x faster than Redis round-trip |
|
|
166
|
+
| Suspicious user | ~200 microseconds | Full pipeline, still under 1ms |
|
|
167
|
+
| Throughput | 741K req/s | Token Bucket, single worker |
|
|
168
|
+
| Memory per key | 150 bytes | vs 800 bytes in Redis |
|
|
169
|
+
| Count-Min Sketch | 10MB | Replaces 800MB Redis for 1M keys |
|
|
170
|
+
|
|
171
|
+
*Measured on Intel i7-12700K, Python 3.12, asyncio single-worker, median of 10,000 requests.*
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Migrate from slowapi
|
|
176
|
+
|
|
177
|
+
**Before:**
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from slowapi import Limiter
|
|
181
|
+
from slowapi.util import get_remote_address
|
|
182
|
+
from starlette.requests import Request
|
|
183
|
+
|
|
184
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
185
|
+
|
|
186
|
+
@app.get("/api/data")
|
|
187
|
+
@limiter.limit("10/minute")
|
|
188
|
+
async def get_data(request: Request):
|
|
189
|
+
return {"data": "value"}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**After:**
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from drogue.adapters.fastapi import DrogueLimiter
|
|
196
|
+
|
|
197
|
+
limiter = DrogueLimiter(app)
|
|
198
|
+
|
|
199
|
+
@app.get("/api/data")
|
|
200
|
+
@limiter.limit("10/minute")
|
|
201
|
+
async def get_data():
|
|
202
|
+
return {"data": "value"}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Migration checklist:**
|
|
206
|
+
|
|
207
|
+
1. Replace `from slowapi import Limiter` with `from drogue.adapters.fastapi import DrogueLimiter`
|
|
208
|
+
2. Replace `Limiter(key_func=get_remote_address)` with `DrogueLimiter(app)`
|
|
209
|
+
3. Remove `request: Request` from decorated endpoints
|
|
210
|
+
4. Rate limit headers work the same (X-RateLimit-Limit/Remaining/Reset)
|
|
211
|
+
5. Redis config: same URL format, add `storage_backend="redis"` to config
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Competitive Matrix
|
|
216
|
+
|
|
217
|
+
| Feature | drogue | slowapi | Next closest |
|
|
218
|
+
|---------|--------|---------|--------------|
|
|
219
|
+
| Cross-framework | FastAPI + Flask + Django | FastAPI only | Django only |
|
|
220
|
+
| Clean signatures | Yes | No | No (rateon only) |
|
|
221
|
+
| WebSocket | Yes | No | No |
|
|
222
|
+
| DDoS detection | Yes (Z-score + streaming) | No | No |
|
|
223
|
+
| Trust caching (9x speedup) | Yes | No | No |
|
|
224
|
+
| Count-Min Sketch | Yes (10MB for 1M keys) | No | No |
|
|
225
|
+
| Geo-blocking | No | No | Yes (fastapi-guard) |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Known Limitations
|
|
230
|
+
|
|
231
|
+
- Ban state is in-memory only by default. Redis persistence planned for v0.3.
|
|
232
|
+
- Trust cache is per-process. Multi-worker setups need separate trust state per worker.
|
|
233
|
+
- Flask headers for dict-returning views do not inject automatically.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Security
|
|
238
|
+
|
|
239
|
+
If you discover a security vulnerability, please open a GitHub issue with the [security] tag. We will respond within 48 hours.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Roadmap
|
|
244
|
+
|
|
245
|
+
- **v0.2** -- Redis-backed ban state, WebSocket support for Django and Flask
|
|
246
|
+
- **v0.3** -- Trust cache cross-process sync, advanced Sentinel features
|
|
247
|
+
- **v1.0** -- Production-ready, full documentation site
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
git clone https://github.com/Zlynv/drogue.git
|
|
255
|
+
cd drogue
|
|
256
|
+
pip install -e ".[dev]"
|
|
257
|
+
|
|
258
|
+
# Run tests
|
|
259
|
+
pytest
|
|
260
|
+
|
|
261
|
+
# Run linting
|
|
262
|
+
ruff check src/drogue/
|
|
263
|
+
|
|
264
|
+
# Run integration tests
|
|
265
|
+
cd _integration_test && pip install -r requirements.txt && pytest
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Author
|
|
277
|
+
|
|
278
|
+
Created by [Zlynv](https://github.com/Zlynv).
|
drogue-0.1.0/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# drogue -- Rate Limiting and DDoS Protection for Python
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#development)
|
|
6
|
+
|
|
7
|
+
> Drop-in replacement for slowapi with DDoS detection, WebSocket support, and 9x faster trusted-user paths.
|
|
8
|
+
|
|
9
|
+
**Author:** Zlynv
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why drogue?
|
|
14
|
+
|
|
15
|
+
**slowapi** requires `request: Request` in every endpoint, does not support WebSocket, and has no DDoS detection.
|
|
16
|
+
|
|
17
|
+
**drogue** fixes all three:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# slowapi -- polluted signature, no WebSocket, no DDoS detection
|
|
21
|
+
@app.get("/api/data")
|
|
22
|
+
@limiter.limit("10/minute")
|
|
23
|
+
async def get_data(request: Request): # forced parameter
|
|
24
|
+
return {"data": "value"}
|
|
25
|
+
|
|
26
|
+
# drogue -- clean signature, WebSocket support, DDoS detection
|
|
27
|
+
@app.get("/api/data")
|
|
28
|
+
@limiter.limit("10/minute")
|
|
29
|
+
async def get_data(): # clean
|
|
30
|
+
return {"data": "value"}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**What drogue adds that slowapi does not have:**
|
|
34
|
+
|
|
35
|
+
- Clean signatures -- no `request: Request` needed
|
|
36
|
+
- WebSocket rate limiting -- `@limiter.limit_ws("100/minute")`
|
|
37
|
+
- DDoS detection -- Z-score anomaly detection plus streaming Sentinel Model
|
|
38
|
+
- Early warning -- probe pattern detection 30-120 seconds before attacks
|
|
39
|
+
- Trust caching -- 9x throughput for verified users (5 microseconds vs 43 microseconds)
|
|
40
|
+
- Defense randomization -- attackers cannot learn your thresholds
|
|
41
|
+
- Memory efficient -- 10MB in-process replaces 80MB Redis
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install drogue
|
|
49
|
+
|
|
50
|
+
# With framework support
|
|
51
|
+
pip install drogue[fastapi] # FastAPI + Starlette
|
|
52
|
+
pip install drogue[django] # Django
|
|
53
|
+
pip install drogue[flask] # Flask
|
|
54
|
+
pip install drogue[drf] # Django REST Framework
|
|
55
|
+
pip install drogue[redis] # Redis backend
|
|
56
|
+
pip install drogue[all] # Everything
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from fastapi import FastAPI
|
|
65
|
+
from drogue.adapters.fastapi import DrogueLimiter
|
|
66
|
+
|
|
67
|
+
app = FastAPI()
|
|
68
|
+
limiter = DrogueLimiter(app, default_limits=["100/minute"])
|
|
69
|
+
|
|
70
|
+
@app.get("/api/data")
|
|
71
|
+
@limiter.limit("10/minute")
|
|
72
|
+
async def get_data():
|
|
73
|
+
return {"data": "value"}
|
|
74
|
+
|
|
75
|
+
@app.get("/api/heavy")
|
|
76
|
+
@limiter.limit("3/minute") # expensive endpoints get lower limits
|
|
77
|
+
async def heavy_operation():
|
|
78
|
+
return {"result": "computed"}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That is it. No `request` parameter. No middleware setup. Works with Flask and Django too -- see [docs/getting-started](docs/getting-started.md).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Features
|
|
86
|
+
|
|
87
|
+
**Rate Limiting** -- Token Bucket (burst-friendly), Sliding Window (most accurate), Fixed Window (simplest), cost-aware limits, blocking mode
|
|
88
|
+
|
|
89
|
+
**Storage** -- In-memory (5 microseconds), Redis (distributed), Count-Min Sketch (10MB for 1M keys)
|
|
90
|
+
|
|
91
|
+
**Frameworks** -- FastAPI (pure ASGI middleware), Flask (decorator plus hook), Django (decorator plus middleware)
|
|
92
|
+
|
|
93
|
+
**Protection** -- DDoS detection (Z-score plus streaming), probe detection (early warning), progressive auto-ban (5 levels), circuit breaker, CIDR filtering, adaptive limits, shadow mode, trust caching, defense randomization, honeypots
|
|
94
|
+
|
|
95
|
+
**Observability** -- Prometheus metrics, OpenTelemetry tracing, structured logging
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Performance
|
|
100
|
+
|
|
101
|
+
| Metric | drogue | Context |
|
|
102
|
+
|--------|--------|---------|
|
|
103
|
+
| Trusted user | ~5 microseconds | Faster than a function call |
|
|
104
|
+
| Standard user | ~43 microseconds | 7-23x faster than Redis round-trip |
|
|
105
|
+
| Suspicious user | ~200 microseconds | Full pipeline, still under 1ms |
|
|
106
|
+
| Throughput | 741K req/s | Token Bucket, single worker |
|
|
107
|
+
| Memory per key | 150 bytes | vs 800 bytes in Redis |
|
|
108
|
+
| Count-Min Sketch | 10MB | Replaces 800MB Redis for 1M keys |
|
|
109
|
+
|
|
110
|
+
*Measured on Intel i7-12700K, Python 3.12, asyncio single-worker, median of 10,000 requests.*
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Migrate from slowapi
|
|
115
|
+
|
|
116
|
+
**Before:**
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from slowapi import Limiter
|
|
120
|
+
from slowapi.util import get_remote_address
|
|
121
|
+
from starlette.requests import Request
|
|
122
|
+
|
|
123
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
124
|
+
|
|
125
|
+
@app.get("/api/data")
|
|
126
|
+
@limiter.limit("10/minute")
|
|
127
|
+
async def get_data(request: Request):
|
|
128
|
+
return {"data": "value"}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**After:**
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from drogue.adapters.fastapi import DrogueLimiter
|
|
135
|
+
|
|
136
|
+
limiter = DrogueLimiter(app)
|
|
137
|
+
|
|
138
|
+
@app.get("/api/data")
|
|
139
|
+
@limiter.limit("10/minute")
|
|
140
|
+
async def get_data():
|
|
141
|
+
return {"data": "value"}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Migration checklist:**
|
|
145
|
+
|
|
146
|
+
1. Replace `from slowapi import Limiter` with `from drogue.adapters.fastapi import DrogueLimiter`
|
|
147
|
+
2. Replace `Limiter(key_func=get_remote_address)` with `DrogueLimiter(app)`
|
|
148
|
+
3. Remove `request: Request` from decorated endpoints
|
|
149
|
+
4. Rate limit headers work the same (X-RateLimit-Limit/Remaining/Reset)
|
|
150
|
+
5. Redis config: same URL format, add `storage_backend="redis"` to config
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Competitive Matrix
|
|
155
|
+
|
|
156
|
+
| Feature | drogue | slowapi | Next closest |
|
|
157
|
+
|---------|--------|---------|--------------|
|
|
158
|
+
| Cross-framework | FastAPI + Flask + Django | FastAPI only | Django only |
|
|
159
|
+
| Clean signatures | Yes | No | No (rateon only) |
|
|
160
|
+
| WebSocket | Yes | No | No |
|
|
161
|
+
| DDoS detection | Yes (Z-score + streaming) | No | No |
|
|
162
|
+
| Trust caching (9x speedup) | Yes | No | No |
|
|
163
|
+
| Count-Min Sketch | Yes (10MB for 1M keys) | No | No |
|
|
164
|
+
| Geo-blocking | No | No | Yes (fastapi-guard) |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Known Limitations
|
|
169
|
+
|
|
170
|
+
- Ban state is in-memory only by default. Redis persistence planned for v0.3.
|
|
171
|
+
- Trust cache is per-process. Multi-worker setups need separate trust state per worker.
|
|
172
|
+
- Flask headers for dict-returning views do not inject automatically.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Security
|
|
177
|
+
|
|
178
|
+
If you discover a security vulnerability, please open a GitHub issue with the [security] tag. We will respond within 48 hours.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Roadmap
|
|
183
|
+
|
|
184
|
+
- **v0.2** -- Redis-backed ban state, WebSocket support for Django and Flask
|
|
185
|
+
- **v0.3** -- Trust cache cross-process sync, advanced Sentinel features
|
|
186
|
+
- **v1.0** -- Production-ready, full documentation site
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
git clone https://github.com/Zlynv/drogue.git
|
|
194
|
+
cd drogue
|
|
195
|
+
pip install -e ".[dev]"
|
|
196
|
+
|
|
197
|
+
# Run tests
|
|
198
|
+
pytest
|
|
199
|
+
|
|
200
|
+
# Run linting
|
|
201
|
+
ruff check src/drogue/
|
|
202
|
+
|
|
203
|
+
# Run integration tests
|
|
204
|
+
cd _integration_test && pip install -r requirements.txt && pytest
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Author
|
|
216
|
+
|
|
217
|
+
Created by [Zlynv](https://github.com/Zlynv).
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Django settings for integration test."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
8
|
+
|
|
9
|
+
SECRET_KEY = "test-secret-key-not-for-production"
|
|
10
|
+
DEBUG = True
|
|
11
|
+
ALLOWED_HOSTS = ["*"]
|
|
12
|
+
ROOT_URLCONF = "_integration_test.django_urls"
|
|
13
|
+
INSTALLED_APPS = [
|
|
14
|
+
"django.contrib.contenttypes",
|
|
15
|
+
"django.contrib.auth",
|
|
16
|
+
]
|
|
17
|
+
MIDDLEWARE = []
|
|
18
|
+
DATABASES = {
|
|
19
|
+
"default": {
|
|
20
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
21
|
+
"NAME": ":memory:",
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
25
|
+
USE_TZ = False
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Django URL configuration for integration test."""
|
|
2
|
+
from django.urls import path
|
|
3
|
+
|
|
4
|
+
from _integration_test import django_views
|
|
5
|
+
|
|
6
|
+
urlpatterns = [
|
|
7
|
+
path("api/ping", django_views.ping),
|
|
8
|
+
path("api/slow", django_views.slow_view),
|
|
9
|
+
path("api/fixed", django_views.fixed_view),
|
|
10
|
+
path("api/free", django_views.free),
|
|
11
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Django test views using drogue — proper integration pattern."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
8
|
+
|
|
9
|
+
from django.http import HttpRequest, JsonResponse
|
|
10
|
+
|
|
11
|
+
from drogue.adapters.django.limiter import DrogueRateLimiter
|
|
12
|
+
from drogue.core.rules.rule import AlgorithmType
|
|
13
|
+
from drogue.core.storage.memory import MemoryStorage
|
|
14
|
+
|
|
15
|
+
_storage = MemoryStorage()
|
|
16
|
+
_limiter = DrogueRateLimiter(storage=_storage, default_limits=["100/minute"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ping(request: HttpRequest) -> JsonResponse:
|
|
20
|
+
return JsonResponse({"status": "ok"})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ping = _limiter.limit("10/minute")(ping)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def slow_view(request: HttpRequest) -> JsonResponse:
|
|
27
|
+
return JsonResponse({"status": "ok"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
slow_view = _limiter.limit("5/minute", algorithm=AlgorithmType.SLIDING_WINDOW)(slow_view)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fixed_view(request: HttpRequest) -> JsonResponse:
|
|
34
|
+
return JsonResponse({"status": "ok"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
fixed_view = _limiter.limit("5/minute", algorithm=AlgorithmType.FIXED_WINDOW)(fixed_view)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def free(request: HttpRequest) -> JsonResponse:
|
|
41
|
+
return JsonResponse({"status": "ok"})
|