flask-Humanify 0.2.1__py3-none-any.whl → 0.2.2__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 +3 -2
- flask_humanify/features/error_handler.py +177 -0
- flask_humanify/templates/audio_challenge.html +3 -1
- flask_humanify/templates/exception.html +66 -0
- {flask_humanify-0.2.1.dist-info → flask_humanify-0.2.2.dist-info}/METADATA +78 -1
- {flask_humanify-0.2.1.dist-info → flask_humanify-0.2.2.dist-info}/RECORD +9 -7
- {flask_humanify-0.2.1.dist-info → flask_humanify-0.2.2.dist-info}/WHEEL +0 -0
- {flask_humanify-0.2.1.dist-info → flask_humanify-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {flask_humanify-0.2.1.dist-info → flask_humanify-0.2.2.dist-info}/top_level.txt +0 -0
flask_humanify/__init__.py
CHANGED
@@ -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.
|
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
|
+
)
|
@@ -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
|
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>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: flask-Humanify
|
3
|
-
Version: 0.2.
|
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
|
@@ -129,3 +129,80 @@ Add the extension to your Flask app:
|
|
129
129
|
app = Flask(__name__)
|
130
130
|
humanify = Humanify(app)
|
131
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
|
+
```
|
@@ -1,4 +1,4 @@
|
|
1
|
-
flask_humanify/__init__.py,sha256=
|
1
|
+
flask_humanify/__init__.py,sha256=rvTDStRFUl6sFNVgm7NKEA3fkH2gP7osDl3IuUy-SNU,334
|
2
2
|
flask_humanify/humanify.py,sha256=C-HhqWfvENMyOjU2LJ8aUW0npLm2LavuiTcvAf_LTCM,22179
|
3
3
|
flask_humanify/memory_server.py,sha256=NlGYrHuH6S7Ei7I5gc78aYvZvEq6bUz-lgLhuiCXTEM,30339
|
4
4
|
flask_humanify/utils.py,sha256=mG8oZL6U52yDWOlE3umqf5NNuwKB-LnhMjr_xdpQcAo,18581
|
@@ -7,14 +7,16 @@ flask_humanify/datasets/animals.pkl,sha256=zhOY-J3h18ZMBE5D9q_ujQc7ZW1WRNcuYfM4J
|
|
7
7
|
flask_humanify/datasets/characters.pkl,sha256=tEbGnbi5S9Y2Us9arVghdV43luqOAiGTQMLiU2bka6U,12756114
|
8
8
|
flask_humanify/datasets/ipset.json,sha256=YNPqwI109lYkfvZeOPsoDH_dKJxOCs0G2nvx_s2mvqU,30601191
|
9
9
|
flask_humanify/datasets/keys.pkl,sha256=sYzgJ5SP5SluWwyatFnWc-hj8rzwaOEOMVLJOw4VZik,32763786
|
10
|
+
flask_humanify/features/error_handler.py,sha256=bLOyaS6jtkLyxf7RviXD5fVrJ7fBAd9I8Zt2Q1m54n8,6123
|
10
11
|
flask_humanify/features/rate_limiter.py,sha256=QMIwfEllTDup6jxckEbE83PlJZeLw-0SvyxPqzXJYzU,2204
|
11
12
|
flask_humanify/templates/access_denied.html,sha256=p8ea_9gvv83aYFHaVKKedmQr6M8Z7NyHJK_OT3jdTOs,3169
|
12
|
-
flask_humanify/templates/audio_challenge.html,sha256=
|
13
|
+
flask_humanify/templates/audio_challenge.html,sha256=Tvs7mTah59n4I-w5N8ABcW8AMPN8zAAcFYZUXBm6NH0,6442
|
14
|
+
flask_humanify/templates/exception.html,sha256=OcAuyoBQ5wLMQvFAVi2LS7gcDsLmCpkuHCBQoh_eGTg,1711
|
13
15
|
flask_humanify/templates/grid_challenge.html,sha256=cToubxQvI7bJ-BKS9wceIvsAmp6h2EGYLhgyRr6pFxU,7179
|
14
16
|
flask_humanify/templates/one_click_challenge.html,sha256=21WOkYWrW48GXpVPBMPrEJ7Iq5JawkrXKUyUUfxWmTc,5996
|
15
17
|
flask_humanify/templates/rate_limited.html,sha256=Bv98MoetuSJGpWkDaQfhl7JwcWJiGaG2gwqxvSphaTM,3114
|
16
|
-
flask_humanify-0.2.
|
17
|
-
flask_humanify-0.2.
|
18
|
-
flask_humanify-0.2.
|
19
|
-
flask_humanify-0.2.
|
20
|
-
flask_humanify-0.2.
|
18
|
+
flask_humanify-0.2.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
19
|
+
flask_humanify-0.2.2.dist-info/METADATA,sha256=5Mrphy_GSkMiJVc7ZEdkiO2_-eVILQ1plCWlxI7bO0E,6092
|
20
|
+
flask_humanify-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
21
|
+
flask_humanify-0.2.2.dist-info/top_level.txt,sha256=9-c6uhxwCpPE3BJYge1Y9Z_bYmWitI0fY5RgqMiFWr0,15
|
22
|
+
flask_humanify-0.2.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|