flask-Humanify 0.1.0__py3-none-any.whl → 0.1.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.
@@ -0,0 +1,77 @@
1
+ import hashlib
2
+ import time
3
+ from collections import defaultdict, deque
4
+
5
+ from flask import request, redirect, url_for, render_template
6
+ from flask_humanify.utils import get_client_ip, get_return_url
7
+
8
+
9
+ class RateLimiter:
10
+ """
11
+ Rate limiter.
12
+ """
13
+
14
+ def __init__(self, app=None, max_requests: int = 2, time_window: int = 10):
15
+ """
16
+ Initialize the rate limiter.
17
+ """
18
+ self.app = app
19
+ if app is not None:
20
+ self.init_app(app)
21
+ self.max_requests = max_requests
22
+ self.time_window = time_window
23
+ self.ip_request_times = defaultdict(deque)
24
+
25
+ def init_app(self, app):
26
+ """
27
+ Initialize the rate limiter.
28
+ """
29
+ self.app = app
30
+ self.app.before_request(self.before_request)
31
+
32
+ @self.app.route(
33
+ "/humanify/rate_limited",
34
+ methods=["GET"],
35
+ endpoint="humanify.rate_limited",
36
+ )
37
+ def rate_limited():
38
+ """
39
+ Rate limited route.
40
+ """
41
+ return (
42
+ render_template("rate_limited.html").replace(
43
+ "RETURN_URL", get_return_url(request)
44
+ ),
45
+ 429,
46
+ {"Cache-Control": "public, max-age=15552000"},
47
+ )
48
+
49
+ def before_request(self):
50
+ """
51
+ Before request hook.
52
+ """
53
+ ip = get_client_ip(request)
54
+ if request.endpoint == "humanify.rate_limited":
55
+ return
56
+ if self.is_rate_limited(ip or "127.0.0.1"):
57
+ return redirect(
58
+ url_for("humanify.rate_limited", return_url=request.full_path)
59
+ )
60
+
61
+ def is_rate_limited(self, ip: str) -> bool:
62
+ """
63
+ Check if the IP is rate limited.
64
+ """
65
+ hashed_ip = hashlib.sha256(ip.encode()).hexdigest()
66
+
67
+ current_time = time.time()
68
+ request_times = self.ip_request_times[hashed_ip]
69
+
70
+ while request_times and request_times[0] <= current_time - self.time_window:
71
+ request_times.popleft()
72
+
73
+ if len(request_times) < self.max_requests:
74
+ request_times.append(current_time)
75
+ return False
76
+
77
+ return True
@@ -4,7 +4,10 @@ import socket
4
4
  import time
5
5
  import threading
6
6
  import os
7
+ import importlib.metadata
8
+ import importlib.resources
7
9
  import urllib.request
10
+ from pathlib import Path
8
11
  from typing import Dict, List, Optional, Tuple
9
12
  from datetime import datetime, timedelta
10
13
  from netaddr import IPNetwork, IPAddress
@@ -13,20 +16,39 @@ from netaddr import IPNetwork, IPAddress
13
16
  logger = logging.getLogger(__name__)
14
17
 
15
18
 
19
+ try:
20
+ importlib.metadata.distribution("flask-humanify")
21
+ BASE_DIR = importlib.resources.files("flask_humanify")
22
+ except importlib.metadata.PackageNotFoundError:
23
+ BASE_DIR = Path(__file__).parent
24
+
25
+ DATASET_DIR = BASE_DIR / "datasets"
26
+ if not DATASET_DIR.exists():
27
+ DATASET_DIR.mkdir(parents=True)
28
+
29
+ IPSET_DATA_PATH = str(DATASET_DIR / "ipset.json")
30
+
31
+
16
32
  class IPSetMemoryServer:
17
33
  """A singleton memory server that manages IP sets and provides lookup functionality."""
18
34
 
19
35
  _instance = None
20
36
  _lock = threading.Lock()
21
37
 
22
- def __new__(cls, port: int = 9876, data_path: str = "ipset.json"):
38
+ def __new__(cls, port: int = 9876, data_path: Optional[str] = None):
39
+ if data_path is None:
40
+ data_path = IPSET_DATA_PATH
41
+
23
42
  with cls._lock:
24
43
  if cls._instance is None:
25
44
  cls._instance = super(IPSetMemoryServer, cls).__new__(cls)
26
45
  cls._instance.initialized = False
27
46
  return cls._instance
28
47
 
29
- def __init__(self, port: int = 9876, data_path: str = "ipset.json"):
48
+ def __init__(self, port: int = 9876, data_path: Optional[str] = None):
49
+ if data_path is None:
50
+ data_path = IPSET_DATA_PATH
51
+
30
52
  if getattr(self, "initialized", False):
31
53
  return
32
54
 
@@ -281,8 +303,11 @@ class IPSetClient:
281
303
  self.socket = None
282
304
 
283
305
 
284
- def ensure_server_running(port: int = 9876, data_path: str = "ipset.json") -> None:
306
+ def ensure_server_running(port: int = 9876, data_path: Optional[str] = None) -> None:
285
307
  """Ensure that the memory server is running."""
308
+ if data_path is None:
309
+ data_path = IPSET_DATA_PATH
310
+
286
311
  server = IPSetMemoryServer(port=port, data_path=data_path)
287
312
  server.start()
288
313
 
@@ -0,0 +1,110 @@
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>Access Denied</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
+ .fill {
33
+ background: rgba(0,0,0,0.15);
34
+ }
35
+ }
36
+
37
+ .content {
38
+ max-width: 600px;
39
+ }
40
+
41
+ .emoji {
42
+ font-size: 48px;
43
+ margin-bottom: 10px;
44
+ }
45
+
46
+ h1 {
47
+ font-size: 22px;
48
+ margin: 15px 0;
49
+ }
50
+
51
+ p {
52
+ margin: 15px 0;
53
+ opacity: 0.8;
54
+ }
55
+
56
+ .btn {
57
+ display: inline-block;
58
+ padding: 12px 24px;
59
+ background: #181818;
60
+ color: #f2f2f2;
61
+ border-radius: 6px;
62
+ text-decoration: none;
63
+ margin-top: 20px;
64
+ position: relative;
65
+ overflow: hidden;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .fill {
70
+ position: absolute;
71
+ left: 0;
72
+ top: 0;
73
+ height: 100%;
74
+ width: 0;
75
+ background: rgba(255,255,255,0.15);
76
+ animation: fillBtn 10s linear forwards;
77
+ }
78
+
79
+ @keyframes fillBtn {
80
+ to { width: 100%; }
81
+ }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <div class="content">
86
+ <div class="emoji">
87
+ 🚨
88
+ </div>
89
+ <h1>Sorry, but you can't see the requested page.</h1>
90
+ <p>Automated scripts are likely attempting to request this page. If you use anonymizing tools like VPNs or proxies, consider disabling them temporarily.</p>
91
+ <a href="RETURN_URL" id="retry" class="btn">
92
+ <div class="fill"></div>
93
+ <span>Try again</span>
94
+ </a>
95
+ </div>
96
+
97
+ <noscript>
98
+ <style>
99
+ .btn { pointer-events: all !important; }
100
+ .fill { display: none; }
101
+ </style>
102
+ </noscript>
103
+
104
+ <script>
105
+ setTimeout(function() {
106
+ document.getElementById('retry').style.pointerEvents = 'all';
107
+ }, 10000);
108
+ </script>
109
+ </body>
110
+ </html>
@@ -0,0 +1,192 @@
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>Verify Human</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
+ .fill {
28
+ background: rgba(0,0,0,0.15);
29
+ }
30
+ }
31
+
32
+ .content {
33
+ max-width: 600px;
34
+ display: flex;
35
+ flex-direction: column;
36
+ align-items: center;
37
+ }
38
+
39
+ .emoji {
40
+ font-size: 48px;
41
+ margin-bottom: 10px;
42
+ }
43
+
44
+ h1 {
45
+ font-size: 22px;
46
+ margin: 15px 0;
47
+ }
48
+
49
+ p {
50
+ margin: 15px 0;
51
+ opacity: 0.8;
52
+ }
53
+
54
+ .preview-container {
55
+ width: 200px;
56
+ height: 200px;
57
+ margin-bottom: 20px;
58
+ }
59
+
60
+ .preview-container img {
61
+ width: 100%;
62
+ height: 100%;
63
+ object-fit: cover;
64
+ border-radius: 8px;
65
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
66
+ }
67
+
68
+ .text-container {
69
+ text-align: center;
70
+ margin-bottom: 25px;
71
+ }
72
+
73
+ .error {
74
+ color: #e53935;
75
+ margin: 10px 0;
76
+ font-size: 16px;
77
+ }
78
+
79
+ .images-row {
80
+ display: flex;
81
+ flex-direction: row;
82
+ justify-content: center;
83
+ flex-wrap: wrap;
84
+ gap: 15px;
85
+ margin-bottom: 25px;
86
+ width: 100%;
87
+ }
88
+
89
+ .image-button {
90
+ background: none;
91
+ border: none;
92
+ padding: 0;
93
+ cursor: pointer;
94
+ width: 100px;
95
+ height: 100px;
96
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
97
+ border-radius: 8px;
98
+ overflow: hidden;
99
+ position: relative;
100
+ }
101
+
102
+ .image-button:hover {
103
+ transform: translateY(-3px);
104
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
105
+ }
106
+
107
+ .image-button img {
108
+ width: 100%;
109
+ height: 100%;
110
+ object-fit: cover;
111
+ }
112
+
113
+ .audio-challenge-link {
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ margin-top: 15px;
118
+ color: #4a6ed0;
119
+ text-decoration: none;
120
+ font-size: 14px;
121
+ transition: all 0.2s ease;
122
+ }
123
+
124
+ .audio-challenge-link:hover {
125
+ transform: translateY(-2px);
126
+ }
127
+
128
+ .audio-challenge-link svg {
129
+ margin-right: 6px;
130
+ width: 16px;
131
+ height: 16px;
132
+ fill: currentColor;
133
+ }
134
+
135
+ @media (max-width: 600px) {
136
+ .preview-container {
137
+ width: 180px;
138
+ height: 180px;
139
+ }
140
+
141
+ .images-row {
142
+ gap: 10px;
143
+ justify-content: center;
144
+ }
145
+
146
+ .image-button {
147
+ width: 80px;
148
+ height: 80px;
149
+ }
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <div class="content">
155
+ <div class="preview-container">
156
+ <img src="{{ preview_image }}" alt="Reference image">
157
+ </div>
158
+
159
+ <div class="text-container">
160
+ {% if subject == "smiling dog" %}
161
+ <p>To verify you're not a bot, select the dog that smiles like shown above.</p>
162
+ {% else %}
163
+ <p>To verify you're not a bot, select the image that matches the motif shown above.</p>
164
+ {% endif %}
165
+ {% if error %}
166
+ <p class="error">{{ error }}</p>
167
+ {% endif %}
168
+ </div>
169
+
170
+ <div class="images-row">
171
+ {% for image in images %}
172
+ <form action="{{ url_for('captchaify.verify') }}" method="POST">
173
+ <input type="hidden" name="return_url" value="{{ return_url }}">
174
+ <input type="hidden" name="captcha_data" value="{{ captcha_data }}">
175
+ <button type="submit" class="image-button" name="{{ loop.index }}" value="1">
176
+ <img src="{{ image }}" alt="Selection image {{ loop.index }}">
177
+ </button>
178
+ </form>
179
+ {% endfor %}
180
+ </div>
181
+
182
+ {% if audio_challenge_available %}
183
+ <a class="audio-challenge-link" href="{{ url_for('captchaify.audio_challenge', return_url=return_url) }}">
184
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
185
+ <path d="M10 1a1 1 0 0 1 1 1v13.59l1.29-1.29a1 1 0 1 1 1.42 1.42l-3 3a1 1 0 0 1-1.42 0l-3-3a1 1 0 0 1 1.42-1.42L9 15.59V2a1 1 0 0 1 1-1zm2-1a1 1 0 1 1 0 2 5 5 0 0 0-5 5 1 1 0 1 1-2 0 7 7 0 0 1 7-7zm2 3a1 1 0 1 1 0 2 3 3 0 0 0-3 3 1 1 0 1 1-2 0 5 5 0 0 1 5-5z" />
186
+ </svg>
187
+ Audio challenge
188
+ </a>
189
+ {% endif %}
190
+ </div>
191
+ </body>
192
+ </html>
@@ -0,0 +1,110 @@
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>Rate Limited</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
+ .fill {
33
+ background: rgba(0,0,0,0.15);
34
+ }
35
+ }
36
+
37
+ .content {
38
+ max-width: 600px;
39
+ }
40
+
41
+ .emoji {
42
+ font-size: 48px;
43
+ margin-bottom: 10px;
44
+ }
45
+
46
+ h1 {
47
+ font-size: 22px;
48
+ margin: 15px 0;
49
+ }
50
+
51
+ p {
52
+ margin: 15px 0;
53
+ opacity: 0.8;
54
+ }
55
+
56
+ .btn {
57
+ display: inline-block;
58
+ padding: 12px 24px;
59
+ background: #181818;
60
+ color: #f2f2f2;
61
+ border-radius: 6px;
62
+ text-decoration: none;
63
+ margin-top: 20px;
64
+ position: relative;
65
+ overflow: hidden;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .fill {
70
+ position: absolute;
71
+ left: 0;
72
+ top: 0;
73
+ height: 100%;
74
+ width: 0;
75
+ background: rgba(255,255,255,0.15);
76
+ animation: fillBtn 10s linear forwards;
77
+ }
78
+
79
+ @keyframes fillBtn {
80
+ to { width: 100%; }
81
+ }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <div class="content">
86
+ <div class="emoji">
87
+ 🫖
88
+ </div>
89
+ <h1>Have some Tea</h1>
90
+ <p>It appears that you are sending an excessive number of requests to this website. Please reduce the frequency of your requests.</p>
91
+ <a href="RETURN_URL" id="retry" class="btn">
92
+ <div class="fill"></div>
93
+ <span>Try again</span>
94
+ </a>
95
+ </div>
96
+
97
+ <noscript>
98
+ <style>
99
+ .btn { pointer-events: all !important; }
100
+ .fill { display: none; }
101
+ </style>
102
+ </noscript>
103
+
104
+ <script>
105
+ setTimeout(function() {
106
+ document.getElementById('retry').style.pointerEvents = 'all';
107
+ }, 10000);
108
+ </script>
109
+ </body>
110
+ </html>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask-Humanify
3
- Version: 0.1.0
3
+ Version: 0.1.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
@@ -55,15 +55,17 @@ if __name__ == "__main__":
55
55
  app.run()
56
56
  ```
57
57
 
58
+ ## Usage
59
+
58
60
  ### Installation
59
61
  Install the package with pip:
60
62
  ```bash
61
- pip install flask-Humanify
63
+ pip install flask-humanify --upgrade
62
64
  ```
63
65
 
64
66
  Import the extension:
65
67
  ```python
66
- from flask_Humanify import Humanify
68
+ from flask_humanify import Humanify
67
69
  ```
68
70
 
69
71
  Add the extension to your Flask app:
@@ -0,0 +1,14 @@
1
+ flask_humanify/__init__.py,sha256=L67JR_FDgC9iNnBrnRchNPZe_z7tXbwV_xJ4NEQKGFw,269
2
+ flask_humanify/humanify.py,sha256=JJeTcJlMte6zj2QRlat2876iJzFg40YNoMEX5kWepiU,3772
3
+ flask_humanify/ipset.py,sha256=ZZZRtgtddkZr2q1zuI6803hJQ8OodVHNdyZvZGqpmMI,10866
4
+ flask_humanify/utils.py,sha256=CJ4FPhNJ75FuWcAn_ZuZkqRa9HR3jVvYOu9NZaN4L1o,2461
5
+ flask_humanify/datasets/ipset.json,sha256=YNPqwI109lYkfvZeOPsoDH_dKJxOCs0G2nvx_s2mvqU,30601191
6
+ flask_humanify/features/rate_limiter.py,sha256=kl3l1SHtDOPzOb1TLZizIRp-mJo5d30uW62ocmBm_fM,2175
7
+ flask_humanify/templates/access_denied.html,sha256=Y7EzM53LRBMJBniczTWYvinHuPPFQVKrqvCmKaqLHak,3165
8
+ flask_humanify/templates/oneclick_captcha.html,sha256=CnK4qTOdjIrlnFB8lytuK8pEcpj1dL5uYzj0ZNBkx-E,6241
9
+ flask_humanify/templates/rate_limited.html,sha256=LSS1-c9F3YLxXF-jiyyNN6vIMoIAdmd6ObO1PDcjIxI,3110
10
+ flask_humanify-0.1.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
11
+ flask_humanify-0.1.2.dist-info/METADATA,sha256=9oxqmwHRjGz9RVxSwxuZWdXcjYdPtaC2j17rOkOWVyE,2787
12
+ flask_humanify-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ flask_humanify-0.1.2.dist-info/top_level.txt,sha256=9-c6uhxwCpPE3BJYge1Y9Z_bYmWitI0fY5RgqMiFWr0,15
14
+ flask_humanify-0.1.2.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ flask_humanify
@@ -1,9 +0,0 @@
1
- flask_Humanify/__init__.py,sha256=me3NQI32tVcx5iTq5hpCJ4JOG4wdlj9x8Lnx-Y2Rhvk,269
2
- flask_Humanify/humanify.py,sha256=JJeTcJlMte6zj2QRlat2876iJzFg40YNoMEX5kWepiU,3772
3
- flask_Humanify/ipset.py,sha256=LK2OrqwccSrikofGrdSEzgOx17i-HsPYsR9gHJyPDhQ,10219
4
- flask_Humanify/utils.py,sha256=CJ4FPhNJ75FuWcAn_ZuZkqRa9HR3jVvYOu9NZaN4L1o,2461
5
- flask_humanify-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
6
- flask_humanify-0.1.0.dist-info/METADATA,sha256=oIDLoHKKyylisbMBTwAaPVnwBVffeWzEZjoxpbUxNYU,2767
7
- flask_humanify-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- flask_humanify-0.1.0.dist-info/top_level.txt,sha256=XcJXzmBdGt7o2wY93Yhc0Epzeb2nORaZ8-fLsbwOYQ8,15
9
- flask_humanify-0.1.0.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- flask_Humanify
File without changes
File without changes