flask-Humanify 0.1.4__tar.gz → 0.2.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.
- {flask_humanify-0.1.4/flask_Humanify.egg-info → flask_humanify-0.2.0}/PKG-INFO +9 -4
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/README.md +3 -3
- {flask_humanify-0.1.4 → flask_humanify-0.2.0/flask_Humanify.egg-info}/PKG-INFO +9 -4
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_Humanify.egg-info/SOURCES.txt +8 -2
- flask_humanify-0.2.0/flask_Humanify.egg-info/requires.txt +7 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/__init__.py +1 -1
- flask_humanify-0.2.0/flask_humanify/datasets/ai_dogs.pkl +0 -0
- flask_humanify-0.2.0/flask_humanify/datasets/animals.pkl +0 -0
- flask_humanify-0.2.0/flask_humanify/datasets/characters.pkl +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/features/rate_limiter.py +1 -1
- flask_humanify-0.2.0/flask_humanify/humanify.py +535 -0
- flask_humanify-0.2.0/flask_humanify/memory_server.py +836 -0
- flask_humanify-0.2.0/flask_humanify/secret_key.bin +0 -0
- flask_humanify-0.2.0/flask_humanify/templates/audio_challenge.html +208 -0
- flask_humanify-0.2.0/flask_humanify/templates/grid_challenge.html +232 -0
- flask_humanify-0.1.4/flask_humanify/templates/oneclick_captcha.html → flask_humanify-0.2.0/flask_humanify/templates/one_click_challenge.html +4 -9
- flask_humanify-0.2.0/flask_humanify/utils.py +508 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/pyproject.toml +2 -2
- flask_humanify-0.1.4/flask_Humanify.egg-info/requires.txt +0 -2
- flask_humanify-0.1.4/flask_humanify/humanify.py +0 -158
- flask_humanify-0.1.4/flask_humanify/ipset.py +0 -315
- flask_humanify-0.1.4/flask_humanify/utils.py +0 -88
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/LICENSE +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/MANIFEST.in +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_Humanify.egg-info/dependency_links.txt +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_Humanify.egg-info/top_level.txt +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/datasets/ipset.json +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/templates/access_denied.html +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/templates/rate_limited.html +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/setup.cfg +0 -0
- {flask_humanify-0.1.4 → flask_humanify-0.2.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: flask-Humanify
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Protect against bots and DDoS attacks
|
5
5
|
Author-email: TN3W <tn3w@protonmail.com>
|
6
6
|
License-Expression: Apache-2.0
|
@@ -27,6 +27,11 @@ Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
28
28
|
Requires-Dist: Flask
|
29
29
|
Requires-Dist: netaddr
|
30
|
+
Requires-Dist: opencv-python-headless
|
31
|
+
Requires-Dist: numpy
|
32
|
+
Requires-Dist: cryptography
|
33
|
+
Requires-Dist: scipy
|
34
|
+
Requires-Dist: pydub
|
30
35
|
Dynamic: license-file
|
31
36
|
|
32
37
|
<h1 align="center">flask-Humanify</h1>
|
@@ -40,10 +45,10 @@ from flask import Flask
|
|
40
45
|
from flask_Humanify import Humanify
|
41
46
|
|
42
47
|
app = Flask(__name__)
|
43
|
-
humanify = Humanify(app)
|
48
|
+
humanify = Humanify(app, challenge_type="one_click", captcha_dataset="ai_dogs")
|
44
49
|
|
45
50
|
# Register the middleware to deny access to bots
|
46
|
-
humanify.register_middleware(action="
|
51
|
+
humanify.register_middleware(action="challenge")
|
47
52
|
|
48
53
|
@app.route("/")
|
49
54
|
def index():
|
@@ -65,7 +70,7 @@ def index():
|
|
65
70
|
A route that is protected against bots and DDoS attacks.
|
66
71
|
"""
|
67
72
|
if humanify.is_bot:
|
68
|
-
return humanify.
|
73
|
+
return humanify.challenge()
|
69
74
|
return "Hello, Human!"
|
70
75
|
```
|
71
76
|
|
@@ -9,10 +9,10 @@ from flask import Flask
|
|
9
9
|
from flask_Humanify import Humanify
|
10
10
|
|
11
11
|
app = Flask(__name__)
|
12
|
-
humanify = Humanify(app)
|
12
|
+
humanify = Humanify(app, challenge_type="one_click", captcha_dataset="ai_dogs")
|
13
13
|
|
14
14
|
# Register the middleware to deny access to bots
|
15
|
-
humanify.register_middleware(action="
|
15
|
+
humanify.register_middleware(action="challenge")
|
16
16
|
|
17
17
|
@app.route("/")
|
18
18
|
def index():
|
@@ -34,7 +34,7 @@ def index():
|
|
34
34
|
A route that is protected against bots and DDoS attacks.
|
35
35
|
"""
|
36
36
|
if humanify.is_bot:
|
37
|
-
return humanify.
|
37
|
+
return humanify.challenge()
|
38
38
|
return "Hello, Human!"
|
39
39
|
```
|
40
40
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: flask-Humanify
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Protect against bots and DDoS attacks
|
5
5
|
Author-email: TN3W <tn3w@protonmail.com>
|
6
6
|
License-Expression: Apache-2.0
|
@@ -27,6 +27,11 @@ Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
28
28
|
Requires-Dist: Flask
|
29
29
|
Requires-Dist: netaddr
|
30
|
+
Requires-Dist: opencv-python-headless
|
31
|
+
Requires-Dist: numpy
|
32
|
+
Requires-Dist: cryptography
|
33
|
+
Requires-Dist: scipy
|
34
|
+
Requires-Dist: pydub
|
30
35
|
Dynamic: license-file
|
31
36
|
|
32
37
|
<h1 align="center">flask-Humanify</h1>
|
@@ -40,10 +45,10 @@ from flask import Flask
|
|
40
45
|
from flask_Humanify import Humanify
|
41
46
|
|
42
47
|
app = Flask(__name__)
|
43
|
-
humanify = Humanify(app)
|
48
|
+
humanify = Humanify(app, challenge_type="one_click", captcha_dataset="ai_dogs")
|
44
49
|
|
45
50
|
# Register the middleware to deny access to bots
|
46
|
-
humanify.register_middleware(action="
|
51
|
+
humanify.register_middleware(action="challenge")
|
47
52
|
|
48
53
|
@app.route("/")
|
49
54
|
def index():
|
@@ -65,7 +70,7 @@ def index():
|
|
65
70
|
A route that is protected against bots and DDoS attacks.
|
66
71
|
"""
|
67
72
|
if humanify.is_bot:
|
68
|
-
return humanify.
|
73
|
+
return humanify.challenge()
|
69
74
|
return "Hello, Human!"
|
70
75
|
```
|
71
76
|
|
@@ -10,10 +10,16 @@ flask_Humanify.egg-info/requires.txt
|
|
10
10
|
flask_Humanify.egg-info/top_level.txt
|
11
11
|
flask_humanify/__init__.py
|
12
12
|
flask_humanify/humanify.py
|
13
|
-
flask_humanify/
|
13
|
+
flask_humanify/memory_server.py
|
14
|
+
flask_humanify/secret_key.bin
|
14
15
|
flask_humanify/utils.py
|
16
|
+
flask_humanify/datasets/ai_dogs.pkl
|
17
|
+
flask_humanify/datasets/animals.pkl
|
18
|
+
flask_humanify/datasets/characters.pkl
|
15
19
|
flask_humanify/datasets/ipset.json
|
16
20
|
flask_humanify/features/rate_limiter.py
|
17
21
|
flask_humanify/templates/access_denied.html
|
18
|
-
flask_humanify/templates/
|
22
|
+
flask_humanify/templates/audio_challenge.html
|
23
|
+
flask_humanify/templates/grid_challenge.html
|
24
|
+
flask_humanify/templates/one_click_challenge.html
|
19
25
|
flask_humanify/templates/rate_limited.html
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,535 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
import logging
|
3
|
+
import random
|
4
|
+
from typing import List, Optional
|
5
|
+
|
6
|
+
from werkzeug.wrappers import Response
|
7
|
+
from flask import (
|
8
|
+
Blueprint,
|
9
|
+
request,
|
10
|
+
render_template,
|
11
|
+
redirect,
|
12
|
+
url_for,
|
13
|
+
current_app,
|
14
|
+
g,
|
15
|
+
abort,
|
16
|
+
)
|
17
|
+
from .memory_server import MemoryClient, ensure_server_running
|
18
|
+
from .utils import (
|
19
|
+
get_client_ip,
|
20
|
+
get_return_url,
|
21
|
+
validate_clearance_token,
|
22
|
+
generate_user_hash,
|
23
|
+
manipulate_image_bytes,
|
24
|
+
image_bytes_to_data_url,
|
25
|
+
generate_captcha_token,
|
26
|
+
validate_captcha_token,
|
27
|
+
generate_clearance_token,
|
28
|
+
combine_audio_files,
|
29
|
+
audio_bytes_to_data_url,
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
VPN_PROVIDERS = [
|
34
|
+
"NordVPN",
|
35
|
+
"ProtonVPN",
|
36
|
+
"ExpressVPN",
|
37
|
+
"Surfshark",
|
38
|
+
"PrivateInternetAccess",
|
39
|
+
"CyberGhost",
|
40
|
+
"TunnelBear",
|
41
|
+
"Mullvad",
|
42
|
+
]
|
43
|
+
|
44
|
+
IMAGE_CAPTCHA_MAPPING = {
|
45
|
+
"grid": {
|
46
|
+
"num_correct": (2, 3),
|
47
|
+
"num_images": 9,
|
48
|
+
"preview_image": False,
|
49
|
+
"hardness_range": (1, 4),
|
50
|
+
},
|
51
|
+
"one_click": {
|
52
|
+
"num_correct": 1,
|
53
|
+
"num_images": 6,
|
54
|
+
"preview_image": True,
|
55
|
+
"hardness_range": (1, 2),
|
56
|
+
},
|
57
|
+
}
|
58
|
+
|
59
|
+
AUDIO_CAPTCHA_CONFIG = {
|
60
|
+
"num_chars": 6,
|
61
|
+
"language": "en",
|
62
|
+
}
|
63
|
+
|
64
|
+
logger = logging.getLogger(__name__)
|
65
|
+
|
66
|
+
|
67
|
+
@dataclass
|
68
|
+
class HumanifyResult:
|
69
|
+
"""
|
70
|
+
Result of the Humanify check.
|
71
|
+
"""
|
72
|
+
|
73
|
+
ip: Optional[str] = None
|
74
|
+
is_vpn: bool = False
|
75
|
+
vpn_provider: Optional[str] = None
|
76
|
+
is_proxy: bool = False
|
77
|
+
is_datacenter: bool = False
|
78
|
+
is_forum_spammer: bool = False
|
79
|
+
is_firehol: bool = False
|
80
|
+
is_tor_exit_node: bool = False
|
81
|
+
is_invalid_ip: bool = False
|
82
|
+
|
83
|
+
@property
|
84
|
+
def is_bot(self) -> bool:
|
85
|
+
"""
|
86
|
+
Check if the IP is a bot.
|
87
|
+
"""
|
88
|
+
return (
|
89
|
+
self.is_invalid_ip
|
90
|
+
or self.is_vpn
|
91
|
+
or self.is_proxy
|
92
|
+
or self.is_datacenter
|
93
|
+
or self.is_forum_spammer
|
94
|
+
or self.is_firehol
|
95
|
+
or self.is_tor_exit_node
|
96
|
+
)
|
97
|
+
|
98
|
+
@classmethod
|
99
|
+
def from_ip_groups(cls, ip: str, ip_groups: List[str]) -> "HumanifyResult":
|
100
|
+
"""
|
101
|
+
Create a HumanifyResult from a list of IP groups.
|
102
|
+
"""
|
103
|
+
vpn_provider = next((name for name in VPN_PROVIDERS if name in ip_groups), None)
|
104
|
+
|
105
|
+
result = HumanifyResult(
|
106
|
+
ip=ip,
|
107
|
+
is_vpn=vpn_provider is not None,
|
108
|
+
vpn_provider=vpn_provider,
|
109
|
+
is_proxy="FireholProxies" in ip_groups or "AwesomeProxies" in ip_groups,
|
110
|
+
is_datacenter="Datacenter" in ip_groups,
|
111
|
+
is_forum_spammer="StopForumSpam" in ip_groups,
|
112
|
+
is_firehol="FireholLevel1" in ip_groups,
|
113
|
+
is_tor_exit_node="TorExitNodes" in ip_groups,
|
114
|
+
)
|
115
|
+
return result
|
116
|
+
|
117
|
+
def __bool__(self) -> bool:
|
118
|
+
"""
|
119
|
+
Check if the IP is a bot.
|
120
|
+
"""
|
121
|
+
return self.is_bot
|
122
|
+
|
123
|
+
|
124
|
+
class Humanify:
|
125
|
+
"""
|
126
|
+
Protect against bots and DDoS attacks.
|
127
|
+
"""
|
128
|
+
|
129
|
+
def __init__(
|
130
|
+
self, app=None, challenge_type: str = "one_click", captcha_dataset: str = "ai_dogs"
|
131
|
+
):
|
132
|
+
self.app = app
|
133
|
+
self.challenge_type = challenge_type
|
134
|
+
self.captcha_dataset = captcha_dataset
|
135
|
+
if app is not None:
|
136
|
+
self.init_app(app)
|
137
|
+
|
138
|
+
def init_app(self, app):
|
139
|
+
"""
|
140
|
+
Initialize the Humanify extension.
|
141
|
+
"""
|
142
|
+
self.app = app
|
143
|
+
|
144
|
+
ensure_server_running(
|
145
|
+
image_dataset=self.captcha_dataset,
|
146
|
+
)
|
147
|
+
self.memory_client = MemoryClient()
|
148
|
+
self.memory_client.connect()
|
149
|
+
self._secret_key = self.memory_client.get_secret_key()
|
150
|
+
|
151
|
+
self.blueprint = Blueprint(
|
152
|
+
"humanify", __name__, template_folder="templates", static_folder=None
|
153
|
+
)
|
154
|
+
self._register_routes()
|
155
|
+
app.register_blueprint(self.blueprint)
|
156
|
+
|
157
|
+
def _register_routes(self) -> None:
|
158
|
+
"""Register the humanify routes."""
|
159
|
+
|
160
|
+
@self.blueprint.route("/humanify/challenge", methods=["GET"])
|
161
|
+
def challenge():
|
162
|
+
"""
|
163
|
+
Challenge route.
|
164
|
+
"""
|
165
|
+
return self._render_challenge()
|
166
|
+
|
167
|
+
@self.blueprint.route("/humanify/audio_challenge", methods=["GET"])
|
168
|
+
def audio_challenge():
|
169
|
+
"""
|
170
|
+
Audio challenge route.
|
171
|
+
"""
|
172
|
+
return self._render_challenge(is_audio=True)
|
173
|
+
|
174
|
+
@self.blueprint.route("/humanify/verify", methods=["POST"])
|
175
|
+
def verify():
|
176
|
+
"""
|
177
|
+
Verify route.
|
178
|
+
"""
|
179
|
+
return self._verify_captcha()
|
180
|
+
|
181
|
+
@self.blueprint.route("/humanify/verify_audio", methods=["POST"])
|
182
|
+
def verify_audio():
|
183
|
+
"""
|
184
|
+
Verify audio route.
|
185
|
+
"""
|
186
|
+
return self._verify_audio_captcha()
|
187
|
+
|
188
|
+
@self.blueprint.route("/humanify/access_denied", methods=["GET"])
|
189
|
+
def access_denied():
|
190
|
+
"""
|
191
|
+
Access denied route.
|
192
|
+
"""
|
193
|
+
return (
|
194
|
+
render_template("access_denied.html").replace(
|
195
|
+
"RETURN_URL", get_return_url(request)
|
196
|
+
),
|
197
|
+
403,
|
198
|
+
{"Cache-Control": "public, max-age=15552000"},
|
199
|
+
)
|
200
|
+
|
201
|
+
def register_middleware(self, action: str = "challenge"):
|
202
|
+
"""
|
203
|
+
Register the middleware.
|
204
|
+
"""
|
205
|
+
|
206
|
+
self.app = self.app or current_app
|
207
|
+
|
208
|
+
@self.app.before_request
|
209
|
+
def before_request():
|
210
|
+
"""
|
211
|
+
Before request hook.
|
212
|
+
"""
|
213
|
+
if request.endpoint and request.endpoint.startswith("humanify."):
|
214
|
+
return
|
215
|
+
|
216
|
+
if self.is_bot:
|
217
|
+
if action == "challenge":
|
218
|
+
return self.challenge()
|
219
|
+
if action == "deny_access":
|
220
|
+
return self.deny_access()
|
221
|
+
|
222
|
+
@property
|
223
|
+
def client_ip(self) -> Optional[str]:
|
224
|
+
"""Get the client IP address."""
|
225
|
+
if hasattr(g, "humanify_client_ip"):
|
226
|
+
return g.humanify_client_ip
|
227
|
+
|
228
|
+
client_ip = get_client_ip(request)
|
229
|
+
g.humanify_client_ip = client_ip
|
230
|
+
return client_ip
|
231
|
+
|
232
|
+
@property
|
233
|
+
def check_result(self) -> HumanifyResult:
|
234
|
+
"""
|
235
|
+
Check if the IP is a bot.
|
236
|
+
"""
|
237
|
+
if self.client_ip is None:
|
238
|
+
return HumanifyResult(ip=self.client_ip, is_invalid_ip=True)
|
239
|
+
|
240
|
+
if hasattr(g, "humanify_ip_groups"):
|
241
|
+
humanify_ip_groups = g.humanify_ip_groups
|
242
|
+
if isinstance(humanify_ip_groups, list):
|
243
|
+
return HumanifyResult.from_ip_groups(self.client_ip, humanify_ip_groups)
|
244
|
+
|
245
|
+
ip_groups = self.memory_client.lookup_ip(self.client_ip)
|
246
|
+
g.humanify_ip_groups = ip_groups
|
247
|
+
return HumanifyResult.from_ip_groups(self.client_ip, ip_groups)
|
248
|
+
|
249
|
+
@property
|
250
|
+
def has_valid_clearance_token(self) -> bool:
|
251
|
+
"""Check if the current client has a valid clearance token."""
|
252
|
+
return validate_clearance_token(
|
253
|
+
request.cookies.get("clearance_token", ""),
|
254
|
+
self._secret_key,
|
255
|
+
generate_user_hash(
|
256
|
+
self.client_ip or "127.0.0.1",
|
257
|
+
request.user_agent.string or "",
|
258
|
+
),
|
259
|
+
)
|
260
|
+
|
261
|
+
@property
|
262
|
+
def is_bot(self) -> bool:
|
263
|
+
"""Check if the current client is a bot."""
|
264
|
+
return not self.has_valid_clearance_token and self.check_result.is_bot
|
265
|
+
|
266
|
+
def deny_access(self) -> Response:
|
267
|
+
"""
|
268
|
+
Redirect to the access denied page.
|
269
|
+
"""
|
270
|
+
return redirect(url_for("humanify.access_denied", return_url=request.full_path))
|
271
|
+
|
272
|
+
def challenge(self) -> Response:
|
273
|
+
"""
|
274
|
+
Challenge the client.
|
275
|
+
"""
|
276
|
+
return redirect(url_for("humanify.challenge", return_url=request.full_path))
|
277
|
+
|
278
|
+
def _render_challenge(self, is_audio: bool = False) -> Response:
|
279
|
+
return_url = get_return_url(request)
|
280
|
+
if self.has_valid_clearance_token:
|
281
|
+
return redirect(return_url)
|
282
|
+
|
283
|
+
error = request.args.get("error", None)
|
284
|
+
if error not in [
|
285
|
+
"Invalid captcha token",
|
286
|
+
"Wrong selection. Try again.",
|
287
|
+
"Wrong response. Try again.",
|
288
|
+
]:
|
289
|
+
error = None
|
290
|
+
|
291
|
+
if is_audio:
|
292
|
+
return self._render_audio_challenge(return_url, error)
|
293
|
+
|
294
|
+
if self.challenge_type in ["grid", "one_click"]:
|
295
|
+
return self._render_image_challenge(return_url, error)
|
296
|
+
|
297
|
+
abort(404, "Invalid challenge type")
|
298
|
+
|
299
|
+
def _render_image_challenge(
|
300
|
+
self, return_url: str, error: Optional[str]
|
301
|
+
) -> Response:
|
302
|
+
"""
|
303
|
+
Render the image challenge.
|
304
|
+
"""
|
305
|
+
|
306
|
+
captcha_config = IMAGE_CAPTCHA_MAPPING[self.challenge_type]
|
307
|
+
use_preview_image = captcha_config["preview_image"]
|
308
|
+
|
309
|
+
images_bytes, correct_indexes, subject = self.memory_client.get_captcha_images(
|
310
|
+
num_correct=captcha_config["num_correct"],
|
311
|
+
num_images=captcha_config["num_images"],
|
312
|
+
preview_image=use_preview_image,
|
313
|
+
dataset_name=self.captcha_dataset,
|
314
|
+
)
|
315
|
+
|
316
|
+
if not images_bytes:
|
317
|
+
abort(500, "Could not load captcha images")
|
318
|
+
|
319
|
+
processed_images = []
|
320
|
+
for i, img_bytes in enumerate(images_bytes):
|
321
|
+
try:
|
322
|
+
hardness = random.randint(
|
323
|
+
captcha_config["hardness_range"][0],
|
324
|
+
captcha_config["hardness_range"][1],
|
325
|
+
)
|
326
|
+
distorted = manipulate_image_bytes(
|
327
|
+
img_bytes,
|
328
|
+
is_small=not (i == 0 and use_preview_image),
|
329
|
+
hardness=hardness,
|
330
|
+
)
|
331
|
+
processed_images.append(image_bytes_to_data_url(distorted))
|
332
|
+
except Exception as e:
|
333
|
+
current_app.logger.error(f"Error processing image: {e}")
|
334
|
+
processed_images.append(
|
335
|
+
(
|
336
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
337
|
+
"CAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
338
|
+
)
|
339
|
+
)
|
340
|
+
|
341
|
+
preview_image = None
|
342
|
+
if use_preview_image:
|
343
|
+
preview_image = processed_images[0]
|
344
|
+
processed_images = processed_images[1:]
|
345
|
+
|
346
|
+
user_hash = generate_user_hash(
|
347
|
+
self.client_ip or "127.0.0.1", request.user_agent.string or ""
|
348
|
+
)
|
349
|
+
captcha_data = generate_captcha_token(
|
350
|
+
user_hash, correct_indexes, self._secret_key
|
351
|
+
)
|
352
|
+
|
353
|
+
return Response(
|
354
|
+
render_template(
|
355
|
+
f"{self.challenge_type}_challenge.html",
|
356
|
+
images=processed_images,
|
357
|
+
preview_image=preview_image,
|
358
|
+
subject=subject,
|
359
|
+
captcha_data=captcha_data,
|
360
|
+
return_url=return_url or "/",
|
361
|
+
error=error,
|
362
|
+
audio_challenge_available=True,
|
363
|
+
),
|
364
|
+
mimetype="text/html",
|
365
|
+
)
|
366
|
+
|
367
|
+
def _render_audio_challenge(
|
368
|
+
self, return_url: str, error: Optional[str]
|
369
|
+
) -> Response:
|
370
|
+
"""
|
371
|
+
Render the audio challenge.
|
372
|
+
"""
|
373
|
+
num_chars = AUDIO_CAPTCHA_CONFIG["num_chars"]
|
374
|
+
language = AUDIO_CAPTCHA_CONFIG["language"]
|
375
|
+
|
376
|
+
audio_files, correct_chars = self.memory_client.get_captcha_audio(
|
377
|
+
num_chars=num_chars, language=language
|
378
|
+
)
|
379
|
+
|
380
|
+
if not audio_files:
|
381
|
+
abort(500, "Could not load captcha audio")
|
382
|
+
|
383
|
+
combined_audio = combine_audio_files(audio_files)
|
384
|
+
if not combined_audio:
|
385
|
+
abort(500, "Could not process audio files")
|
386
|
+
|
387
|
+
audio_data_url = audio_bytes_to_data_url(combined_audio, "mp3")
|
388
|
+
|
389
|
+
user_hash = generate_user_hash(
|
390
|
+
self.client_ip or "127.0.0.1", request.user_agent.string or ""
|
391
|
+
)
|
392
|
+
captcha_data = generate_captcha_token(
|
393
|
+
user_hash, correct_chars, self._secret_key
|
394
|
+
)
|
395
|
+
|
396
|
+
return Response(
|
397
|
+
render_template(
|
398
|
+
"audio_challenge.html",
|
399
|
+
audio_file=audio_data_url,
|
400
|
+
captcha_data=captcha_data,
|
401
|
+
return_url=return_url or "/",
|
402
|
+
error=error,
|
403
|
+
image_challenge_available=True,
|
404
|
+
),
|
405
|
+
mimetype="text/html",
|
406
|
+
)
|
407
|
+
|
408
|
+
def _verify_captcha(self) -> Response:
|
409
|
+
"""Verify the captcha solution."""
|
410
|
+
return_url = get_return_url(request)
|
411
|
+
if self.has_valid_clearance_token:
|
412
|
+
return redirect(return_url)
|
413
|
+
|
414
|
+
captcha_data = request.form.get("captcha_data", "")
|
415
|
+
if not captcha_data:
|
416
|
+
return redirect(
|
417
|
+
url_for(
|
418
|
+
"humanify.challenge",
|
419
|
+
error="Invalid captcha token",
|
420
|
+
return_url=return_url,
|
421
|
+
)
|
422
|
+
)
|
423
|
+
|
424
|
+
user_hash = generate_user_hash(
|
425
|
+
self.client_ip or "127.0.0.1", request.user_agent.string or ""
|
426
|
+
)
|
427
|
+
decrypted_data = validate_captcha_token(
|
428
|
+
captcha_data, self._secret_key, user_hash
|
429
|
+
)
|
430
|
+
|
431
|
+
if decrypted_data is None:
|
432
|
+
return redirect(
|
433
|
+
url_for(
|
434
|
+
"humanify.challenge",
|
435
|
+
error="Invalid captcha token",
|
436
|
+
return_url=return_url,
|
437
|
+
)
|
438
|
+
)
|
439
|
+
|
440
|
+
verify_functions = {
|
441
|
+
"grid": self._verify_image_captcha,
|
442
|
+
"one_click": self._verify_image_captcha,
|
443
|
+
}
|
444
|
+
|
445
|
+
verify_function = verify_functions[self.challenge_type]
|
446
|
+
if not verify_function(decrypted_data):
|
447
|
+
return redirect(
|
448
|
+
url_for(
|
449
|
+
"humanify.challenge",
|
450
|
+
error="Wrong selection. Try again.",
|
451
|
+
return_url=return_url,
|
452
|
+
)
|
453
|
+
)
|
454
|
+
|
455
|
+
clearance_token = generate_clearance_token(user_hash, self._secret_key)
|
456
|
+
|
457
|
+
response = redirect(return_url or "/")
|
458
|
+
response.set_cookie(
|
459
|
+
"clearance_token",
|
460
|
+
clearance_token,
|
461
|
+
max_age=14400,
|
462
|
+
httponly=True,
|
463
|
+
samesite="Strict",
|
464
|
+
)
|
465
|
+
|
466
|
+
return response
|
467
|
+
|
468
|
+
def _verify_audio_captcha(self) -> Response:
|
469
|
+
"""Verify the audio captcha solution."""
|
470
|
+
return_url = get_return_url(request)
|
471
|
+
if self.has_valid_clearance_token:
|
472
|
+
return redirect(return_url)
|
473
|
+
|
474
|
+
captcha_data = request.form.get("captcha_data", "")
|
475
|
+
if not captcha_data:
|
476
|
+
return redirect(
|
477
|
+
url_for(
|
478
|
+
"humanify.audio_challenge",
|
479
|
+
error="Invalid captcha token",
|
480
|
+
return_url=return_url,
|
481
|
+
)
|
482
|
+
)
|
483
|
+
|
484
|
+
user_hash = generate_user_hash(
|
485
|
+
self.client_ip or "127.0.0.1", request.user_agent.string or ""
|
486
|
+
)
|
487
|
+
correct_chars = validate_captcha_token(
|
488
|
+
captcha_data, self._secret_key, user_hash, valid_lengths=[197]
|
489
|
+
)
|
490
|
+
|
491
|
+
if correct_chars is None:
|
492
|
+
return redirect(
|
493
|
+
url_for(
|
494
|
+
"humanify.audio_challenge",
|
495
|
+
error="Invalid captcha token",
|
496
|
+
return_url=return_url,
|
497
|
+
)
|
498
|
+
)
|
499
|
+
|
500
|
+
audio_response = request.form.get("audio_response", "").lower().strip()
|
501
|
+
if not audio_response or audio_response != correct_chars:
|
502
|
+
return redirect(
|
503
|
+
url_for(
|
504
|
+
"humanify.audio_challenge",
|
505
|
+
error="Wrong response. Try again.",
|
506
|
+
return_url=return_url,
|
507
|
+
)
|
508
|
+
)
|
509
|
+
|
510
|
+
clearance_token = generate_clearance_token(user_hash, self._secret_key)
|
511
|
+
|
512
|
+
response = redirect(return_url or "/")
|
513
|
+
response.set_cookie(
|
514
|
+
"clearance_token",
|
515
|
+
clearance_token,
|
516
|
+
max_age=14400,
|
517
|
+
httponly=True,
|
518
|
+
samesite="Strict",
|
519
|
+
)
|
520
|
+
|
521
|
+
return response
|
522
|
+
|
523
|
+
def _verify_image_captcha(self, decrypted_data: str) -> bool:
|
524
|
+
"""Verify the image captcha."""
|
525
|
+
captcha_config = IMAGE_CAPTCHA_MAPPING[self.challenge_type]
|
526
|
+
|
527
|
+
selected_indexes = []
|
528
|
+
for i in range(1, captcha_config["num_images"] + 1):
|
529
|
+
if request.form.get(str(i), None) == "1":
|
530
|
+
selected_indexes.append(str(i - 1))
|
531
|
+
|
532
|
+
selected_str = "".join(sorted(selected_indexes))
|
533
|
+
correct_str = "".join(sorted(list(decrypted_data)))
|
534
|
+
|
535
|
+
return selected_str == correct_str
|