tollbooth 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.
- tollbooth-0.1.0/LICENSE +21 -0
- tollbooth-0.1.0/PKG-INFO +21 -0
- tollbooth-0.1.0/README.md +373 -0
- tollbooth-0.1.0/pyproject.toml +28 -0
- tollbooth-0.1.0/setup.cfg +4 -0
- tollbooth-0.1.0/tests/test_e2e.py +616 -0
- tollbooth-0.1.0/tests/test_engine.py +555 -0
- tollbooth-0.1.0/tests/test_integrations.py +971 -0
- tollbooth-0.1.0/tests/test_middleware.py +456 -0
- tollbooth-0.1.0/tollbooth/__init__.py +24 -0
- tollbooth-0.1.0/tollbooth/challenge.html +274 -0
- tollbooth-0.1.0/tollbooth/engine.py +518 -0
- tollbooth-0.1.0/tollbooth/integrations/__init__.py +3 -0
- tollbooth-0.1.0/tollbooth/integrations/base.py +182 -0
- tollbooth-0.1.0/tollbooth/integrations/django.py +121 -0
- tollbooth-0.1.0/tollbooth/integrations/falcon.py +112 -0
- tollbooth-0.1.0/tollbooth/integrations/fastapi.py +58 -0
- tollbooth-0.1.0/tollbooth/integrations/flask.py +114 -0
- tollbooth-0.1.0/tollbooth/integrations/starlette.py +89 -0
- tollbooth-0.1.0/tollbooth/middleware.py +215 -0
- tollbooth-0.1.0/tollbooth/rules.json +135 -0
- tollbooth-0.1.0/tollbooth.egg-info/PKG-INFO +21 -0
- tollbooth-0.1.0/tollbooth.egg-info/SOURCES.txt +24 -0
- tollbooth-0.1.0/tollbooth.egg-info/dependency_links.txt +1 -0
- tollbooth-0.1.0/tollbooth.egg-info/requires.txt +20 -0
- tollbooth-0.1.0/tollbooth.egg-info/top_level.txt +1 -0
tollbooth-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 libcaptcha
|
|
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.
|
tollbooth-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tollbooth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Proof-of-work bot challenge middleware
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Provides-Extra: test
|
|
8
|
+
Requires-Dist: pytest; extra == "test"
|
|
9
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
10
|
+
Requires-Dist: httpx; extra == "test"
|
|
11
|
+
Provides-Extra: django
|
|
12
|
+
Requires-Dist: django>=4.0; extra == "django"
|
|
13
|
+
Provides-Extra: flask
|
|
14
|
+
Requires-Dist: flask>=2.0; extra == "flask"
|
|
15
|
+
Provides-Extra: fastapi
|
|
16
|
+
Requires-Dist: fastapi>=0.100; extra == "fastapi"
|
|
17
|
+
Provides-Extra: starlette
|
|
18
|
+
Requires-Dist: starlette>=0.27; extra == "starlette"
|
|
19
|
+
Provides-Extra: falcon
|
|
20
|
+
Requires-Dist: falcon>=3.0; extra == "falcon"
|
|
21
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# tollbooth
|
|
4
|
+
|
|
5
|
+
Proof-of-work bot challenge middleware for Python. Zero dependencies.
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastapi import FastAPI, Depends
|
|
11
|
+
from tollbooth.integrations.fastapi import TollboothMiddleware
|
|
12
|
+
|
|
13
|
+
app = FastAPI()
|
|
14
|
+
app.add_middleware(TollboothMiddleware, secret="your-secret-key")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Bots get a browser challenge page. Humans solve it once, get a cookie, browse freely.
|
|
18
|
+
|
|
19
|
+
## Why tollbooth over [Anubis](https://github.com/TecharoHQ/anubis)?
|
|
20
|
+
|
|
21
|
+
| | tollbooth | Anubis |
|
|
22
|
+
| ----------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| **Language** | Python (drop-in middleware) | Go (standalone reverse proxy) |
|
|
24
|
+
| **Dependencies** | 0 | [31 direct, ~160 transitive](https://github.com/TecharoHQ/anubis/blob/main/go.mod#L3-L203) |
|
|
25
|
+
| **Code size** | ~800 lines | ~10,000 lines |
|
|
26
|
+
| **Integration** | `app.add_middleware(...)` | Separate process + reverse proxy |
|
|
27
|
+
| **PoW algorithm** | Balloon hashing (memory-hard) | [Plain SHA-256](https://github.com/TecharoHQ/anubis/blob/main/lib/challenge/proofofwork/proofofwork.go#L35-L85) |
|
|
28
|
+
| **Rules format** | JSON | [YAML + CEL expressions](https://github.com/TecharoHQ/anubis/blob/main/lib/config/config.go#L58-L73) |
|
|
29
|
+
| **Frameworks** | Flask, Django, FastAPI, Starlette, Falcon | None (reverse proxy only) |
|
|
30
|
+
|
|
31
|
+
### Security: memory-hard PoW
|
|
32
|
+
|
|
33
|
+
Anubis uses [plain SHA-256 hashing](https://github.com/TecharoHQ/anubis/blob/main/lib/challenge/proofofwork/proofofwork.go#L35-L85) — fast on GPUs and ASICs. An attacker with a GPU farm can solve challenges orders of magnitude faster than a browser.
|
|
34
|
+
|
|
35
|
+
Tollbooth uses **Balloon hashing** (Boneh, Corrigan-Gibbs, Schechter 2016) — a memory-hard function that requires `spaceCost * 32 bytes` per attempt. GPU parallelism is bottlenecked by memory bandwidth, not compute. This makes mass-solving economically impractical.
|
|
36
|
+
|
|
37
|
+
### Integration: middleware vs reverse proxy
|
|
38
|
+
|
|
39
|
+
Anubis runs as a [separate process with a reverse proxy](https://github.com/TecharoHQ/anubis/blob/main/cmd/anubis/main.go#L211-L265), adding network hops, deployment complexity, and a new failure domain.
|
|
40
|
+
|
|
41
|
+
Tollbooth is a middleware — it lives in your process, shares your config, and adds zero infrastructure:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# WSGI (Flask, Django)
|
|
45
|
+
app = TollboothWSGI(app, secret="key")
|
|
46
|
+
|
|
47
|
+
# ASGI (FastAPI, Starlette)
|
|
48
|
+
app = TollboothASGI(app, secret="key")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Rules: JSON vs YAML+CEL
|
|
52
|
+
|
|
53
|
+
Anubis requires [YAML policies with optional CEL expressions](https://github.com/TecharoHQ/anubis/blob/main/data/botPolicies.yaml) and [a complex config struct](https://github.com/TecharoHQ/anubis/blob/main/lib/config/config.go#L58-L73) with GeoIP, ASN, Thoth subscriptions, and 30+ CLI flags.
|
|
54
|
+
|
|
55
|
+
Tollbooth: one JSON file, four actions, regex matching.
|
|
56
|
+
|
|
57
|
+
### Performance: in-process vs network hop
|
|
58
|
+
|
|
59
|
+
Anubis [proxies every request through a separate Go process](https://github.com/TecharoHQ/anubis/blob/main/lib/anubis.go#L187-L296) — the full request pipeline includes reverse proxy setup, header rewriting, and upstream forwarding.
|
|
60
|
+
|
|
61
|
+
Tollbooth evaluates rules in-process with zero serialization. Allowed requests add microseconds of overhead. Challenged requests are handled before your app even sees them.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install tollbooth
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
With framework extras:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install tollbooth[flask]
|
|
73
|
+
pip install tollbooth[django]
|
|
74
|
+
pip install tollbooth[fastapi]
|
|
75
|
+
pip install tollbooth[starlette]
|
|
76
|
+
pip install tollbooth[falcon]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Browser Server
|
|
83
|
+
│ │
|
|
84
|
+
│ GET /page │
|
|
85
|
+
│──────────────────────────────────►│
|
|
86
|
+
│ │ rules evaluate request
|
|
87
|
+
│ 429 + challenge page │ → action: challenge
|
|
88
|
+
│◄──────────────────────────────────│
|
|
89
|
+
│ │
|
|
90
|
+
│ Web Workers solve PoW │
|
|
91
|
+
│ Balloon(random_data + nonce) │
|
|
92
|
+
│ until ≥ difficulty leading │
|
|
93
|
+
│ zero bits in hash │
|
|
94
|
+
│ │
|
|
95
|
+
│ POST /.tollbooth/verify │
|
|
96
|
+
│ { id, nonce, redirect } │
|
|
97
|
+
│──────────────────────────────────►│
|
|
98
|
+
│ │ server verifies PoW
|
|
99
|
+
│ 302 + Set-Cookie (JWT) │ → issues signed cookie
|
|
100
|
+
│◄──────────────────────────────────│
|
|
101
|
+
│ │
|
|
102
|
+
│ GET /page (with cookie) │
|
|
103
|
+
│──────────────────────────────────►│
|
|
104
|
+
│ 200 OK │ cookie valid → pass through
|
|
105
|
+
│◄──────────────────────────────────│
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The challenge page uses `navigator.hardwareConcurrency` Web Workers to mine in parallel. The JWT cookie is HMAC-SHA256 signed, bound to the client's IP hash, and valid for 7 days.
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
111
|
+
|
|
112
|
+
### Raw WSGI / ASGI
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from tollbooth import TollboothWSGI, TollboothASGI
|
|
116
|
+
|
|
117
|
+
# WSGI
|
|
118
|
+
app = TollboothWSGI(your_app, secret="your-secret-key")
|
|
119
|
+
|
|
120
|
+
# ASGI
|
|
121
|
+
app = TollboothASGI(your_app, secret="your-secret-key")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Flask
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from flask import Flask
|
|
128
|
+
from tollbooth.integrations.flask import Tollbooth
|
|
129
|
+
|
|
130
|
+
app = Flask(__name__)
|
|
131
|
+
tb = Tollbooth(app, secret="your-secret-key")
|
|
132
|
+
|
|
133
|
+
@app.route("/")
|
|
134
|
+
def index():
|
|
135
|
+
return "Hello!"
|
|
136
|
+
|
|
137
|
+
@tb.exempt
|
|
138
|
+
@app.route("/health")
|
|
139
|
+
def health():
|
|
140
|
+
return "ok"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Django
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# settings.py
|
|
147
|
+
TOLLBOOTH = {"secret": "your-secret-key"}
|
|
148
|
+
MIDDLEWARE = [
|
|
149
|
+
"tollbooth.integrations.django.TollboothMiddleware",
|
|
150
|
+
# ...
|
|
151
|
+
]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Per-view exemption:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from tollbooth.integrations.django import tollbooth_exempt
|
|
158
|
+
|
|
159
|
+
@tollbooth_exempt
|
|
160
|
+
def health(request):
|
|
161
|
+
return HttpResponse("ok")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### FastAPI
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from fastapi import FastAPI
|
|
168
|
+
from tollbooth.integrations.fastapi import TollboothMiddleware
|
|
169
|
+
|
|
170
|
+
app = FastAPI()
|
|
171
|
+
app.add_middleware(TollboothMiddleware, secret="your-secret-key")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Or as a dependency for specific routes:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from tollbooth.integrations.fastapi import TollboothDep
|
|
178
|
+
|
|
179
|
+
protect = TollboothDep("your-secret-key")
|
|
180
|
+
|
|
181
|
+
@app.get("/protected", dependencies=[Depends(protect)])
|
|
182
|
+
def protected():
|
|
183
|
+
return {"ok": True}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Starlette
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from starlette.applications import Starlette
|
|
190
|
+
from tollbooth.integrations.starlette import TollboothMiddleware
|
|
191
|
+
|
|
192
|
+
app = Starlette()
|
|
193
|
+
app.add_middleware(TollboothMiddleware, secret="your-secret-key")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Falcon
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
import falcon
|
|
200
|
+
from tollbooth.integrations.falcon import TollboothMiddleware
|
|
201
|
+
|
|
202
|
+
app = falcon.App(middleware=[
|
|
203
|
+
TollboothMiddleware(secret="your-secret-key"),
|
|
204
|
+
])
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Configuration
|
|
208
|
+
|
|
209
|
+
Pass options as keyword arguments to any integration:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
TollboothWSGI(
|
|
213
|
+
app,
|
|
214
|
+
secret="your-secret-key",
|
|
215
|
+
default_difficulty=12, # leading zero bits (default: 10)
|
|
216
|
+
space_cost=2048, # balloon memory blocks (default: 1024)
|
|
217
|
+
time_cost=1, # mixing rounds (default: 1)
|
|
218
|
+
delta=3, # random lookups per step (default: 3)
|
|
219
|
+
cookie_ttl=86400, # cookie lifetime seconds (default: 604800)
|
|
220
|
+
challenge_ttl=1800, # challenge validity seconds (default: 1800)
|
|
221
|
+
challenge_threshold=5, # weight sum to trigger challenge (default: 5)
|
|
222
|
+
branding=True, # show "Protected by tollbooth" (default: True)
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Each +1 difficulty doubles expected solve time. Higher `space_cost` increases memory per attempt (`space_cost * 32` bytes).
|
|
227
|
+
|
|
228
|
+
## Rules
|
|
229
|
+
|
|
230
|
+
Rules are evaluated top-to-bottom. First matching terminal action (`allow`, `deny`, `challenge`) wins. `weigh` rules accumulate weight — if the sum reaches `challenge_threshold`, a challenge is issued.
|
|
231
|
+
|
|
232
|
+
### Format
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
[
|
|
236
|
+
{
|
|
237
|
+
"name": "rule-name",
|
|
238
|
+
"action": "allow | deny | challenge | weigh",
|
|
239
|
+
"user_agent": "regex",
|
|
240
|
+
"path": "regex",
|
|
241
|
+
"headers": { "Header-Name": "regex" },
|
|
242
|
+
"remote_addresses": ["192.168.0.0/24"],
|
|
243
|
+
"difficulty": 12,
|
|
244
|
+
"weight": 3
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
All match fields are optional. A rule with no match fields matches everything. All fields use regex except `remote_addresses` (CIDR notation).
|
|
250
|
+
|
|
251
|
+
### Actions
|
|
252
|
+
|
|
253
|
+
| Action | Behavior |
|
|
254
|
+
| ----------- | ------------------------------------------ |
|
|
255
|
+
| `allow` | Pass through immediately |
|
|
256
|
+
| `deny` | Return 403 |
|
|
257
|
+
| `challenge` | Serve PoW challenge page |
|
|
258
|
+
| `weigh` | Add `weight` to score, continue evaluating |
|
|
259
|
+
|
|
260
|
+
### Default rules
|
|
261
|
+
|
|
262
|
+
Tollbooth ships with [rules.json](rules.json) covering:
|
|
263
|
+
|
|
264
|
+
**Deny** — Cloudflare Workers abuse, known bad bots, vulnerability scanners, WordPress probes, dotfile probes, shell probes, path traversal attempts
|
|
265
|
+
|
|
266
|
+
**Allow** — `.well-known/`, `favicon.ico`, `robots.txt`, health checks, search engines, feed readers, monitoring services, link previews, archive.org
|
|
267
|
+
|
|
268
|
+
**Challenge** — AI bots (difficulty 10), headless browsers (6), aggressive scrapers (8), empty user agents (6), generic browsers
|
|
269
|
+
|
|
270
|
+
**Weigh** — curl/wget (+3), missing Accept header (+3), missing Accept-Language (+2), `Connection: close` (+2)
|
|
271
|
+
|
|
272
|
+
### Custom rules
|
|
273
|
+
|
|
274
|
+
Override by passing a `rules_file` path or constructing a `Policy` directly:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from tollbooth import Policy, Rule, TollboothWSGI
|
|
278
|
+
|
|
279
|
+
policy = Policy(rules=[
|
|
280
|
+
Rule(name="internal", action="allow",
|
|
281
|
+
remote_addresses=["10.0.0.0/8"]),
|
|
282
|
+
Rule(name="api-bots", action="challenge",
|
|
283
|
+
path="^/api/", difficulty=14),
|
|
284
|
+
Rule(name="default", action="challenge"),
|
|
285
|
+
])
|
|
286
|
+
|
|
287
|
+
app = TollboothWSGI(your_app, secret="key", policy=policy)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Rule templates
|
|
291
|
+
|
|
292
|
+
Block AI scrapers:
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"name": "ai-bots",
|
|
297
|
+
"action": "deny",
|
|
298
|
+
"user_agent": "(?i:GPTBot|ChatGPT|Claude-Web|CCBot|Bytespider)"
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Protect API endpoints:
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{ "name": "api-protect", "action": "challenge", "path": "^/api/", "difficulty": 14 }
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Allowlist internal IPs:
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{ "name": "internal", "action": "allow", "remote_addresses": ["10.0.0.0/8", "172.16.0.0/12"] }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Weight scoring for suspicious signals:
|
|
315
|
+
|
|
316
|
+
```json
|
|
317
|
+
[
|
|
318
|
+
{ "name": "no-accept", "action": "weigh", "weight": 3, "headers": { "Accept": "^$" } },
|
|
319
|
+
{ "name": "no-lang", "action": "weigh", "weight": 2, "headers": { "Accept-Language": "^$" } },
|
|
320
|
+
{ "name": "curl", "action": "weigh", "weight": 3, "user_agent": "(?i:^curl/|^Wget/)" }
|
|
321
|
+
]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
With `challenge_threshold=5`, curl (weight 3) + missing Accept-Language (weight 2) = 5, triggers a challenge.
|
|
325
|
+
|
|
326
|
+
## Integrations
|
|
327
|
+
|
|
328
|
+
All integrations share the same options via `TollboothBase`:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from tollbooth.integrations.base import TollboothBase
|
|
332
|
+
|
|
333
|
+
tb = TollboothBase(
|
|
334
|
+
secret="key",
|
|
335
|
+
exclude=[r"^/static/", r"^/health$"], # regex skip list
|
|
336
|
+
json_mode=True, # return JSON instead of HTML challenges
|
|
337
|
+
)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
| Integration | Middleware class | Per-route | Exempt decorator |
|
|
341
|
+
| ------------- | --------------------- | -------------------- | ------------------- |
|
|
342
|
+
| **Flask** | `Tollbooth(app)` | `@tb.protect` | `@tb.exempt` |
|
|
343
|
+
| **Django** | `TollboothMiddleware` | `@tollbooth_protect` | `@tollbooth_exempt` |
|
|
344
|
+
| **FastAPI** | `TollboothMiddleware` | `TollboothDep` | `exclude=[...]` |
|
|
345
|
+
| **Starlette** | `TollboothMiddleware` | — | `exclude=[...]` |
|
|
346
|
+
| **Falcon** | `TollboothMiddleware` | `tollbooth_hook` | `exclude=[...]` |
|
|
347
|
+
| **WSGI** | `TollboothWSGI` | — | — |
|
|
348
|
+
| **ASGI** | `TollboothASGI` | — | — |
|
|
349
|
+
|
|
350
|
+
### JSON mode
|
|
351
|
+
|
|
352
|
+
For API/SPA backends, enable `json_mode=True`. Challenges return JSON instead of HTML:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"challenge": {
|
|
357
|
+
"id": "abc123",
|
|
358
|
+
"data": "random_hex",
|
|
359
|
+
"difficulty": 10,
|
|
360
|
+
"space_cost": 1024,
|
|
361
|
+
"time_cost": 1,
|
|
362
|
+
"delta": 3,
|
|
363
|
+
"verify_path": "/.tollbooth/verify",
|
|
364
|
+
"redirect": "/api/data"
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Solve with the [sha256-balloon](../sha256-balloon/) client library and POST the nonce to the verify endpoint.
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
[MIT](../LICENSE)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tollbooth"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Proof-of-work bot challenge middleware"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = []
|
|
7
|
+
|
|
8
|
+
[project.optional-dependencies]
|
|
9
|
+
test = ["pytest", "pytest-asyncio", "httpx"]
|
|
10
|
+
django = ["django>=4.0"]
|
|
11
|
+
flask = ["flask>=2.0"]
|
|
12
|
+
fastapi = ["fastapi>=0.100"]
|
|
13
|
+
starlette = ["starlette>=0.27"]
|
|
14
|
+
falcon = ["falcon>=3.0"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools>=68"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
include = ["tollbooth*"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
tollbooth = ["rules.json", "challenge.html"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
asyncio_mode = "auto"
|