flask-Humanify 0.1.4__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flask_humanify/utils.py CHANGED
@@ -1,8 +1,25 @@
1
- from urllib.parse import urlparse
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+ import io
5
+ import logging
6
+ import math
7
+ import random
8
+ import secrets
9
+ import time
2
10
  from typing import List, Optional
11
+ from urllib.parse import urlparse
3
12
 
13
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
4
14
  from flask import Request
5
- from netaddr import IPAddress, AddrFormatError
15
+ from netaddr import AddrFormatError, IPAddress
16
+ import cv2
17
+ import numpy as np
18
+ from pydub import AudioSegment
19
+ from scipy.io.wavfile import write as write_wav
20
+
21
+
22
+ logger = logging.getLogger(__name__)
6
23
 
7
24
 
8
25
  def is_valid_routable_ip(ip: str) -> bool:
@@ -86,3 +103,406 @@ def get_return_url(request: Request) -> str:
86
103
  return return_url.strip("?")
87
104
 
88
105
  return return_url
106
+
107
+
108
+ def generate_random_token(length: int = 32) -> str:
109
+ """Generate a random token using URL-safe base64 character set."""
110
+ url_safe_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
111
+ return "".join(secrets.choice(url_safe_chars) for _ in range(length))
112
+
113
+
114
+ def generate_signature(data: str, key: bytes) -> str:
115
+ """Generate a signature for the given data using the given key."""
116
+ hmac_digest = hmac.new(key, data.encode("utf-8"), hashlib.sha256).digest()
117
+ return base64.urlsafe_b64encode(hmac_digest).decode("utf-8").rstrip("=")
118
+
119
+
120
+ def validate_signature(data: str, signature: str, key: bytes) -> bool:
121
+ """Validate the signature for the given data using the given key."""
122
+ expected_signature = generate_signature(data, key)
123
+ return hmac.compare_digest(expected_signature, signature)
124
+
125
+
126
+ def generate_user_hash(ip: str, user_agent: str) -> str:
127
+ """Generate a user hash for the given ip and user agent."""
128
+ return hashlib.sha256(f"{ip}{user_agent}".encode("utf-8")).hexdigest()
129
+
130
+
131
+ def generate_clearance_token(user_hash: str, key: bytes) -> str:
132
+ """Generate a clearance token for the given user hash."""
133
+ nonce = generate_random_token(32)
134
+ timestamp = str(int(time.time())).zfill(10)
135
+ data = f"{nonce}{timestamp}{user_hash}"
136
+ signature = generate_signature(data, key)
137
+ return f"{data}{signature}"
138
+
139
+
140
+ def validate_clearance_token(
141
+ token: str, key: bytes, user_hash: str, ttl: int = 14400
142
+ ) -> bool:
143
+ """Validate the clearance token."""
144
+ try:
145
+ if len(token) < 85:
146
+ return False
147
+
148
+ signature_length = 43
149
+
150
+ nonce = token[:32]
151
+ timestamp = token[32:42]
152
+ token_user_hash = token[42:106]
153
+ signature = token[-signature_length:]
154
+
155
+ if token_user_hash != user_hash:
156
+ return False
157
+
158
+ data = f"{nonce}{timestamp}{user_hash}"
159
+ if not validate_signature(data, signature, key):
160
+ return False
161
+
162
+ if int(timestamp) + ttl < int(time.time()):
163
+ return False
164
+
165
+ return True
166
+ except Exception as e:
167
+ logger.error("Token validation error: %s", str(e))
168
+ return False
169
+
170
+
171
+ def encrypt_data(data: str, key: bytes) -> str:
172
+ """Encrypt data using AES-GCM."""
173
+ aesgcm = AESGCM(key[:32])
174
+ iv = secrets.token_bytes(12)
175
+ ciphertext = aesgcm.encrypt(iv, data.encode("utf-8"), None)
176
+ encrypted = iv + ciphertext
177
+ return base64.urlsafe_b64encode(encrypted).decode("utf-8")
178
+
179
+
180
+ def decrypt_data(encrypted_data: str, key: bytes) -> Optional[str]:
181
+ """Decrypt data encrypted with AES-GCM."""
182
+ try:
183
+ encrypted = base64.urlsafe_b64decode(encrypted_data)
184
+ iv = encrypted[:12]
185
+ ciphertext = encrypted[12:]
186
+
187
+ aesgcm = AESGCM(key[:32])
188
+ decrypted = aesgcm.decrypt(iv, ciphertext, None)
189
+ return decrypted.decode("utf-8")
190
+ except (ValueError, KeyError):
191
+ return None
192
+
193
+
194
+ def generate_captcha_token(user_hash: str, correct_indexes: str, key: bytes) -> str:
195
+ """Generate a captcha verification token."""
196
+ nonce = generate_random_token(32)
197
+ timestamp = str(int(time.time())).zfill(10)
198
+
199
+ encrypted_answer = encrypt_data(correct_indexes, key)
200
+
201
+ data = f"{nonce}{timestamp}{user_hash}{encrypted_answer}"
202
+ return f"{data}{generate_signature(data, key)}"
203
+
204
+
205
+ def validate_captcha_token(
206
+ token: str,
207
+ key: bytes,
208
+ user_hash: str,
209
+ ttl: int = 600,
210
+ valid_lengths: Optional[List[int]] = None,
211
+ ) -> Optional[str]:
212
+ """Validate the captcha token and return the correct indexes if valid."""
213
+ try:
214
+ if valid_lengths is None:
215
+ valid_lengths = [189, 193]
216
+
217
+ if len(token) not in valid_lengths:
218
+ return None
219
+
220
+ nonce = token[:32]
221
+ timestamp = token[32:42]
222
+ token_user_hash = token[42:106]
223
+ encrypted_answer = token[106:-43]
224
+ signature = token[-43:]
225
+
226
+ if token_user_hash != user_hash:
227
+ print("User hash mismatch")
228
+ return None
229
+
230
+ data = f"{nonce}{timestamp}{token_user_hash}{encrypted_answer}"
231
+
232
+ if not validate_signature(data, signature, key):
233
+ print("Signature mismatch")
234
+ return None
235
+
236
+ if int(timestamp) + ttl < int(time.time()):
237
+ print("Token expired")
238
+ return None
239
+
240
+ correct_indexes = decrypt_data(encrypted_answer, key)
241
+ return correct_indexes
242
+
243
+ except Exception as e:
244
+ logger.error("Token validation error: %s", str(e))
245
+ return None
246
+
247
+
248
+ def manipulate_image_bytes(
249
+ image_data: bytes, is_small: bool = False, hardness: int = 1
250
+ ) -> bytes:
251
+ """Manipulates an image represented by bytes to create a distorted version."""
252
+ # pylint: disable=no-member
253
+
254
+ img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR)
255
+ if img is None:
256
+ logger.error("Image data could not be decoded by OpenCV")
257
+ raise ValueError("Image data could not be decoded.")
258
+
259
+ size = 100 if is_small else 200
260
+ img = cv2.resize(img, (size, size), interpolation=cv2.INTER_LINEAR)
261
+
262
+ if hardness > 3:
263
+ num_dots = np.random.randint(20, 100) * (hardness - 3)
264
+ dot_coords = np.random.randint(0, [size, size], size=(num_dots, 2))
265
+ colors = np.random.randint(0, 256, size=(num_dots, 3))
266
+
267
+ for (x, y), color in zip(dot_coords, colors):
268
+ img[y, x] = color
269
+
270
+ num_lines = np.random.randint(20, 100) * (hardness - 3)
271
+ start_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
272
+ end_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
273
+ colors = np.random.randint(0, 256, size=(num_lines, 3))
274
+
275
+ for (start, end), color in zip(zip(start_coords, end_coords), colors):
276
+ cv2.line(img, tuple(start), tuple(end), color.tolist(), 1)
277
+
278
+ max_shift = max(3, hardness + 1)
279
+ x_shifts = np.random.randint(-max(2, hardness + 4), max_shift, size=(size, size))
280
+ y_shifts = np.random.randint(-max(1, hardness + 4), max_shift, size=(size, size))
281
+
282
+ map_x, map_y = np.meshgrid(np.arange(size), np.arange(size))
283
+ map_x = (map_x + x_shifts) % size
284
+ map_y = (map_y + y_shifts) % size
285
+
286
+ shifted_img = cv2.remap(
287
+ img, map_x.astype(np.float32), map_y.astype(np.float32), cv2.INTER_LINEAR
288
+ )
289
+ shifted_img_hsv = cv2.cvtColor(shifted_img, cv2.COLOR_BGR2HSV)
290
+
291
+ shifted_img_hsv[..., 1] = np.clip(
292
+ shifted_img_hsv[..., 1] * (1 + hardness * 0.12), 0, 255
293
+ )
294
+ shifted_img_hsv[..., 2] = np.clip(
295
+ shifted_img_hsv[..., 2] * (1 - hardness * 0.09), 0, 255
296
+ )
297
+
298
+ shifted_img = cv2.cvtColor(shifted_img_hsv, cv2.COLOR_HSV2BGR)
299
+ shifted_img = cv2.GaussianBlur(shifted_img, (5, 5), hardness * 0.1)
300
+
301
+ _, output_bytes = cv2.imencode(".png", shifted_img)
302
+ if not _:
303
+ logger.error("Image encoding failed")
304
+ raise ValueError("Image encoding failed.")
305
+
306
+ return output_bytes.tobytes()
307
+
308
+
309
+ def image_bytes_to_data_url(image_bytes: bytes, image_format: str = "png") -> str:
310
+ """Convert image bytes to a data URL."""
311
+ b64_image = base64.b64encode(image_bytes).decode("utf-8")
312
+ return f"data:image/{image_format};base64,{b64_image}"
313
+
314
+
315
+ def audio_bytes_to_data_url(audio_bytes: bytes, audio_format: str = "mp3") -> str:
316
+ """Convert audio bytes to a data URL."""
317
+ b64_audio = base64.b64encode(audio_bytes).decode("utf-8")
318
+ return f"data:audio/{audio_format};base64,{b64_audio}"
319
+
320
+
321
+ # Audio processing functions
322
+
323
+ WAVE_SAMPLE_RATE = 44100 # Hz
324
+ audio_cache = {}
325
+
326
+
327
+ def numpy_to_audio_segment(samples, sample_rate=44100):
328
+ """Convert numpy array directly to AudioSegment without temporary files."""
329
+ try:
330
+ samples = samples.astype(np.int16)
331
+ wav_io = io.BytesIO()
332
+ write_wav(wav_io, sample_rate, samples)
333
+ wav_io.seek(0)
334
+
335
+ return AudioSegment.from_wav(wav_io)
336
+ except ImportError:
337
+ logger.error("pydub or scipy not installed. Audio processing unavailable.")
338
+ return None
339
+
340
+
341
+ def generate_sine_wave(freq, duration_ms, sample_rate=44100):
342
+ """Generate a sine wave at the specified frequency and duration."""
343
+ cache_key = f"sine_{freq}_{duration_ms}_{sample_rate}"
344
+ if cache_key in audio_cache:
345
+ return audio_cache[cache_key]
346
+
347
+ num_samples = int(sample_rate * duration_ms / 1000.0)
348
+ t = np.linspace(0, duration_ms / 1000.0, num_samples, endpoint=False)
349
+ samples = (np.sin(2 * np.pi * freq * t) * 32767).astype(np.int16)
350
+
351
+ beep_segment = numpy_to_audio_segment(samples, sample_rate)
352
+
353
+ audio_cache[cache_key] = beep_segment
354
+ return beep_segment
355
+
356
+
357
+ def change_speed(audio_segment, speed=1.0):
358
+ """Change the speed of an AudioSegment."""
359
+ if speed == 1.0:
360
+ return audio_segment
361
+
362
+ return audio_segment._spawn(
363
+ audio_segment.raw_data,
364
+ overrides={"frame_rate": int(audio_segment.frame_rate * speed)},
365
+ ).set_frame_rate(audio_segment.frame_rate)
366
+
367
+
368
+ def change_volume(audio_segment, level=1.0):
369
+ """Change the volume of an AudioSegment."""
370
+ if level == 1.0:
371
+ return audio_segment
372
+
373
+ db_change = 20 * math.log10(level)
374
+ return audio_segment.apply_gain(db_change)
375
+
376
+
377
+ def create_silence(duration_ms):
378
+ """Create a silent AudioSegment."""
379
+ try:
380
+ return AudioSegment.silent(duration=duration_ms)
381
+ except ImportError:
382
+ logger.error("pydub not installed. Audio processing unavailable.")
383
+ return None
384
+
385
+
386
+ def create_noise(duration_ms, level=0.05, sample_rate=44100):
387
+ """Create white noise."""
388
+ cache_key = f"noise_{duration_ms}_{level}_{sample_rate}"
389
+ if cache_key in audio_cache:
390
+ return audio_cache[cache_key]
391
+
392
+ num_samples = int(sample_rate * duration_ms / 1000.0)
393
+ noise_samples = (np.random.uniform(-1, 1, num_samples) * level * 32767).astype(
394
+ np.int16
395
+ )
396
+
397
+ noise_segment = numpy_to_audio_segment(noise_samples, sample_rate)
398
+
399
+ audio_cache[cache_key] = noise_segment
400
+ return noise_segment
401
+
402
+
403
+ def mix_audio(audio1, audio2, position_ms=0):
404
+ """Mix two AudioSegments."""
405
+ try:
406
+ return audio1.overlay(audio2, position=position_ms)
407
+ except Exception as e:
408
+ logger.error(f"Audio overlay failed: {e}")
409
+ try:
410
+ if audio1.frame_rate != audio2.frame_rate:
411
+ audio2 = audio2.set_frame_rate(audio1.frame_rate)
412
+ if audio1.channels != audio2.channels:
413
+ audio2 = audio2.set_channels(audio1.channels)
414
+ if audio1.sample_width != audio2.sample_width:
415
+ audio2 = audio2.set_sample_width(audio1.sample_width)
416
+
417
+ return audio1.overlay(audio2, position=position_ms)
418
+ except Exception as e2:
419
+ logger.error(f"Second audio overlay attempt failed: {e2}")
420
+ return audio1
421
+
422
+
423
+ def batch_mix_audio(base_audio, segments_with_positions):
424
+ """
425
+ More efficient way to mix multiple audio segments with their positions.
426
+
427
+ Args:
428
+ base_audio: Base AudioSegment
429
+ segments_with_positions: List of tuples (segment, position_ms)
430
+
431
+ Returns:
432
+ Mixed AudioSegment
433
+ """
434
+ result = base_audio
435
+
436
+ segments_with_positions.sort(key=lambda x: x[1])
437
+
438
+ batch_size = 10
439
+ for i in range(0, len(segments_with_positions), batch_size):
440
+ batch = segments_with_positions[i : i + batch_size]
441
+
442
+ for segment, position in batch:
443
+ result = mix_audio(result, segment, position)
444
+
445
+ return result
446
+
447
+
448
+ def bytes_to_audio_segment(audio_bytes):
449
+ """Convert bytes directly to AudioSegment without temp files."""
450
+ try:
451
+ wav_io = io.BytesIO(audio_bytes)
452
+ return AudioSegment.from_wav(wav_io)
453
+ except ImportError:
454
+ logger.error("pydub not installed. Audio processing unavailable.")
455
+ return None
456
+
457
+
458
+ def combine_audio_files(audio_files):
459
+ """
460
+ Combine a list of audio file bytes into a single audio file.
461
+
462
+ Args:
463
+ audio_files: List of audio file bytes
464
+
465
+ Returns:
466
+ Combined audio file bytes
467
+ """
468
+ try:
469
+ if not audio_files:
470
+ logger.error("No audio files provided")
471
+ return None
472
+
473
+ segments = []
474
+ for audio_bytes in audio_files:
475
+ wav_io = io.BytesIO(audio_bytes)
476
+ try:
477
+ segment = AudioSegment.from_wav(wav_io)
478
+ segments.append(segment)
479
+ except Exception as e:
480
+ logger.error(f"Error converting audio bytes to segment: {e}")
481
+
482
+ if not segments:
483
+ logger.error("No valid audio segments found")
484
+ return None
485
+
486
+ result = create_silence(random.randint(200, 500))
487
+
488
+ for segment in segments:
489
+ result += segment
490
+ result += create_silence(random.randint(300, 700))
491
+
492
+ noise_level = random.uniform(0.01, 0.03)
493
+ result = add_background_noise(result, noise_level)
494
+
495
+ output_io = io.BytesIO()
496
+ result.export(output_io, format="mp3")
497
+ output_io.seek(0)
498
+
499
+ return output_io.read()
500
+ except ImportError:
501
+ logger.error("pydub not installed. Audio processing unavailable.")
502
+ return None
503
+
504
+
505
+ def add_background_noise(audio_segment, noise_level=0.05):
506
+ """Add background noise to an AudioSegment."""
507
+ noise = create_noise(len(audio_segment), level=noise_level)
508
+ return mix_audio(audio_segment, noise)
@@ -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
 
@@ -0,0 +1,20 @@
1
+ flask_humanify/__init__.py,sha256=m00B4jvNt8wDtNtodyHv1GKDxW3CdblFpVE1Bea0-7g,269
2
+ flask_humanify/humanify.py,sha256=YZFndaMwC24VtddrH5_SX7Lx995QKZGehv49kDK181w,16252
3
+ flask_humanify/memory_server.py,sha256=pmW1MjFYaLVizjzL_9YuDTc2T-3XkkdKLK9QPfmQ-4U,30310
4
+ flask_humanify/secret_key.bin,sha256=5e9HXg-A5HtFRVYv4zA1rkSQAP03RcnwdCpnA_98_8Q,32
5
+ flask_humanify/utils.py,sha256=jWAQDZmH356VBRnfSgPloQbqkNj6bbRjIOXNRUAPUyk,16016
6
+ flask_humanify/datasets/ai_dogs.pkl,sha256=ODznMPafM3d4ZNM2MPqEdX21nh53CZhsLornWc1IZPc,24645910
7
+ flask_humanify/datasets/animals.pkl,sha256=zhOY-J3h18ZMBE5D9q_ujQc7ZW1WRNcuYfM4J2P1huM,10323365
8
+ flask_humanify/datasets/characters.pkl,sha256=tEbGnbi5S9Y2Us9arVghdV43luqOAiGTQMLiU2bka6U,12756114
9
+ flask_humanify/datasets/ipset.json,sha256=YNPqwI109lYkfvZeOPsoDH_dKJxOCs0G2nvx_s2mvqU,30601191
10
+ flask_humanify/features/rate_limiter.py,sha256=QMIwfEllTDup6jxckEbE83PlJZeLw-0SvyxPqzXJYzU,2204
11
+ flask_humanify/templates/access_denied.html,sha256=p8ea_9gvv83aYFHaVKKedmQr6M8Z7NyHJK_OT3jdTOs,3169
12
+ flask_humanify/templates/audio_challenge.html,sha256=1QR-EQs3CJleT64jWxAreJG-RIUHuQox-jXswOCemXg,6397
13
+ flask_humanify/templates/grid_challenge.html,sha256=cToubxQvI7bJ-BKS9wceIvsAmp6h2EGYLhgyRr6pFxU,7179
14
+ flask_humanify/templates/one_click_challenge.html,sha256=21WOkYWrW48GXpVPBMPrEJ7Iq5JawkrXKUyUUfxWmTc,5996
15
+ flask_humanify/templates/rate_limited.html,sha256=Bv98MoetuSJGpWkDaQfhl7JwcWJiGaG2gwqxvSphaTM,3114
16
+ flask_humanify-0.2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
17
+ flask_humanify-0.2.0.dist-info/METADATA,sha256=ah_0kX60dPqLsJF6LreSX85_YgbdxjTL7_gGNHRupPI,3233
18
+ flask_humanify-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ flask_humanify-0.2.0.dist-info/top_level.txt,sha256=9-c6uhxwCpPE3BJYge1Y9Z_bYmWitI0fY5RgqMiFWr0,15
20
+ flask_humanify-0.2.0.dist-info/RECORD,,