flask-Humanify 0.2.0__tar.gz → 0.2.2__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.2.0/flask_Humanify.egg-info → flask_humanify-0.2.2}/PKG-INFO +112 -2
  2. flask_humanify-0.2.2/README.md +172 -0
  3. {flask_humanify-0.2.0 → flask_humanify-0.2.2/flask_Humanify.egg-info}/PKG-INFO +112 -2
  4. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_Humanify.egg-info/SOURCES.txt +3 -1
  5. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/__init__.py +3 -2
  6. flask_humanify-0.2.2/flask_humanify/datasets/keys.pkl +0 -0
  7. flask_humanify-0.2.2/flask_humanify/features/error_handler.py +177 -0
  8. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/humanify.py +183 -21
  9. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/memory_server.py +5 -3
  10. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/templates/audio_challenge.html +3 -1
  11. flask_humanify-0.2.2/flask_humanify/templates/exception.html +66 -0
  12. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/utils.py +86 -20
  13. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/pyproject.toml +1 -1
  14. flask_humanify-0.2.0/README.md +0 -62
  15. flask_humanify-0.2.0/flask_humanify/secret_key.bin +0 -0
  16. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/LICENSE +0 -0
  17. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/MANIFEST.in +0 -0
  18. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_Humanify.egg-info/dependency_links.txt +0 -0
  19. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_Humanify.egg-info/requires.txt +0 -0
  20. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_Humanify.egg-info/top_level.txt +0 -0
  21. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/datasets/ai_dogs.pkl +0 -0
  22. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/datasets/animals.pkl +0 -0
  23. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/datasets/characters.pkl +0 -0
  24. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/datasets/ipset.json +0 -0
  25. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/features/rate_limiter.py +0 -0
  26. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/templates/access_denied.html +0 -0
  27. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/templates/grid_challenge.html +0 -0
  28. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/templates/one_click_challenge.html +0 -0
  29. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/flask_humanify/templates/rate_limited.html +0 -0
  30. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/setup.cfg +0 -0
  31. {flask_humanify-0.2.0 → flask_humanify-0.2.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-Humanify
3
- Version: 0.2.0
3
+ Version: 0.2.2
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", captcha_dataset="ai_dogs")
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
@@ -96,3 +129,80 @@ Add the extension to your Flask app:
96
129
  app = Flask(__name__)
97
130
  humanify = Humanify(app)
98
131
  ```
132
+
133
+ ## Additional Features
134
+
135
+ ### Rate Limiting
136
+
137
+ Flask-Humanify includes a rate limiting feature to protect your application from excessive requests:
138
+
139
+ ```python
140
+ from flask import Flask
141
+ from flask_humanify import Humanify, RateLimiter
142
+
143
+ app = Flask(__name__)
144
+ humanify = Humanify(app)
145
+ # Default: 10 requests per 10 seconds
146
+ rate_limiter = RateLimiter(app)
147
+
148
+ # Or customize rate limits
149
+ rate_limiter = RateLimiter(app, max_requests=20, time_window=30)
150
+ ```
151
+
152
+ The rate limiter will automatically:
153
+
154
+ - Track requests by IP address
155
+ - Hash IPs for privacy
156
+ - Redirect to a rate-limited page when limits are exceeded
157
+ - Ignore rate limits for special pages like the rate-limited and access-denied pages
158
+
159
+ ### Error Handling
160
+
161
+ Flask-Humanify provides a clean error handling system:
162
+
163
+ ```python
164
+ from flask import Flask
165
+ from flask_humanify import Humanify, ErrorHandler
166
+
167
+ app = Flask(__name__)
168
+ humanify = Humanify(app)
169
+ # Handle all standard HTTP errors
170
+ error_handler = ErrorHandler(app)
171
+
172
+ # Or handle only specific error codes
173
+ error_handler = ErrorHandler(app, errors=[404, 429, 500])
174
+ ```
175
+
176
+ The error handler:
177
+
178
+ - Renders user-friendly error pages
179
+ - Uses the custom exception.html template
180
+ - Provides appropriate error messages and descriptions
181
+ - Includes HTTP status codes and titles
182
+
183
+ ### Complete Example
184
+
185
+ Here's a complete example combining all features:
186
+
187
+ ```python
188
+ from flask import Flask
189
+ from flask_humanify import Humanify, RateLimiter, ErrorHandler
190
+
191
+ app = Flask(__name__)
192
+ # Setup core protection
193
+ humanify = Humanify(app, challenge_type="one_click", image_dataset="animals")
194
+ humanify.register_middleware(action="challenge")
195
+
196
+ # Add rate limiting
197
+ rate_limiter = RateLimiter(app, max_requests=15, time_window=60)
198
+
199
+ # Add error handling
200
+ error_handler = ErrorHandler(app)
201
+
202
+ @app.route("/")
203
+ def index():
204
+ return "Hello, Human!"
205
+
206
+ if __name__ == "__main__":
207
+ app.run(debug=True)
208
+ ```
@@ -0,0 +1,172 @@
1
+ <h1 align="center">flask-Humanify</h1>
2
+ <p align="center">A strong bot protection system for Flask with many features: rate limiting, special rules for users, web crawler detection, and automatic bot detection.</p>
3
+ <p align="center"><a rel="noreferrer noopener" href="https://github.com/tn3w/flask-Humanify"><img alt="Github" src="https://img.shields.io/badge/Github-141e24.svg?&style=for-the-badge&logo=github&logoColor=white"></a> <a rel="noreferrer noopener" href="https://pypi.org/project/flask-Humanify/"><img alt="PyPI" src="https://img.shields.io/badge/PyPi-141e24.svg?&style=for-the-badge&logo=python&logoColor=white"></a> <a rel="noreferrer noopener" href="https://libraries.io/pypi/flask-Humanify"><img alt="Libraries.io" src="https://img.shields.io/badge/Libraries.io-141e24.svg?&style=for-the-badge&logo=npm&logoColor=white"></a></p>
4
+
5
+ <br>
6
+
7
+ ```python
8
+ from flask import Flask
9
+ from flask_Humanify import Humanify
10
+
11
+ app = Flask(__name__)
12
+ humanify = Humanify(app, challenge_type="one_click", image_dataset="ai_dogs")
13
+
14
+ # Register the middleware to deny access to bots
15
+ humanify.register_middleware(action="challenge")
16
+
17
+ @app.route("/")
18
+ def index():
19
+ """
20
+ A route that is protected against bots and DDoS attacks.
21
+ """
22
+ return "Hello, Human!"
23
+
24
+ if __name__ == "__main__":
25
+ app.run()
26
+ ```
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
+
61
+ Not using the middleware:
62
+
63
+ ```python
64
+ @app.route("/")
65
+ def index():
66
+ """
67
+ A route that is protected against bots and DDoS attacks.
68
+ """
69
+ if humanify.is_bot:
70
+ return humanify.challenge()
71
+ return "Hello, Human!"
72
+ ```
73
+
74
+ ## Usage
75
+
76
+ ### Installation
77
+
78
+ Install the package with pip:
79
+
80
+ ```bash
81
+ pip install flask-humanify --upgrade
82
+ ```
83
+
84
+ Import the extension:
85
+
86
+ ```python
87
+ from flask_humanify import Humanify
88
+ ```
89
+
90
+ Add the extension to your Flask app:
91
+
92
+ ```python
93
+ app = Flask(__name__)
94
+ humanify = Humanify(app)
95
+ ```
96
+
97
+ ## Additional Features
98
+
99
+ ### Rate Limiting
100
+
101
+ Flask-Humanify includes a rate limiting feature to protect your application from excessive requests:
102
+
103
+ ```python
104
+ from flask import Flask
105
+ from flask_humanify import Humanify, RateLimiter
106
+
107
+ app = Flask(__name__)
108
+ humanify = Humanify(app)
109
+ # Default: 10 requests per 10 seconds
110
+ rate_limiter = RateLimiter(app)
111
+
112
+ # Or customize rate limits
113
+ rate_limiter = RateLimiter(app, max_requests=20, time_window=30)
114
+ ```
115
+
116
+ The rate limiter will automatically:
117
+
118
+ - Track requests by IP address
119
+ - Hash IPs for privacy
120
+ - Redirect to a rate-limited page when limits are exceeded
121
+ - Ignore rate limits for special pages like the rate-limited and access-denied pages
122
+
123
+ ### Error Handling
124
+
125
+ Flask-Humanify provides a clean error handling system:
126
+
127
+ ```python
128
+ from flask import Flask
129
+ from flask_humanify import Humanify, ErrorHandler
130
+
131
+ app = Flask(__name__)
132
+ humanify = Humanify(app)
133
+ # Handle all standard HTTP errors
134
+ error_handler = ErrorHandler(app)
135
+
136
+ # Or handle only specific error codes
137
+ error_handler = ErrorHandler(app, errors=[404, 429, 500])
138
+ ```
139
+
140
+ The error handler:
141
+
142
+ - Renders user-friendly error pages
143
+ - Uses the custom exception.html template
144
+ - Provides appropriate error messages and descriptions
145
+ - Includes HTTP status codes and titles
146
+
147
+ ### Complete Example
148
+
149
+ Here's a complete example combining all features:
150
+
151
+ ```python
152
+ from flask import Flask
153
+ from flask_humanify import Humanify, RateLimiter, ErrorHandler
154
+
155
+ app = Flask(__name__)
156
+ # Setup core protection
157
+ humanify = Humanify(app, challenge_type="one_click", image_dataset="animals")
158
+ humanify.register_middleware(action="challenge")
159
+
160
+ # Add rate limiting
161
+ rate_limiter = RateLimiter(app, max_requests=15, time_window=60)
162
+
163
+ # Add error handling
164
+ error_handler = ErrorHandler(app)
165
+
166
+ @app.route("/")
167
+ def index():
168
+ return "Hello, Human!"
169
+
170
+ if __name__ == "__main__":
171
+ app.run(debug=True)
172
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-Humanify
3
- Version: 0.2.0
3
+ Version: 0.2.2
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", captcha_dataset="ai_dogs")
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
@@ -96,3 +129,80 @@ Add the extension to your Flask app:
96
129
  app = Flask(__name__)
97
130
  humanify = Humanify(app)
98
131
  ```
132
+
133
+ ## Additional Features
134
+
135
+ ### Rate Limiting
136
+
137
+ Flask-Humanify includes a rate limiting feature to protect your application from excessive requests:
138
+
139
+ ```python
140
+ from flask import Flask
141
+ from flask_humanify import Humanify, RateLimiter
142
+
143
+ app = Flask(__name__)
144
+ humanify = Humanify(app)
145
+ # Default: 10 requests per 10 seconds
146
+ rate_limiter = RateLimiter(app)
147
+
148
+ # Or customize rate limits
149
+ rate_limiter = RateLimiter(app, max_requests=20, time_window=30)
150
+ ```
151
+
152
+ The rate limiter will automatically:
153
+
154
+ - Track requests by IP address
155
+ - Hash IPs for privacy
156
+ - Redirect to a rate-limited page when limits are exceeded
157
+ - Ignore rate limits for special pages like the rate-limited and access-denied pages
158
+
159
+ ### Error Handling
160
+
161
+ Flask-Humanify provides a clean error handling system:
162
+
163
+ ```python
164
+ from flask import Flask
165
+ from flask_humanify import Humanify, ErrorHandler
166
+
167
+ app = Flask(__name__)
168
+ humanify = Humanify(app)
169
+ # Handle all standard HTTP errors
170
+ error_handler = ErrorHandler(app)
171
+
172
+ # Or handle only specific error codes
173
+ error_handler = ErrorHandler(app, errors=[404, 429, 500])
174
+ ```
175
+
176
+ The error handler:
177
+
178
+ - Renders user-friendly error pages
179
+ - Uses the custom exception.html template
180
+ - Provides appropriate error messages and descriptions
181
+ - Includes HTTP status codes and titles
182
+
183
+ ### Complete Example
184
+
185
+ Here's a complete example combining all features:
186
+
187
+ ```python
188
+ from flask import Flask
189
+ from flask_humanify import Humanify, RateLimiter, ErrorHandler
190
+
191
+ app = Flask(__name__)
192
+ # Setup core protection
193
+ humanify = Humanify(app, challenge_type="one_click", image_dataset="animals")
194
+ humanify.register_middleware(action="challenge")
195
+
196
+ # Add rate limiting
197
+ rate_limiter = RateLimiter(app, max_requests=15, time_window=60)
198
+
199
+ # Add error handling
200
+ error_handler = ErrorHandler(app)
201
+
202
+ @app.route("/")
203
+ def index():
204
+ return "Hello, Human!"
205
+
206
+ if __name__ == "__main__":
207
+ app.run(debug=True)
208
+ ```
@@ -11,15 +11,17 @@ 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
+ flask_humanify/features/error_handler.py
20
21
  flask_humanify/features/rate_limiter.py
21
22
  flask_humanify/templates/access_denied.html
22
23
  flask_humanify/templates/audio_challenge.html
24
+ flask_humanify/templates/exception.html
23
25
  flask_humanify/templates/grid_challenge.html
24
26
  flask_humanify/templates/one_click_challenge.html
25
27
  flask_humanify/templates/rate_limited.html
@@ -4,11 +4,12 @@ Flask-Humanify
4
4
  A Flask extension that protects against bots and DDoS attacks.
5
5
  """
6
6
 
7
- __version__ = "0.2.0"
7
+ __version__ = "0.2.2"
8
8
 
9
9
  from . import utils
10
10
  from .humanify import Humanify
11
11
  from .features.rate_limiter import RateLimiter
12
+ from .features.error_handler import ErrorHandler
12
13
 
13
14
 
14
- __all__ = ["Humanify", "RateLimiter", "utils"]
15
+ __all__ = ["Humanify", "RateLimiter", "ErrorHandler", "utils"]
@@ -0,0 +1,177 @@
1
+ from typing import Final, Optional
2
+ from flask import Flask, render_template
3
+
4
+
5
+ ERROR_CODES: Final[dict] = {
6
+ 400: {
7
+ "title": "Bad Request",
8
+ "description": "The server could not understand your request due to invalid syntax.",
9
+ },
10
+ 401: {
11
+ "title": "Unauthorized",
12
+ "description": "You must authenticate yourself to get the requested response.",
13
+ },
14
+ 403: {
15
+ "title": "Forbidden",
16
+ "description": "You do not have access rights to the content.",
17
+ },
18
+ 404: {
19
+ "title": "Not Found",
20
+ "description": "The server cannot find the requested resource.",
21
+ },
22
+ 405: {
23
+ "title": "Method Not Allowed",
24
+ "description": "The request method is known by the server but is not supported by the target resource.",
25
+ },
26
+ 406: {
27
+ "title": "Not Acceptable",
28
+ "description": (
29
+ "The server cannot produce a response matching the list of acceptable values "
30
+ "defined in your request's proactive content negotiation headers."
31
+ ),
32
+ },
33
+ 408: {
34
+ "title": "Request Timeout",
35
+ "description": (
36
+ "The server did not receive a complete request message from you within the time that "
37
+ "it was prepared to wait."
38
+ ),
39
+ },
40
+ 409: {
41
+ "title": "Conflict",
42
+ "description": (
43
+ "The request could not be completed due to a conflict with the current state of "
44
+ "the target resource."
45
+ ),
46
+ },
47
+ 410: {
48
+ "title": "Gone",
49
+ "description": "The requested resource is no longer available and will not be available again.",
50
+ },
51
+ 411: {
52
+ "title": "Length Required",
53
+ "description": "The server refuses to accept the request without a defined Content-Length header.",
54
+ },
55
+ 412: {
56
+ "title": "Precondition Failed",
57
+ "description": (
58
+ "The server does not meet one of the preconditions that you put on "
59
+ "the request header fields."
60
+ ),
61
+ },
62
+ 413: {
63
+ "title": "Payload Too Large",
64
+ "description": "The request entity is larger than limits defined by the server.",
65
+ },
66
+ 414: {
67
+ "title": "URI Too Long",
68
+ "description": "The URI requested by you is longer than the server is willing to interpret.",
69
+ },
70
+ 415: {
71
+ "title": "Unsupported Media Type",
72
+ "description": "The media format of the requested data is not supported by the server.",
73
+ },
74
+ 416: {
75
+ "title": "Range Not Satisfiable",
76
+ "description": "The range specified by the Range header field in your request can't be fulfilled.",
77
+ },
78
+ 417: {
79
+ "title": "Expectation Failed",
80
+ "description": (
81
+ "The expectation given in your request's Expect header field could not be met by at "
82
+ "least one of the inbound servers."
83
+ ),
84
+ },
85
+ 418: {
86
+ "title": "I'm a teapot",
87
+ "description": "The web server rejects the attempt to make coffee with a teapot.",
88
+ },
89
+ 422: {
90
+ "title": "Unprocessable Entity",
91
+ "description": "The request was well-formed but was unable to be followed due to semantic errors.",
92
+ },
93
+ 423: {
94
+ "title": "Locked",
95
+ "description": "The resource that is being accessed is locked.",
96
+ },
97
+ 424: {
98
+ "title": "Failed Dependency",
99
+ "description": "The request failed due to failure of a previous request.",
100
+ },
101
+ 428: {
102
+ "title": "Precondition Required",
103
+ "description": "The origin server requires your request to be conditional.",
104
+ },
105
+ 429: {
106
+ "title": "Too Many Requests",
107
+ "description": "You have sent too many requests in a given amount of time.",
108
+ },
109
+ 431: {
110
+ "title": "Request Header Fields Too Large",
111
+ "description": (
112
+ "The server is unwilling to process your request because its header "
113
+ "fields are too large."
114
+ ),
115
+ },
116
+ 451: {
117
+ "title": "Unavailable For Legal Reasons",
118
+ "description": "The server is denying access to the resource as a consequence of a legal demand.",
119
+ },
120
+ 500: {
121
+ "title": "Internal Server Error",
122
+ "description": "The server has encountered a situation it doesn't know how to handle.",
123
+ },
124
+ 501: {
125
+ "title": "Not Implemented",
126
+ "description": "The request method is not supported by the server and cannot be handled.",
127
+ },
128
+ 502: {
129
+ "title": "Bad Gateway",
130
+ "description": (
131
+ "The server, while acting as a gateway or proxy, received an invalid response from "
132
+ "the upstream server."
133
+ ),
134
+ },
135
+ 503: {
136
+ "title": "Service Unavailable",
137
+ "description": "The server is not ready to handle the request.",
138
+ },
139
+ 504: {
140
+ "title": "Gateway Timeout",
141
+ "description": (
142
+ "The server is acting as a gateway or proxy and did not receive a timely response "
143
+ "from the upstream server."
144
+ ),
145
+ },
146
+ 505: {
147
+ "title": "HTTP Version Not Supported",
148
+ "description": "The HTTP version used in your request is not supported by the server.",
149
+ },
150
+ }
151
+
152
+
153
+ class ErrorHandler:
154
+ def __init__(self, app: Flask, errors: Optional[list[int]] = None):
155
+ self.app = app
156
+
157
+ for error_code in errors or ERROR_CODES:
158
+ self.app.register_error_handler(error_code, self.handle_error)
159
+
160
+ def handle_error(self, error: Exception) -> tuple:
161
+ """Render exception page with appropriate error information."""
162
+ code = getattr(error, "code", type(error).__name__)
163
+ info = ERROR_CODES.get(code, {})
164
+ title = f"{code} | {info.get('title', 'Error')}"
165
+ message = (
166
+ info.get("description")
167
+ or str(error).split(" ", 1)[-1].strip()
168
+ or "An error occurred"
169
+ )
170
+
171
+ return render_template("exception.html").replace(
172
+ "EXCEPTION_TITLE", title
173
+ ).replace("EXCEPTION_CODE", str(code)).replace(
174
+ "EXCEPTION_MESSAGE", message
175
+ ), getattr(
176
+ error, "code", 500
177
+ )
@@ -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, 4),
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, 2),
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, app=None, challenge_type: str = "one_click", captcha_dataset: str = "ai_dogs"
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.captcha_dataset = captcha_dataset
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.captcha_dataset,
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(self, action: str = "challenge"):
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
- if self.is_bot:
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.captcha_dataset,
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
- hardness = random.randint(
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=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(distorted))
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=True,
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=True,
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, image_dataset: str = "animals", audio_dataset: Optional[str] = None
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 = "animals",
823
- audio_dataset: str = "characters",
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:
@@ -198,7 +198,9 @@
198
198
  href="{{ url_for('humanify.challenge', return_url=return_url) }}"
199
199
  >
200
200
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
201
- <path d="M96 416q-14 0-23-9t-9-23V128q0-14 9-23t23-9h320q14 0 23 9t9 23v256q0 14-9 23t-23 9zm88-176q20 0 34-14t14-34-14-34-34-14-34 14-14 34 14 34 34 14m216 128v-64l-64-64-96 96-56-57-88 89z"/>
201
+ <path
202
+ d="M96 416q-14 0-23-9t-9-23V128q0-14 9-23t23-9h320q14 0 23 9t9 23v256q0 14-9 23t-23 9zm88-176q20 0 34-14t14-34-14-34-34-14-34 14-14 34 14 34 34 14m216 128v-64l-64-64-96 96-56-57-88 89z"
203
+ />
202
204
  </svg>
203
205
  Image challenge
204
206
  </a>
@@ -0,0 +1,66 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>EXCEPTION_TITLE</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, sans-serif;
10
+ background: #f2f2f2;
11
+ color: #181818;
12
+ margin: 0;
13
+ line-height: 1.5;
14
+ text-align: center;
15
+ display: grid;
16
+ place-items: center;
17
+ height: 100vh;
18
+ padding: 0 20px;
19
+ }
20
+
21
+ @media (prefers-color-scheme: dark) {
22
+ body {
23
+ background: #121212;
24
+ color: #f2f2f2;
25
+ }
26
+
27
+ .btn {
28
+ background: #f2f2f2;
29
+ color: #121212;
30
+ }
31
+ }
32
+
33
+ .content {
34
+ max-width: 600px;
35
+ }
36
+
37
+ h1 {
38
+ font-size: 64px;
39
+ margin: 15px 0;
40
+ }
41
+
42
+ p {
43
+ font-size: 20px;
44
+ margin: 15px 0;
45
+ opacity: 0.8;
46
+ }
47
+
48
+ .btn {
49
+ display: inline-block;
50
+ padding: 12px 24px;
51
+ background: #181818;
52
+ color: #f2f2f2;
53
+ border-radius: 6px;
54
+ text-decoration: none;
55
+ margin-top: 20px;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="content">
61
+ <h1>EXCEPTION_CODE</h1>
62
+ <p>EXCEPTION_MESSAGE</p>
63
+ <a href="/" class="btn">Back to home</a>
64
+ </div>
65
+ </body>
66
+ </html>
@@ -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
- 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))
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
- for (x, y), color in zip(dot_coords, colors):
268
- img[y, x] = color
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
- 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))
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
- for (start, end), color in zip(zip(start_coords, end_coords), colors):
276
- cv2.line(img, tuple(start), tuple(end), color.tolist(), 1)
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
- 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))
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] * (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
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), hardness * 0.1)
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 _:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flask-Humanify"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Protect against bots and DDoS attacks"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,62 +0,0 @@
1
- <h1 align="center">flask-Humanify</h1>
2
- <p align="center">A strong bot protection system for Flask with many features: rate limiting, special rules for users, web crawler detection, and automatic bot detection.</p>
3
- <p align="center"><a rel="noreferrer noopener" href="https://github.com/tn3w/flask-Humanify"><img alt="Github" src="https://img.shields.io/badge/Github-141e24.svg?&style=for-the-badge&logo=github&logoColor=white"></a> <a rel="noreferrer noopener" href="https://pypi.org/project/flask-Humanify/"><img alt="PyPI" src="https://img.shields.io/badge/PyPi-141e24.svg?&style=for-the-badge&logo=python&logoColor=white"></a> <a rel="noreferrer noopener" href="https://libraries.io/pypi/flask-Humanify"><img alt="Libraries.io" src="https://img.shields.io/badge/Libraries.io-141e24.svg?&style=for-the-badge&logo=npm&logoColor=white"></a></p>
4
-
5
- <br>
6
-
7
- ```python
8
- from flask import Flask
9
- from flask_Humanify import Humanify
10
-
11
- app = Flask(__name__)
12
- humanify = Humanify(app, challenge_type="one_click", captcha_dataset="ai_dogs")
13
-
14
- # Register the middleware to deny access to bots
15
- humanify.register_middleware(action="challenge")
16
-
17
- @app.route("/")
18
- def index():
19
- """
20
- A route that is protected against bots and DDoS attacks.
21
- """
22
- return "Hello, Human!"
23
-
24
- if __name__ == "__main__":
25
- app.run()
26
- ```
27
-
28
- Not using the middleware:
29
-
30
- ```python
31
- @app.route("/")
32
- def index():
33
- """
34
- A route that is protected against bots and DDoS attacks.
35
- """
36
- if humanify.is_bot:
37
- return humanify.challenge()
38
- return "Hello, Human!"
39
- ```
40
-
41
- ## Usage
42
-
43
- ### Installation
44
-
45
- Install the package with pip:
46
-
47
- ```bash
48
- pip install flask-humanify --upgrade
49
- ```
50
-
51
- Import the extension:
52
-
53
- ```python
54
- from flask_humanify import Humanify
55
- ```
56
-
57
- Add the extension to your Flask app:
58
-
59
- ```python
60
- app = Flask(__name__)
61
- humanify = Humanify(app)
62
- ```
File without changes
File without changes
File without changes