12
12
except ImportError :
13
13
pass
14
14
15
+ from ssl import SSLContext , create_default_context
15
16
from errno import EAGAIN , ECONNRESET , ETIMEDOUT
16
17
from sys import implementation
17
18
from time import monotonic , sleep
39
40
REQUEST_HANDLED_NO_RESPONSE = "request_handled_no_response"
40
41
REQUEST_HANDLED_RESPONSE_SENT = "request_handled_response_sent"
41
42
43
+ # CircuitPython does not have these error codes
44
+ MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE = - 30592
45
+
42
46
43
47
class Server : # pylint: disable=too-many-instance-attributes
44
48
"""A basic socket-based HTTP server."""
@@ -52,25 +56,57 @@ class Server: # pylint: disable=too-many-instance-attributes
52
56
root_path : str
53
57
"""Root directory to serve files from. ``None`` if serving files is disabled."""
54
58
59
+ @staticmethod
60
+ def _validate_https_cert_provided (certfile : str , keyfile : str ) -> None :
61
+ if not certfile or not keyfile :
62
+ raise ValueError ("Both certfile and keyfile must be specified for HTTPS" )
63
+
64
+ @staticmethod
65
+ def _create_ssl_context (certfile : str , keyfile : str ) -> SSLContext :
66
+ ssl_context = create_default_context ()
67
+ ssl_context .load_verify_locations (cadata = "" )
68
+ ssl_context .load_cert_chain (certfile , keyfile )
69
+
70
+ return ssl_context
71
+
55
72
def __init__ (
56
- self , socket_source : _ISocketPool , root_path : str = None , * , debug : bool = False
73
+ self ,
74
+ socket_source : _ISocketPool ,
75
+ root_path : str = None ,
76
+ * ,
77
+ https : bool = False ,
78
+ certfile : str = None ,
79
+ keyfile : str = None ,
80
+ debug : bool = False ,
57
81
) -> None :
58
82
"""Create a server, and get it ready to run.
59
83
60
84
:param socket: An object that is a source of sockets. This could be a `socketpool`
61
85
in CircuitPython or the `socket` module in CPython.
62
86
:param str root_path: Root directory to serve files from
63
87
:param bool debug: Enables debug messages useful during development
88
+ :param bool https: If True, the server will use HTTPS
89
+ :param str certfile: Path to the certificate file, required if ``https`` is True
90
+ :param str keyfile: Path to the private key file, required if ``https`` is True
64
91
"""
65
- self ._auths = []
66
92
self ._buffer = bytearray (1024 )
67
93
self ._timeout = 1
94
+
95
+ self ._auths = []
68
96
self ._routes : "List[Route]" = []
97
+ self .headers = Headers ()
98
+
69
99
self ._socket_source = socket_source
70
100
self ._sock = None
71
- self . headers = Headers ()
101
+
72
102
self .host , self .port = None , None
73
103
self .root_path = root_path
104
+ self .https = https
105
+
106
+ if https :
107
+ self ._validate_https_cert_provided (certfile , keyfile )
108
+ self ._ssl_context = self ._create_ssl_context (certfile , keyfile )
109
+
74
110
if root_path in ["" , "/" ] and debug :
75
111
_debug_warning_exposed_files (root_path )
76
112
self .stopped = True
@@ -197,6 +233,7 @@ def serve_forever(
197
233
@staticmethod
198
234
def _create_server_socket (
199
235
socket_source : _ISocketPool ,
236
+ ssl_context : SSLContext ,
200
237
host : str ,
201
238
port : int ,
202
239
) -> _ISocket :
@@ -206,6 +243,9 @@ def _create_server_socket(
206
243
if implementation .version >= (9 ,) or implementation .name != "circuitpython" :
207
244
sock .setsockopt (socket_source .SOL_SOCKET , socket_source .SO_REUSEADDR , 1 )
208
245
246
+ if ssl_context is not None :
247
+ sock = ssl_context .wrap_socket (sock , server_side = True )
248
+
209
249
sock .bind ((host , port ))
210
250
sock .listen (10 )
211
251
sock .setblocking (False ) # Non-blocking socket
@@ -225,7 +265,9 @@ def start(self, host: str = "0.0.0.0", port: int = 5000) -> None:
225
265
self .host , self .port = host , port
226
266
227
267
self .stopped = False
228
- self ._sock = self ._create_server_socket (self ._socket_source , host , port )
268
+ self ._sock = self ._create_server_socket (
269
+ self ._socket_source , self ._ssl_context , host , port
270
+ )
229
271
230
272
if self .debug :
231
273
_debug_started_server (self )
@@ -439,6 +481,8 @@ def poll(self) -> str:
439
481
# Connection reset by peer, try again later.
440
482
if error .errno == ECONNRESET :
441
483
return NO_REQUEST
484
+ if error .errno == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE :
485
+ return NO_REQUEST
442
486
443
487
if self .debug :
444
488
_debug_exception_in_handler (error )
@@ -547,9 +591,10 @@ def _debug_warning_exposed_files(root_path: str):
547
591
548
592
def _debug_started_server (server : "Server" ):
549
593
"""Prints a message when the server starts."""
594
+ scheme = "https" if server .https else "http"
550
595
host , port = server .host , server .port
551
596
552
- print (f"Started development server on http ://{ host } :{ port } " )
597
+ print (f"Started development server on { scheme } ://{ host } :{ port } " )
553
598
554
599
555
600
def _debug_response_sent (response : "Response" , time_elapsed : float ):
0 commit comments