flask-Humanify 0.1.3.2__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/__init__.py +1 -1
- flask_humanify/datasets/ai_dogs.pkl +0 -0
- flask_humanify/datasets/animals.pkl +0 -0
- flask_humanify/datasets/characters.pkl +0 -0
- flask_humanify/features/rate_limiter.py +1 -1
- flask_humanify/humanify.py +393 -16
- flask_humanify/memory_server.py +836 -0
- flask_humanify/secret_key.bin +0 -0
- flask_humanify/templates/access_denied.html +32 -25
- flask_humanify/templates/audio_challenge.html +208 -0
- flask_humanify/templates/grid_challenge.html +232 -0
- flask_humanify/templates/{oneclick_captcha.html → one_click_challenge.html} +48 -45
- flask_humanify/templates/rate_limited.html +32 -25
- flask_humanify/utils.py +422 -2
- {flask_humanify-0.1.3.2.dist-info → flask_humanify-0.2.0.dist-info}/METADATA +14 -4
- flask_humanify-0.2.0.dist-info/RECORD +20 -0
- flask_humanify/ipset.py +0 -315
- flask_humanify-0.1.3.2.dist-info/RECORD +0 -14
- {flask_humanify-0.1.3.2.dist-info → flask_humanify-0.2.0.dist-info}/WHEEL +0 -0
- {flask_humanify-0.1.3.2.dist-info → flask_humanify-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {flask_humanify-0.1.3.2.dist-info → flask_humanify-0.2.0.dist-info}/top_level.txt +0 -0
flask_humanify/utils.py
CHANGED
@@ -1,8 +1,25 @@
|
|
1
|
-
|
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
|
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.
|
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():
|
@@ -57,6 +62,7 @@ if __name__ == "__main__":
|
|
57
62
|
```
|
58
63
|
|
59
64
|
Not using the middleware:
|
65
|
+
|
60
66
|
```python
|
61
67
|
@app.route("/")
|
62
68
|
def index():
|
@@ -64,24 +70,28 @@ def index():
|
|
64
70
|
A route that is protected against bots and DDoS attacks.
|
65
71
|
"""
|
66
72
|
if humanify.is_bot:
|
67
|
-
return humanify.
|
73
|
+
return humanify.challenge()
|
68
74
|
return "Hello, Human!"
|
69
75
|
```
|
70
76
|
|
71
77
|
## Usage
|
72
78
|
|
73
79
|
### Installation
|
80
|
+
|
74
81
|
Install the package with pip:
|
82
|
+
|
75
83
|
```bash
|
76
84
|
pip install flask-humanify --upgrade
|
77
85
|
```
|
78
86
|
|
79
87
|
Import the extension:
|
88
|
+
|
80
89
|
```python
|
81
90
|
from flask_humanify import Humanify
|
82
91
|
```
|
83
92
|
|
84
93
|
Add the extension to your Flask app:
|
94
|
+
|
85
95
|
```python
|
86
96
|
app = Flask(__name__)
|
87
97
|
humanify = Humanify(app)
|
@@ -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,,
|