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.
Files changed (31) hide show
  1. {flask_humanify-0.1.4/flask_Humanify.egg-info → flask_humanify-0.2.0}/PKG-INFO +9 -4
  2. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/README.md +3 -3
  3. {flask_humanify-0.1.4 → flask_humanify-0.2.0/flask_Humanify.egg-info}/PKG-INFO +9 -4
  4. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_Humanify.egg-info/SOURCES.txt +8 -2
  5. flask_humanify-0.2.0/flask_Humanify.egg-info/requires.txt +7 -0
  6. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/__init__.py +1 -1
  7. flask_humanify-0.2.0/flask_humanify/datasets/ai_dogs.pkl +0 -0
  8. flask_humanify-0.2.0/flask_humanify/datasets/animals.pkl +0 -0
  9. flask_humanify-0.2.0/flask_humanify/datasets/characters.pkl +0 -0
  10. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/features/rate_limiter.py +1 -1
  11. flask_humanify-0.2.0/flask_humanify/humanify.py +535 -0
  12. flask_humanify-0.2.0/flask_humanify/memory_server.py +836 -0
  13. flask_humanify-0.2.0/flask_humanify/secret_key.bin +0 -0
  14. flask_humanify-0.2.0/flask_humanify/templates/audio_challenge.html +208 -0
  15. flask_humanify-0.2.0/flask_humanify/templates/grid_challenge.html +232 -0
  16. 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
  17. flask_humanify-0.2.0/flask_humanify/utils.py +508 -0
  18. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/pyproject.toml +2 -2
  19. flask_humanify-0.1.4/flask_Humanify.egg-info/requires.txt +0 -2
  20. flask_humanify-0.1.4/flask_humanify/humanify.py +0 -158
  21. flask_humanify-0.1.4/flask_humanify/ipset.py +0 -315
  22. flask_humanify-0.1.4/flask_humanify/utils.py +0 -88
  23. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/LICENSE +0 -0
  24. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/MANIFEST.in +0 -0
  25. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_Humanify.egg-info/dependency_links.txt +0 -0
  26. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_Humanify.egg-info/top_level.txt +0 -0
  27. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/datasets/ipset.json +0 -0
  28. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/templates/access_denied.html +0 -0
  29. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/flask_humanify/templates/rate_limited.html +0 -0
  30. {flask_humanify-0.1.4 → flask_humanify-0.2.0}/setup.cfg +0 -0
  31. {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.1.4
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="deny_access")
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.deny_access()
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="deny_access")
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.deny_access()
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.1.4
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="deny_access")
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.deny_access()
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/ipset.py
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/oneclick_captcha.html
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
@@ -0,0 +1,7 @@
1
+ Flask
2
+ netaddr
3
+ opencv-python-headless
4
+ numpy
5
+ cryptography
6
+ scipy
7
+ pydub
@@ -4,7 +4,7 @@ Flask-Humanify
4
4
  A Flask extension that protects against bots and DDoS attacks.
5
5
  """
6
6
 
7
- __version__ = "0.1.4"
7
+ __version__ = "0.2.0"
8
8
 
9
9
  from . import utils
10
10
  from .humanify import Humanify
@@ -11,7 +11,7 @@ class RateLimiter:
11
11
  Rate limiter.
12
12
  """
13
13
 
14
- def __init__(self, app=None, max_requests: int = 2, time_window: int = 10):
14
+ def __init__(self, app=None, max_requests: int = 10, time_window: int = 10):
15
15
  """
16
16
  Initialize the rate limiter.
17
17
  """
@@ -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