55import base64
66import hashlib
77import hmac
8+ import ipaddress
89import os
910import secrets
1011import ssl
1112import sys
1213import tempfile
1314import time
14- from datetime import datetime , timedelta
15+ import threading
16+ import socket
17+ from datetime import datetime , timedelta , timezone
1518from http .server import HTTPServer , SimpleHTTPRequestHandler
1619from urllib .parse import parse_qs
1720
@@ -110,7 +113,7 @@ def verify_password(password: str, stored: str) -> bool:
110113 <br><br>
111114 __ERROR__
112115 <br>
113- <button type="submit">Anmelden </button>
116+ <button type="submit">Login </button>
114117 </form>
115118</body>
116119</html>
@@ -161,14 +164,31 @@ def do_POST(self):
161164 pw = (parse_qs (body ).get ("password" ) or ["" ])[0 ]
162165
163166 if not verify_password (pw , self .pw_hash ):
164- return self ._send_login ("Falsches Passwort." )
167+ # Track failed attempts by IP address
168+ client_ip = self .client_address [0 ]
169+ if not hasattr (LoginHandler , '_failed_attempts' ):
170+ LoginHandler ._failed_attempts = {}
171+
172+ LoginHandler ._failed_attempts [client_ip ] = LoginHandler ._failed_attempts .get (client_ip , 0 ) + 1
173+
174+ if LoginHandler ._failed_attempts [client_ip ] >= 5 :
175+ self .send_error (403 , "Too many failed login attempts. Access blocked." )
176+ return
177+
178+ attempts_left = 5 - LoginHandler ._failed_attempts [client_ip ]
179+ return self ._send_login (f"Wrong password. { attempts_left } attempts left." )
180+ # Reset failed attempts on successful login
181+ client_ip = self .client_address [0 ]
182+ if hasattr (LoginHandler , '_failed_attempts' ) and client_ip in LoginHandler ._failed_attempts :
183+ del LoginHandler ._failed_attempts [client_ip ]
184+
165185
166186 cookie_val = make_cookie (self .secret )
167187 self .send_response (302 )
168188 self .send_header ("Location" , "/" )
169189 self .send_header (
170190 "Set-Cookie" ,
171- # Secure ist bei HTTPS korrekt , HttpOnly verhindert JS-Zugriff
191+ # Secure is correct for HTTPS , HttpOnly prevents JS access
172192 f"{ COOKIE_NAME } ={ cookie_val } ; Path=/; HttpOnly; SameSite=Strict; Secure"
173193 )
174194 self .end_headers ()
@@ -186,18 +206,44 @@ def generate_cert(cert_path: str, key_path: str, host: str) -> None:
186206 ])
187207
188208 san_entries = []
189- # DNSName ist ok für "localhost" etc. Für IPs ist eigentlich x509.IPAddress nötig.
190- # Damit es robust bleibt, legen wir DNSName immer an. (Browserwarnung bleibt sowieso self-signed.)
209+ # DNSName is fine for "localhost" etc. For IPs, x509.IPAddress is actually needed.
210+ # To keep it robust, we always create DNSName. (Browser warning remains anyway due to self-signed.)
211+ # Add the provided host
191212 san_entries .append (x509 .DNSName (host ))
213+
214+ # Add localhost and 127.0.0.1
215+ san_entries .append (x509 .DNSName ("localhost" ))
216+ try :
217+ san_entries .append (x509 .IPAddress (ipaddress .IPv4Address ("127.0.0.1" )))
218+ except Exception :
219+ pass
220+
221+ # Add network IP addresses
222+ try :
223+ hostname = socket .gethostname ()
224+ for info in socket .getaddrinfo (hostname , None ):
225+ ip = info [4 ][0 ]
226+ try :
227+ # Try IPv4
228+ san_entries .append (x509 .IPAddress (ipaddress .IPv4Address (ip )))
229+ except Exception :
230+ try :
231+ # Try IPv6
232+ san_entries .append (x509 .IPAddress (ipaddress .IPv6Address (ip )))
233+ except Exception :
234+ pass
235+ except Exception :
236+ pass
237+
192238
193239 cert = (
194240 x509 .CertificateBuilder ()
195241 .subject_name (subject )
196242 .issuer_name (issuer )
197243 .public_key (key .public_key ())
198244 .serial_number (x509 .random_serial_number ())
199- .not_valid_before (datetime .utcnow ( ) - timedelta (minutes = 1 ))
200- .not_valid_after (datetime .utcnow ( ) + timedelta (days = 1 ))
245+ .not_valid_before (datetime .now ( timezone . utc ) - timedelta (minutes = 1 ))
246+ .not_valid_after (datetime .now ( timezone . utc ) + timedelta (days = 1 ))
201247 .add_extension (x509 .SubjectAlternativeName (san_entries ), critical = False )
202248 .sign (key , hashes .SHA256 ())
203249 )
@@ -289,8 +335,6 @@ def handler_factory(*h_args, **h_kwargs):
289335 print ("self-signed cert => Browser warning is normal." )
290336 print ("Press Ctrl+C to stop." )
291337
292- import threading
293-
294338 server_thread = threading .Thread (target = httpd .serve_forever , daemon = True )
295339 server_thread .start ()
296340
0 commit comments