8383__version__ = "0.6"
8484
8585__all__ = [
86- "HTTPServer" , "ThreadingHTTPServer" , "BaseHTTPRequestHandler" ,
87- "SimpleHTTPRequestHandler" , "CGIHTTPRequestHandler" ,
86+ "HTTPServer" , "ThreadingHTTPServer" ,
87+ "HTTPSServer" , "ThreadingHTTPSServer" ,
88+ "BaseHTTPRequestHandler" , "SimpleHTTPRequestHandler" ,
89+ "CGIHTTPRequestHandler" ,
8890]
8991
9092import copy
99101import posixpath
100102import select
101103import shutil
102- import socket # For gethostbyaddr()
104+ import socket
103105import socketserver
104106import sys
105107import time
114116<html lang="en">
115117 <head>
116118 <meta charset="utf-8">
119+ <style type="text/css">
120+ :root {
121+ color-scheme: light dark;
122+ }
123+ </style>
117124 <title>Error response</title>
118125 </head>
119126 <body>
133140
134141class HTTPServer (socketserver .TCPServer ):
135142
136- allow_reuse_address = 1 # Seems to make sense in testing environment
143+ allow_reuse_address = True # Seems to make sense in testing environment
144+ allow_reuse_port = False
137145
138146 def server_bind (self ):
139147 """Override server_bind to store the server name."""
@@ -147,6 +155,47 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
147155 daemon_threads = True
148156
149157
158+ class HTTPSServer (HTTPServer ):
159+ def __init__ (self , server_address , RequestHandlerClass ,
160+ bind_and_activate = True , * , certfile , keyfile = None ,
161+ password = None , alpn_protocols = None ):
162+ try :
163+ import ssl
164+ except ImportError :
165+ raise RuntimeError ("SSL module is missing; "
166+ "HTTPS support is unavailable" )
167+
168+ self .ssl = ssl
169+ self .certfile = certfile
170+ self .keyfile = keyfile
171+ self .password = password
172+ # Support by default HTTP/1.1
173+ self .alpn_protocols = (
174+ ["http/1.1" ] if alpn_protocols is None else alpn_protocols
175+ )
176+
177+ super ().__init__ (server_address ,
178+ RequestHandlerClass ,
179+ bind_and_activate )
180+
181+ def server_activate (self ):
182+ """Wrap the socket in SSLSocket."""
183+ super ().server_activate ()
184+ context = self ._create_context ()
185+ self .socket = context .wrap_socket (self .socket , server_side = True )
186+
187+ def _create_context (self ):
188+ """Create a secure SSL context."""
189+ context = self .ssl .create_default_context (self .ssl .Purpose .CLIENT_AUTH )
190+ context .load_cert_chain (self .certfile , self .keyfile , self .password )
191+ context .set_alpn_protocols (self .alpn_protocols )
192+ return context
193+
194+
195+ class ThreadingHTTPSServer (socketserver .ThreadingMixIn , HTTPSServer ):
196+ daemon_threads = True
197+
198+
150199class BaseHTTPRequestHandler (socketserver .StreamRequestHandler ):
151200
152201 """HTTP request handler base class.
@@ -817,6 +866,7 @@ def list_directory(self, path):
817866 r .append ('<html lang="en">' )
818867 r .append ('<head>' )
819868 r .append (f'<meta charset="{ enc } ">' )
869+ r .append ('<style type="text/css">\n :root {\n color-scheme: light dark;\n }\n </style>' )
820870 r .append (f'<title>{ title } </title>\n </head>' )
821871 r .append (f'<body>\n <h1>{ title } </h1>' )
822872 r .append ('<hr>\n <ul>' )
@@ -1281,20 +1331,29 @@ def _get_best_family(*address):
12811331
12821332def test (HandlerClass = BaseHTTPRequestHandler ,
12831333 ServerClass = ThreadingHTTPServer ,
1284- protocol = "HTTP/1.0" , port = 8000 , bind = None ):
1334+ protocol = "HTTP/1.0" , port = 8000 , bind = None ,
1335+ tls_cert = None , tls_key = None , tls_password = None ):
12851336 """Test the HTTP request handler class.
12861337
12871338 This runs an HTTP server on port 8000 (or the port argument).
12881339
12891340 """
12901341 ServerClass .address_family , addr = _get_best_family (bind , port )
12911342 HandlerClass .protocol_version = protocol
1292- with ServerClass (addr , HandlerClass ) as httpd :
1343+
1344+ if tls_cert :
1345+ server = ServerClass (addr , HandlerClass , certfile = tls_cert ,
1346+ keyfile = tls_key , password = tls_password )
1347+ else :
1348+ server = ServerClass (addr , HandlerClass )
1349+
1350+ with server as httpd :
12931351 host , port = httpd .socket .getsockname ()[:2 ]
12941352 url_host = f'[{ host } ]' if ':' in host else host
1353+ protocol = 'HTTPS' if tls_cert else 'HTTP'
12951354 print (
1296- f"Serving HTTP on { host } port { port } "
1297- f"(http ://{ url_host } :{ port } /) ..."
1355+ f"Serving { protocol } on { host } port { port } "
1356+ f"({ protocol . lower () } ://{ url_host } :{ port } /) ..."
12981357 )
12991358 try :
13001359 httpd .serve_forever ()
@@ -1306,7 +1365,7 @@ def test(HandlerClass=BaseHTTPRequestHandler,
13061365 import argparse
13071366 import contextlib
13081367
1309- parser = argparse .ArgumentParser ()
1368+ parser = argparse .ArgumentParser (color = True )
13101369 parser .add_argument ('--cgi' , action = 'store_true' ,
13111370 help = 'run as CGI server' )
13121371 parser .add_argument ('-b' , '--bind' , metavar = 'ADDRESS' ,
@@ -1319,17 +1378,38 @@ def test(HandlerClass=BaseHTTPRequestHandler,
13191378 default = 'HTTP/1.0' ,
13201379 help = 'conform to this HTTP version '
13211380 '(default: %(default)s)' )
1381+ parser .add_argument ('--tls-cert' , metavar = 'PATH' ,
1382+ help = 'path to the TLS certificate chain file' )
1383+ parser .add_argument ('--tls-key' , metavar = 'PATH' ,
1384+ help = 'path to the TLS key file' )
1385+ parser .add_argument ('--tls-password-file' , metavar = 'PATH' ,
1386+ help = 'path to the password file for the TLS key' )
13221387 parser .add_argument ('port' , default = 8000 , type = int , nargs = '?' ,
13231388 help = 'bind to this port '
13241389 '(default: %(default)s)' )
13251390 args = parser .parse_args ()
1391+
1392+ if not args .tls_cert and args .tls_key :
1393+ parser .error ("--tls-key requires --tls-cert to be set" )
1394+
1395+ tls_key_password = None
1396+ if args .tls_password_file :
1397+ if not args .tls_cert :
1398+ parser .error ("--tls-password-file requires --tls-cert to be set" )
1399+
1400+ try :
1401+ with open (args .tls_password_file , "r" , encoding = "utf-8" ) as f :
1402+ tls_key_password = f .read ().strip ()
1403+ except OSError as e :
1404+ parser .error (f"Failed to read TLS password file: { e } " )
1405+
13261406 if args .cgi :
13271407 handler_class = CGIHTTPRequestHandler
13281408 else :
13291409 handler_class = SimpleHTTPRequestHandler
13301410
13311411 # ensure dual-stack is not disabled; ref #38907
1332- class DualStackServer ( ThreadingHTTPServer ) :
1412+ class DualStackServerMixin :
13331413
13341414 def server_bind (self ):
13351415 # suppress exception when protocol is IPv4
@@ -1342,10 +1422,20 @@ def finish_request(self, request, client_address):
13421422 self .RequestHandlerClass (request , client_address , self ,
13431423 directory = args .directory )
13441424
1425+ class HTTPDualStackServer (DualStackServerMixin , ThreadingHTTPServer ):
1426+ pass
1427+ class HTTPSDualStackServer (DualStackServerMixin , ThreadingHTTPSServer ):
1428+ pass
1429+
1430+ ServerClass = HTTPSDualStackServer if args .tls_cert else HTTPDualStackServer
1431+
13451432 test (
13461433 HandlerClass = handler_class ,
1347- ServerClass = DualStackServer ,
1434+ ServerClass = ServerClass ,
13481435 port = args .port ,
13491436 bind = args .bind ,
13501437 protocol = args .protocol ,
1438+ tls_cert = args .tls_cert ,
1439+ tls_key = args .tls_key ,
1440+ tls_password = tls_key_password ,
13511441 )
0 commit comments