Проблема: bytes literal с Unicode символами (🔐, ╔, ║ и т.д.)
Решение:
- HTML response через .encode('utf-8') вместо bytes literal
- ASCII box drawing вместо Unicode символов
- Убраны emoji из print statements
Тест:
python3 -m py_compile web-client/start_server.py
164 lines
5.2 KiB
Python
Executable File
164 lines
5.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Simple HTTP server for WebSocket client testing with password protection.
|
|
|
|
Usage:
|
|
python3 start_web_server.py [--port PORT] [--password PASSWORD]
|
|
|
|
Open in browser:
|
|
http://localhost:PORT/client.html
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import http.server
|
|
import socketserver
|
|
import os
|
|
import sys
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
PORT = 8080
|
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
|
|
# Default password
|
|
DEFAULT_PASSWORD = "admin"
|
|
|
|
|
|
class AuthHandler(http.server.SimpleHTTPRequestHandler):
|
|
"""HTTP handler with Basic Auth."""
|
|
|
|
def __init__(self, *args, password="admin", **kwargs):
|
|
self.password = password
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def check_auth(self):
|
|
"""Check Basic Auth credentials."""
|
|
auth_header = self.headers.get('Authorization')
|
|
|
|
if not auth_header:
|
|
return False
|
|
|
|
try:
|
|
# Decode "Basic base64(username:password)"
|
|
auth_type, auth_string = auth_header.split(' ', 1)
|
|
if auth_type.lower() != 'basic':
|
|
return False
|
|
|
|
decoded = base64.b64decode(auth_string).decode('utf-8')
|
|
username, password = decoded.split(':', 1)
|
|
|
|
# For now, we only check password (username is ignored)
|
|
return password == self.password
|
|
except Exception:
|
|
return False
|
|
|
|
def send_auth_request(self):
|
|
"""Send 401 Unauthorized response."""
|
|
self.send_response(401)
|
|
self.send_header('WWW-Authenticate', 'Basic realm="Wyoming Satellite"')
|
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
self.end_headers()
|
|
|
|
html = '''
|
|
<html>
|
|
<head><title>401 Unauthorized</title></head>
|
|
<body>
|
|
<h1>Wyoming Satellite - Authentication Required</h1>
|
|
<p>Please enter the password to access the Web UI.</p>
|
|
</body>
|
|
</html>
|
|
'''
|
|
self.wfile.write(html.encode('utf-8'))
|
|
|
|
def do_GET(self):
|
|
"""Handle GET requests with authentication."""
|
|
# Skip auth for static files that are already protected
|
|
if not self.check_auth():
|
|
self.send_auth_request()
|
|
return
|
|
|
|
super().do_GET()
|
|
print(f"[WebServer] ✅ {self.requestline} → authenticated")
|
|
|
|
def log_message(self, format, *args):
|
|
# Suppress default logging
|
|
pass
|
|
|
|
|
|
def create_auth_handler(password):
|
|
"""Create auth handler with password."""
|
|
class Handler(AuthHandler):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, password=password, **kwargs)
|
|
return Handler
|
|
|
|
|
|
class ReusableTCPServer(socketserver.TCPServer):
|
|
"""TCP Server with address reuse enabled."""
|
|
allow_reuse_address = True
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='WebSocket Client Web Server with Password Protection')
|
|
parser.add_argument('--port', type=int, default=PORT, help=f'Port (default: {PORT})')
|
|
parser.add_argument('--host', default='0.0.0.0', help='Host (default: 0.0.0.0)')
|
|
parser.add_argument('--password', default=DEFAULT_PASSWORD, help=f'Password (default: {DEFAULT_PASSWORD})')
|
|
args = parser.parse_args()
|
|
|
|
# Change to script directory
|
|
os.chdir(SCRIPT_DIR)
|
|
|
|
# Create handler with password
|
|
Handler = create_auth_handler(args.password)
|
|
|
|
print("""
|
|
+==========================================================+
|
|
| Wyoming Satellite WebSocket Client Server |
|
|
+==========================================================+
|
|
| Serving: {:<53} |
|
|
| URL: http://localhost:{:<29} |
|
|
| Remote: http://<your-ip>:{:<25} |
|
|
| Password: {:<48} |
|
|
| |
|
|
| Press Ctrl+C to stop |
|
|
+==========================================================+
|
|
""".format(
|
|
str(SCRIPT_DIR),
|
|
args.port,
|
|
args.port,
|
|
'*' * len(args.password)
|
|
))
|
|
|
|
try:
|
|
with ReusableTCPServer((args.host, args.port), Handler) as httpd:
|
|
print(f"Server started on http://{args.host}:{args.port}")
|
|
print("Serving files from:", SCRIPT_DIR)
|
|
print(f"Password protected ({len(args.password)} characters)")
|
|
print("\nPress Ctrl+C to stop\n")
|
|
|
|
try:
|
|
httpd.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down gracefully...")
|
|
# Shutdown in a separate thread to avoid deadlock
|
|
shutdown_thread = threading.Thread(target=httpd.shutdown)
|
|
shutdown_thread.start()
|
|
shutdown_thread.join(timeout=2)
|
|
httpd.server_close()
|
|
print("Server stopped successfully")
|
|
|
|
except OSError as e:
|
|
print(f"\nError: Could not bind to port {args.port}")
|
|
print(f" {e}")
|
|
print("\nTry using a different port:")
|
|
print(f" python3 {sys.argv[0]} --port 8081\n")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"\nUnexpected error: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|