flask-Humanify 0.2.0__tar.gz → 0.2.1__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.2.0/flask_Humanify.egg-info → flask_humanify-0.2.1}/PKG-INFO +35 -2
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/README.md +34 -1
- {flask_humanify-0.2.0 → flask_humanify-0.2.1/flask_Humanify.egg-info}/PKG-INFO +35 -2
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/SOURCES.txt +1 -1
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/__init__.py +1 -1
- flask_humanify-0.2.1/flask_humanify/datasets/keys.pkl +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/humanify.py +183 -21
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/memory_server.py +5 -3
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/utils.py +86 -20
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/pyproject.toml +1 -1
- flask_humanify-0.2.0/flask_humanify/secret_key.bin +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/LICENSE +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/MANIFEST.in +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/dependency_links.txt +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/requires.txt +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_Humanify.egg-info/top_level.txt +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/ai_dogs.pkl +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/animals.pkl +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/characters.pkl +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/datasets/ipset.json +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/features/rate_limiter.py +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/access_denied.html +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/audio_challenge.html +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/grid_challenge.html +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/one_click_challenge.html +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/rate_limited.html +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/setup.cfg +0 -0
- {flask_humanify-0.2.0 → flask_humanify-0.2.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: flask-Humanify
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: Protect against bots and DDoS attacks
|
5
5
|
Author-email: TN3W <tn3w@protonmail.com>
|
6
6
|
License-Expression: Apache-2.0
|
@@ -45,7 +45,7 @@ from flask import Flask
|
|
45
45
|
from flask_Humanify import Humanify
|
46
46
|
|
47
47
|
app = Flask(__name__)
|
48
|
-
humanify = Humanify(app, challenge_type="one_click",
|
48
|
+
humanify = Humanify(app, challenge_type="one_click", image_dataset="ai_dogs")
|
49
49
|
|
50
50
|
# Register the middleware to deny access to bots
|
51
51
|
humanify.register_middleware(action="challenge")
|
@@ -61,6 +61,39 @@ if __name__ == "__main__":
|
|
61
61
|
app.run()
|
62
62
|
```
|
63
63
|
|
64
|
+
### Advanced Protection Rules
|
65
|
+
|
66
|
+
You can customize bot protection with advanced filtering rules:
|
67
|
+
|
68
|
+
```python
|
69
|
+
# Protect specific endpoints with regex patterns
|
70
|
+
humanify.register_middleware(
|
71
|
+
action="challenge",
|
72
|
+
endpoint_patterns=["api.*", "admin.*"] # Protect all API and admin endpoints
|
73
|
+
)
|
74
|
+
|
75
|
+
# Protect specific URL paths
|
76
|
+
humanify.register_middleware(
|
77
|
+
action="deny_access",
|
78
|
+
url_patterns=["/sensitive/*", "/admin/*"] # Deny bot access to sensitive areas
|
79
|
+
)
|
80
|
+
|
81
|
+
# Exclude certain patterns from protection
|
82
|
+
humanify.register_middleware(
|
83
|
+
endpoint_patterns=["api.*"],
|
84
|
+
exclude_patterns=["api.public.*"] # Don't protect public API endpoints
|
85
|
+
)
|
86
|
+
|
87
|
+
# Filter by request parameters
|
88
|
+
humanify.register_middleware(
|
89
|
+
request_filters={
|
90
|
+
"method": ["POST", "PUT", "DELETE"], # Only protect write operations
|
91
|
+
"args.admin": "true", # Only when admin=true query parameter exists
|
92
|
+
"headers.content-type": "regex:application/json.*" # Match content type with regex
|
93
|
+
}
|
94
|
+
)
|
95
|
+
```
|
96
|
+
|
64
97
|
Not using the middleware:
|
65
98
|
|
66
99
|
```python
|
@@ -9,7 +9,7 @@ from flask import Flask
|
|
9
9
|
from flask_Humanify import Humanify
|
10
10
|
|
11
11
|
app = Flask(__name__)
|
12
|
-
humanify = Humanify(app, challenge_type="one_click",
|
12
|
+
humanify = Humanify(app, challenge_type="one_click", image_dataset="ai_dogs")
|
13
13
|
|
14
14
|
# Register the middleware to deny access to bots
|
15
15
|
humanify.register_middleware(action="challenge")
|
@@ -25,6 +25,39 @@ if __name__ == "__main__":
|
|
25
25
|
app.run()
|
26
26
|
```
|
27
27
|
|
28
|
+
### Advanced Protection Rules
|
29
|
+
|
30
|
+
You can customize bot protection with advanced filtering rules:
|
31
|
+
|
32
|
+
```python
|
33
|
+
# Protect specific endpoints with regex patterns
|
34
|
+
humanify.register_middleware(
|
35
|
+
action="challenge",
|
36
|
+
endpoint_patterns=["api.*", "admin.*"] # Protect all API and admin endpoints
|
37
|
+
)
|
38
|
+
|
39
|
+
# Protect specific URL paths
|
40
|
+
humanify.register_middleware(
|
41
|
+
action="deny_access",
|
42
|
+
url_patterns=["/sensitive/*", "/admin/*"] # Deny bot access to sensitive areas
|
43
|
+
)
|
44
|
+
|
45
|
+
# Exclude certain patterns from protection
|
46
|
+
humanify.register_middleware(
|
47
|
+
endpoint_patterns=["api.*"],
|
48
|
+
exclude_patterns=["api.public.*"] # Don't protect public API endpoints
|
49
|
+
)
|
50
|
+
|
51
|
+
# Filter by request parameters
|
52
|
+
humanify.register_middleware(
|
53
|
+
request_filters={
|
54
|
+
"method": ["POST", "PUT", "DELETE"], # Only protect write operations
|
55
|
+
"args.admin": "true", # Only when admin=true query parameter exists
|
56
|
+
"headers.content-type": "regex:application/json.*" # Match content type with regex
|
57
|
+
}
|
58
|
+
)
|
59
|
+
```
|
60
|
+
|
28
61
|
Not using the middleware:
|
29
62
|
|
30
63
|
```python
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: flask-Humanify
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: Protect against bots and DDoS attacks
|
5
5
|
Author-email: TN3W <tn3w@protonmail.com>
|
6
6
|
License-Expression: Apache-2.0
|
@@ -45,7 +45,7 @@ from flask import Flask
|
|
45
45
|
from flask_Humanify import Humanify
|
46
46
|
|
47
47
|
app = Flask(__name__)
|
48
|
-
humanify = Humanify(app, challenge_type="one_click",
|
48
|
+
humanify = Humanify(app, challenge_type="one_click", image_dataset="ai_dogs")
|
49
49
|
|
50
50
|
# Register the middleware to deny access to bots
|
51
51
|
humanify.register_middleware(action="challenge")
|
@@ -61,6 +61,39 @@ if __name__ == "__main__":
|
|
61
61
|
app.run()
|
62
62
|
```
|
63
63
|
|
64
|
+
### Advanced Protection Rules
|
65
|
+
|
66
|
+
You can customize bot protection with advanced filtering rules:
|
67
|
+
|
68
|
+
```python
|
69
|
+
# Protect specific endpoints with regex patterns
|
70
|
+
humanify.register_middleware(
|
71
|
+
action="challenge",
|
72
|
+
endpoint_patterns=["api.*", "admin.*"] # Protect all API and admin endpoints
|
73
|
+
)
|
74
|
+
|
75
|
+
# Protect specific URL paths
|
76
|
+
humanify.register_middleware(
|
77
|
+
action="deny_access",
|
78
|
+
url_patterns=["/sensitive/*", "/admin/*"] # Deny bot access to sensitive areas
|
79
|
+
)
|
80
|
+
|
81
|
+
# Exclude certain patterns from protection
|
82
|
+
humanify.register_middleware(
|
83
|
+
endpoint_patterns=["api.*"],
|
84
|
+
exclude_patterns=["api.public.*"] # Don't protect public API endpoints
|
85
|
+
)
|
86
|
+
|
87
|
+
# Filter by request parameters
|
88
|
+
humanify.register_middleware(
|
89
|
+
request_filters={
|
90
|
+
"method": ["POST", "PUT", "DELETE"], # Only protect write operations
|
91
|
+
"args.admin": "true", # Only when admin=true query parameter exists
|
92
|
+
"headers.content-type": "regex:application/json.*" # Match content type with regex
|
93
|
+
}
|
94
|
+
)
|
95
|
+
```
|
96
|
+
|
64
97
|
Not using the middleware:
|
65
98
|
|
66
99
|
```python
|
@@ -11,12 +11,12 @@ flask_Humanify.egg-info/top_level.txt
|
|
11
11
|
flask_humanify/__init__.py
|
12
12
|
flask_humanify/humanify.py
|
13
13
|
flask_humanify/memory_server.py
|
14
|
-
flask_humanify/secret_key.bin
|
15
14
|
flask_humanify/utils.py
|
16
15
|
flask_humanify/datasets/ai_dogs.pkl
|
17
16
|
flask_humanify/datasets/animals.pkl
|
18
17
|
flask_humanify/datasets/characters.pkl
|
19
18
|
flask_humanify/datasets/ipset.json
|
19
|
+
flask_humanify/datasets/keys.pkl
|
20
20
|
flask_humanify/features/rate_limiter.py
|
21
21
|
flask_humanify/templates/access_denied.html
|
22
22
|
flask_humanify/templates/audio_challenge.html
|
Binary file
|
@@ -1,7 +1,9 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
2
|
import logging
|
3
3
|
import random
|
4
|
-
from typing import List, Optional
|
4
|
+
from typing import List, Optional, Union, Dict, Any, Pattern
|
5
|
+
import re
|
6
|
+
import fnmatch
|
5
7
|
|
6
8
|
from werkzeug.wrappers import Response
|
7
9
|
from flask import (
|
@@ -46,13 +48,13 @@ IMAGE_CAPTCHA_MAPPING = {
|
|
46
48
|
"num_correct": (2, 3),
|
47
49
|
"num_images": 9,
|
48
50
|
"preview_image": False,
|
49
|
-
"hardness_range": (1,
|
51
|
+
"hardness_range": (1, 3),
|
50
52
|
},
|
51
53
|
"one_click": {
|
52
54
|
"num_correct": 1,
|
53
55
|
"num_images": 6,
|
54
56
|
"preview_image": True,
|
55
|
-
"hardness_range": (1,
|
57
|
+
"hardness_range": (1, 3),
|
56
58
|
},
|
57
59
|
}
|
58
60
|
|
@@ -127,11 +129,16 @@ class Humanify:
|
|
127
129
|
"""
|
128
130
|
|
129
131
|
def __init__(
|
130
|
-
self,
|
132
|
+
self,
|
133
|
+
app=None,
|
134
|
+
challenge_type: str = "one_click",
|
135
|
+
image_dataset: Optional[str] = "ai_dogs",
|
136
|
+
audio_dataset: Optional[str] = None,
|
131
137
|
):
|
132
138
|
self.app = app
|
133
139
|
self.challenge_type = challenge_type
|
134
|
-
self.
|
140
|
+
self.image_dataset = image_dataset
|
141
|
+
self.audio_dataset = audio_dataset
|
135
142
|
if app is not None:
|
136
143
|
self.init_app(app)
|
137
144
|
|
@@ -142,7 +149,8 @@ class Humanify:
|
|
142
149
|
self.app = app
|
143
150
|
|
144
151
|
ensure_server_running(
|
145
|
-
image_dataset=self.
|
152
|
+
image_dataset=self.image_dataset,
|
153
|
+
audio_dataset=self.audio_dataset,
|
146
154
|
)
|
147
155
|
self.memory_client = MemoryClient()
|
148
156
|
self.memory_client.connect()
|
@@ -162,6 +170,9 @@ class Humanify:
|
|
162
170
|
"""
|
163
171
|
Challenge route.
|
164
172
|
"""
|
173
|
+
if self.image_dataset is None:
|
174
|
+
return self._render_challenge(is_audio=True)
|
175
|
+
|
165
176
|
return self._render_challenge()
|
166
177
|
|
167
178
|
@self.blueprint.route("/humanify/audio_challenge", methods=["GET"])
|
@@ -169,6 +180,11 @@ class Humanify:
|
|
169
180
|
"""
|
170
181
|
Audio challenge route.
|
171
182
|
"""
|
183
|
+
if self.audio_dataset is None:
|
184
|
+
return redirect(
|
185
|
+
url_for("humanify.challenge", return_url=request.full_path)
|
186
|
+
)
|
187
|
+
|
172
188
|
return self._render_challenge(is_audio=True)
|
173
189
|
|
174
190
|
@self.blueprint.route("/humanify/verify", methods=["POST"])
|
@@ -176,6 +192,9 @@ class Humanify:
|
|
176
192
|
"""
|
177
193
|
Verify route.
|
178
194
|
"""
|
195
|
+
if self.image_dataset is None:
|
196
|
+
abort(404)
|
197
|
+
|
179
198
|
return self._verify_captcha()
|
180
199
|
|
181
200
|
@self.blueprint.route("/humanify/verify_audio", methods=["POST"])
|
@@ -183,6 +202,9 @@ class Humanify:
|
|
183
202
|
"""
|
184
203
|
Verify audio route.
|
185
204
|
"""
|
205
|
+
if self.audio_dataset is None:
|
206
|
+
abort(404)
|
207
|
+
|
186
208
|
return self._verify_audio_captcha()
|
187
209
|
|
188
210
|
@self.blueprint.route("/humanify/access_denied", methods=["GET"])
|
@@ -198,27 +220,168 @@ class Humanify:
|
|
198
220
|
{"Cache-Control": "public, max-age=15552000"},
|
199
221
|
)
|
200
222
|
|
201
|
-
def register_middleware(
|
223
|
+
def register_middleware(
|
224
|
+
self,
|
225
|
+
action: str = "challenge",
|
226
|
+
endpoint_patterns: Union[str, List[str], None] = None,
|
227
|
+
url_patterns: Union[str, List[str], None] = None,
|
228
|
+
exclude_patterns: Union[str, List[str], None] = None,
|
229
|
+
request_filters: Optional[Dict[str, Any]] = None,
|
230
|
+
):
|
202
231
|
"""
|
203
|
-
Register the middleware.
|
232
|
+
Register the middleware with advanced filtering options.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
action: The action to take when a bot is detected ('challenge' or 'deny_access')
|
236
|
+
endpoint_patterns: Endpoint patterns to match (regex or glob patterns)
|
237
|
+
url_patterns: URL patterns to match (regex or glob patterns)
|
238
|
+
exclude_patterns: Patterns to exclude from protection (regex or glob patterns)
|
239
|
+
request_filters: Dict of request attributes and values to filter by
|
204
240
|
"""
|
205
|
-
|
206
241
|
self.app = self.app or current_app
|
207
242
|
|
243
|
+
if isinstance(endpoint_patterns, str):
|
244
|
+
endpoint_patterns = [endpoint_patterns]
|
245
|
+
if isinstance(url_patterns, str):
|
246
|
+
url_patterns = [url_patterns]
|
247
|
+
if isinstance(exclude_patterns, str):
|
248
|
+
exclude_patterns = [exclude_patterns]
|
249
|
+
|
250
|
+
compiled_endpoint_patterns = (
|
251
|
+
self._compile_patterns(endpoint_patterns) if endpoint_patterns else None
|
252
|
+
)
|
253
|
+
compiled_url_patterns = (
|
254
|
+
self._compile_patterns(url_patterns) if url_patterns else None
|
255
|
+
)
|
256
|
+
compiled_exclude_patterns = (
|
257
|
+
self._compile_patterns(exclude_patterns) if exclude_patterns else None
|
258
|
+
)
|
259
|
+
|
208
260
|
@self.app.before_request
|
209
261
|
def before_request():
|
210
262
|
"""
|
211
|
-
Before request hook.
|
263
|
+
Before request hook with advanced filtering.
|
212
264
|
"""
|
213
265
|
if request.endpoint and request.endpoint.startswith("humanify."):
|
214
266
|
return
|
215
267
|
|
216
|
-
|
268
|
+
current_endpoint = request.endpoint or ""
|
269
|
+
current_path = request.path
|
270
|
+
|
271
|
+
if compiled_exclude_patterns and self._matches_any_pattern(
|
272
|
+
current_endpoint, current_path, compiled_exclude_patterns
|
273
|
+
):
|
274
|
+
return
|
275
|
+
|
276
|
+
patterns_specified = (
|
277
|
+
compiled_endpoint_patterns is not None
|
278
|
+
or compiled_url_patterns is not None
|
279
|
+
)
|
280
|
+
|
281
|
+
matches_endpoint = not patterns_specified or (
|
282
|
+
compiled_endpoint_patterns
|
283
|
+
and self._matches_any_pattern(
|
284
|
+
current_endpoint, None, compiled_endpoint_patterns
|
285
|
+
)
|
286
|
+
)
|
287
|
+
|
288
|
+
matches_url = not patterns_specified or (
|
289
|
+
compiled_url_patterns
|
290
|
+
and self._matches_any_pattern(None, current_path, compiled_url_patterns)
|
291
|
+
)
|
292
|
+
|
293
|
+
matches_request_filters = (
|
294
|
+
not request_filters or self._matches_request_filters(request_filters)
|
295
|
+
)
|
296
|
+
|
297
|
+
if (
|
298
|
+
(matches_endpoint or matches_url)
|
299
|
+
and matches_request_filters
|
300
|
+
and self.is_bot
|
301
|
+
):
|
217
302
|
if action == "challenge":
|
218
303
|
return self.challenge()
|
219
304
|
if action == "deny_access":
|
220
305
|
return self.deny_access()
|
221
306
|
|
307
|
+
def _compile_patterns(self, patterns):
|
308
|
+
"""
|
309
|
+
Compile a list of patterns into regex patterns.
|
310
|
+
Handles glob patterns like * and ? by converting them to regex.
|
311
|
+
"""
|
312
|
+
compiled = []
|
313
|
+
for pattern in patterns:
|
314
|
+
if pattern is None:
|
315
|
+
continue
|
316
|
+
|
317
|
+
if "*" in pattern or "?" in pattern:
|
318
|
+
regex_pattern = fnmatch.translate(pattern)
|
319
|
+
compiled.append(re.compile(regex_pattern))
|
320
|
+
else:
|
321
|
+
try:
|
322
|
+
compiled.append(re.compile(pattern))
|
323
|
+
except re.error:
|
324
|
+
compiled.append(re.compile(re.escape(pattern)))
|
325
|
+
|
326
|
+
return compiled
|
327
|
+
|
328
|
+
def _matches_any_pattern(
|
329
|
+
self,
|
330
|
+
endpoint: Optional[str],
|
331
|
+
path: Optional[str],
|
332
|
+
compiled_patterns: List[Pattern],
|
333
|
+
):
|
334
|
+
"""
|
335
|
+
Check if the current endpoint or path matches any of the compiled patterns.
|
336
|
+
"""
|
337
|
+
for pattern in compiled_patterns:
|
338
|
+
if endpoint is not None and pattern.search(endpoint):
|
339
|
+
return True
|
340
|
+
if path is not None and pattern.search(path):
|
341
|
+
return True
|
342
|
+
return False
|
343
|
+
|
344
|
+
def _matches_request_filters(self, request_filters: Dict[str, Any]) -> bool:
|
345
|
+
"""
|
346
|
+
Check if the current request matches all the specified filters.
|
347
|
+
Filters can target any attribute of the request object or its nested properties.
|
348
|
+
"""
|
349
|
+
for key, value in request_filters.items():
|
350
|
+
parts = key.split(".")
|
351
|
+
obj = request
|
352
|
+
|
353
|
+
for part in parts[:-1]:
|
354
|
+
if hasattr(obj, part):
|
355
|
+
obj = getattr(obj, part)
|
356
|
+
elif isinstance(obj, dict) and part in obj:
|
357
|
+
obj = obj[part]
|
358
|
+
else:
|
359
|
+
return False
|
360
|
+
|
361
|
+
final_attr = parts[-1]
|
362
|
+
|
363
|
+
if hasattr(obj, final_attr):
|
364
|
+
attr_value = getattr(obj, final_attr)
|
365
|
+
elif isinstance(obj, dict) and final_attr in obj:
|
366
|
+
attr_value = obj[final_attr]
|
367
|
+
else:
|
368
|
+
return False
|
369
|
+
|
370
|
+
if isinstance(value, str) and value.startswith("regex:"):
|
371
|
+
regex_pattern = value[6:]
|
372
|
+
try:
|
373
|
+
if not re.search(regex_pattern, str(attr_value)):
|
374
|
+
return False
|
375
|
+
except (re.error, TypeError):
|
376
|
+
return False
|
377
|
+
elif isinstance(value, list):
|
378
|
+
if attr_value not in value:
|
379
|
+
return False
|
380
|
+
elif attr_value != value:
|
381
|
+
return False
|
382
|
+
|
383
|
+
return True
|
384
|
+
|
222
385
|
@property
|
223
386
|
def client_ip(self) -> Optional[str]:
|
224
387
|
"""Get the client IP address."""
|
@@ -310,7 +473,7 @@ class Humanify:
|
|
310
473
|
num_correct=captcha_config["num_correct"],
|
311
474
|
num_images=captcha_config["num_images"],
|
312
475
|
preview_image=use_preview_image,
|
313
|
-
dataset_name=self.
|
476
|
+
dataset_name=self.image_dataset,
|
314
477
|
)
|
315
478
|
|
316
479
|
if not images_bytes:
|
@@ -319,16 +482,15 @@ class Humanify:
|
|
319
482
|
processed_images = []
|
320
483
|
for i, img_bytes in enumerate(images_bytes):
|
321
484
|
try:
|
322
|
-
|
323
|
-
captcha_config["hardness_range"][0],
|
324
|
-
captcha_config["hardness_range"][1],
|
325
|
-
)
|
326
|
-
distorted = manipulate_image_bytes(
|
485
|
+
distorted_img_bytes = manipulate_image_bytes(
|
327
486
|
img_bytes,
|
328
487
|
is_small=not (i == 0 and use_preview_image),
|
329
|
-
hardness=
|
488
|
+
hardness=random.randint(
|
489
|
+
captcha_config["hardness_range"][0],
|
490
|
+
captcha_config["hardness_range"][1],
|
491
|
+
),
|
330
492
|
)
|
331
|
-
processed_images.append(image_bytes_to_data_url(
|
493
|
+
processed_images.append(image_bytes_to_data_url(distorted_img_bytes))
|
332
494
|
except Exception as e:
|
333
495
|
current_app.logger.error(f"Error processing image: {e}")
|
334
496
|
processed_images.append(
|
@@ -359,7 +521,7 @@ class Humanify:
|
|
359
521
|
captcha_data=captcha_data,
|
360
522
|
return_url=return_url or "/",
|
361
523
|
error=error,
|
362
|
-
audio_challenge_available=
|
524
|
+
audio_challenge_available=self.audio_dataset is not None,
|
363
525
|
),
|
364
526
|
mimetype="text/html",
|
365
527
|
)
|
@@ -400,7 +562,7 @@ class Humanify:
|
|
400
562
|
captcha_data=captcha_data,
|
401
563
|
return_url=return_url or "/",
|
402
564
|
error=error,
|
403
|
-
image_challenge_available=
|
565
|
+
image_challenge_available=self.image_dataset is not None,
|
404
566
|
),
|
405
567
|
mimetype="text/html",
|
406
568
|
)
|
@@ -204,7 +204,9 @@ class MemoryServer:
|
|
204
204
|
return False
|
205
205
|
|
206
206
|
def load_captcha_datasets(
|
207
|
-
self,
|
207
|
+
self,
|
208
|
+
image_dataset: Optional[str] = None,
|
209
|
+
audio_dataset: Optional[str] = None,
|
208
210
|
) -> bool:
|
209
211
|
"""Load captcha datasets into memory."""
|
210
212
|
try:
|
@@ -819,8 +821,8 @@ class MemoryClient:
|
|
819
821
|
def ensure_server_running(
|
820
822
|
port: int = 9876,
|
821
823
|
data_path: Optional[str] = None,
|
822
|
-
image_dataset: str =
|
823
|
-
audio_dataset: str =
|
824
|
+
image_dataset: Optional[str] = None,
|
825
|
+
audio_dataset: Optional[str] = None,
|
824
826
|
) -> None:
|
825
827
|
"""Ensure that the memory server is running."""
|
826
828
|
if data_path is None:
|
@@ -251,6 +251,8 @@ def manipulate_image_bytes(
|
|
251
251
|
"""Manipulates an image represented by bytes to create a distorted version."""
|
252
252
|
# pylint: disable=no-member
|
253
253
|
|
254
|
+
hardness = min(max(1, hardness), 4)
|
255
|
+
|
254
256
|
img = cv2.imdecode(np.frombuffer(image_data, np.uint8), cv2.IMREAD_COLOR)
|
255
257
|
if img is None:
|
256
258
|
logger.error("Image data could not be decoded by OpenCV")
|
@@ -259,25 +261,85 @@ def manipulate_image_bytes(
|
|
259
261
|
size = 100 if is_small else 200
|
260
262
|
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_LINEAR)
|
261
263
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
264
|
+
mask_pattern = np.zeros((size, size, 3), dtype=np.uint8)
|
265
|
+
|
266
|
+
grid_size = max(8, 16 - hardness * 2)
|
267
|
+
for i in range(0, size, grid_size):
|
268
|
+
thickness = 1
|
269
|
+
cv2.line(mask_pattern, (i, 0), (i, size), (2, 2, 2), thickness)
|
270
|
+
cv2.line(mask_pattern, (0, i), (size, i), (2, 2, 2), thickness)
|
266
271
|
|
267
|
-
|
268
|
-
|
272
|
+
mask_opacity = min(0.06 + hardness * 0.03, 0.18)
|
273
|
+
img = cv2.addWeighted(img, 1 - mask_opacity, mask_pattern, mask_opacity, 0)
|
269
274
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
275
|
+
noise_max = max(1, 1 + hardness // 2)
|
276
|
+
noise_pattern = np.random.randint(
|
277
|
+
0, noise_max, size=(size, size, 3), dtype=np.uint8
|
278
|
+
)
|
279
|
+
img = cv2.add(img, noise_pattern)
|
280
|
+
|
281
|
+
num_dots = np.random.randint(5 + 5 * hardness, 10 + 10 * hardness + 1)
|
282
|
+
dot_coords = np.random.randint(0, [size, size], size=(num_dots, 2))
|
283
|
+
|
284
|
+
dot_intensity = 0.05 + hardness * 0.05
|
285
|
+
rand_max = max(1, 10 * hardness)
|
286
|
+
colors = np.random.randint(0, rand_max, size=(num_dots, 3)) + np.array(
|
287
|
+
[img[coord[1], coord[0]] for coord in dot_coords]
|
288
|
+
) * (1 - dot_intensity)
|
289
|
+
colors = np.clip(colors, 0, 255).astype(np.uint8)
|
290
|
+
|
291
|
+
for (x, y), color in zip(dot_coords, colors):
|
292
|
+
img[y, x] = color
|
293
|
+
|
294
|
+
num_lines = np.random.randint(2 * hardness, 5 * hardness + 1)
|
295
|
+
start_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
|
296
|
+
end_coords = np.random.randint(0, [size, size], size=(num_lines, 2))
|
297
|
+
|
298
|
+
line_intensity = max(4, 3 * hardness)
|
299
|
+
colors = np.random.randint(3, line_intensity, size=(num_lines, 3))
|
300
|
+
|
301
|
+
for (start, end), color in zip(zip(start_coords, end_coords), colors):
|
302
|
+
cv2.line(img, tuple(start), tuple(end), color.tolist(), 1)
|
303
|
+
|
304
|
+
for _ in range(hardness):
|
305
|
+
x = np.random.randint(0, size)
|
306
|
+
y = np.random.randint(0, size)
|
307
|
+
length = np.random.randint(5 + 3 * hardness, 10 + 5 * hardness + 1)
|
308
|
+
angle = np.random.randint(0, 360)
|
309
|
+
text_max = max(3, 2 + hardness)
|
310
|
+
text_color = np.random.randint(1, text_max, 3).tolist()
|
311
|
+
|
312
|
+
end_x = int(x + length * np.cos(np.radians(angle)))
|
313
|
+
end_y = int(y + length * np.sin(np.radians(angle)))
|
314
|
+
cv2.line(img, (x, y), (end_x, end_y), text_color, 1)
|
315
|
+
|
316
|
+
for _ in range(1 + hardness // 2):
|
317
|
+
patch_size = np.random.randint(4 + hardness, 6 + 3 * hardness + 1)
|
318
|
+
x = np.random.randint(0, size - patch_size)
|
319
|
+
y = np.random.randint(0, size - patch_size)
|
320
|
+
|
321
|
+
patch = np.zeros((patch_size, patch_size, 3), dtype=np.uint8)
|
322
|
+
for i in range(0, patch_size, 2):
|
323
|
+
for j in range(0, patch_size, 2):
|
324
|
+
if (i + j) % 4 == 0:
|
325
|
+
patch_color_max = max(2, 1 + hardness)
|
326
|
+
patch[i : i + 2, j : j + 2] = [
|
327
|
+
np.random.randint(1, patch_color_max)
|
328
|
+
] * 3
|
329
|
+
|
330
|
+
patch_opacity = 0.03 + 0.02 * hardness
|
331
|
+
roi = img[y : y + patch_size, x : x + patch_size]
|
332
|
+
img[y : y + patch_size, x : x + patch_size] = cv2.addWeighted(
|
333
|
+
roi, 1 - patch_opacity, patch, patch_opacity, 0
|
334
|
+
)
|
274
335
|
|
275
|
-
|
276
|
-
|
336
|
+
max_shift = hardness
|
337
|
+
x_shifts = np.random.randint(-max_shift, max_shift + 1, size=(size, size))
|
338
|
+
y_shifts = np.random.randint(-max_shift, max_shift + 1, size=(size, size))
|
277
339
|
|
278
|
-
|
279
|
-
|
280
|
-
|
340
|
+
saturation_factor = 1 + hardness * 0.05
|
341
|
+
value_factor = 1 - hardness * 0.03
|
342
|
+
blur_factor = hardness * 0.05
|
281
343
|
|
282
344
|
map_x, map_y = np.meshgrid(np.arange(size), np.arange(size))
|
283
345
|
map_x = (map_x + x_shifts) % size
|
@@ -289,14 +351,18 @@ def manipulate_image_bytes(
|
|
289
351
|
shifted_img_hsv = cv2.cvtColor(shifted_img, cv2.COLOR_BGR2HSV)
|
290
352
|
|
291
353
|
shifted_img_hsv[..., 1] = np.clip(
|
292
|
-
shifted_img_hsv[..., 1] *
|
293
|
-
)
|
294
|
-
shifted_img_hsv[..., 2] = np.clip(
|
295
|
-
shifted_img_hsv[..., 2] * (1 - hardness * 0.09), 0, 255
|
354
|
+
shifted_img_hsv[..., 1] * saturation_factor, 0, 255
|
296
355
|
)
|
356
|
+
shifted_img_hsv[..., 2] = np.clip(shifted_img_hsv[..., 2] * value_factor, 0, 255)
|
297
357
|
|
298
358
|
shifted_img = cv2.cvtColor(shifted_img_hsv, cv2.COLOR_HSV2BGR)
|
299
|
-
shifted_img = cv2.GaussianBlur(shifted_img, (5, 5),
|
359
|
+
shifted_img = cv2.GaussianBlur(shifted_img, (5, 5), blur_factor)
|
360
|
+
|
361
|
+
noise_high = max(1, 1 + hardness // 3)
|
362
|
+
high_freq_noise = np.random.randint(
|
363
|
+
0, noise_high, size=shifted_img.shape, dtype=np.uint8
|
364
|
+
)
|
365
|
+
shifted_img = cv2.add(shifted_img, high_freq_noise)
|
300
366
|
|
301
367
|
_, output_bytes = cv2.imencode(".png", shifted_img)
|
302
368
|
if not _:
|
Binary file
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{flask_humanify-0.2.0 → flask_humanify-0.2.1}/flask_humanify/templates/one_click_challenge.html
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|