8
8
"""
9
9
10
10
try :
11
- from typing import Callable , Protocol , Union , List
11
+ from typing import Callable , Protocol , Union , List , Tuple
12
12
from socket import socket
13
13
from socketpool import SocketPool
14
14
except ImportError :
17
17
from errno import EAGAIN , ECONNRESET , ETIMEDOUT
18
18
19
19
from .authentication import Basic , Bearer , require_authentication
20
- from .exceptions import AuthenticationError , FileNotExistsError , InvalidPathError
20
+ from .exceptions import (
21
+ AuthenticationError ,
22
+ FileNotExistsError ,
23
+ InvalidPathError ,
24
+ ServingFilesDisabledError ,
25
+ )
21
26
from .methods import GET , HEAD
22
27
from .request import Request
23
28
from .response import Response
28
33
class Server :
29
34
"""A basic socket-based HTTP server."""
30
35
31
- def __init__ (self , socket_source : Protocol , root_path : str ) -> None :
36
+ def __init__ (self , socket_source : Protocol , root_path : str = None ) -> None :
32
37
"""Create a server, and get it ready to run.
33
38
34
39
:param socket: An object that is a source of sockets. This could be a `socketpool`
@@ -83,17 +88,19 @@ def route_decorator(func: Callable) -> Callable:
83
88
return route_decorator
84
89
85
90
def serve_forever (self , host : str , port : int = 80 ) -> None :
86
- """Wait for HTTP requests at the given host and port. Does not return.
91
+ """
92
+ Wait for HTTP requests at the given host and port. Does not return.
93
+ Ignores any exceptions raised by the handler function and continues to serve.
87
94
88
95
:param str host: host name or IP address
89
96
:param int port: port
90
97
"""
91
98
self .start (host , port )
92
99
93
- while True :
100
+ while "Serving forever" :
94
101
try :
95
102
self .poll ()
96
- except OSError :
103
+ except : # pylint: disable=bare-except
97
104
continue
98
105
99
106
def start (self , host : str , port : int = 80 ) -> None :
@@ -111,6 +118,32 @@ def start(self, host: str, port: int = 80) -> None:
111
118
self ._sock .listen (10 )
112
119
self ._sock .setblocking (False ) # non-blocking socket
113
120
121
+ def _receive_request (
122
+ self ,
123
+ sock : Union ["SocketPool.Socket" , "socket.socket" ],
124
+ client_address : Tuple [str , int ],
125
+ ) -> Request :
126
+ """Receive bytes from socket until the whole request is received."""
127
+
128
+ # Receiving data until empty line
129
+ header_bytes = self ._receive_header_bytes (sock )
130
+
131
+ # Return if no data received
132
+ if not header_bytes :
133
+ return None
134
+
135
+ request = Request (sock , client_address , header_bytes )
136
+
137
+ content_length = int (request .headers .get ("Content-Length" , 0 ))
138
+ received_body_bytes = request .body
139
+
140
+ # Receiving remaining body bytes
141
+ request .body = self ._receive_body_bytes (
142
+ sock , received_body_bytes , content_length
143
+ )
144
+
145
+ return request
146
+
114
147
def _receive_header_bytes (
115
148
self , sock : Union ["SocketPool.Socket" , "socket.socket" ]
116
149
) -> bytes :
@@ -147,6 +180,51 @@ def _receive_body_bytes(
147
180
raise ex
148
181
return received_body_bytes [:content_length ]
149
182
183
+ def _serve_file_from_filesystem (self , request : Request ):
184
+ filename = "index.html" if request .path == "/" else request .path
185
+ root_path = self .root_path
186
+ buffer_size = self .request_buffer_size
187
+ head_only = request .method == HEAD
188
+
189
+ with Response (request ) as response :
190
+ response .send_file (filename , root_path , buffer_size , head_only )
191
+
192
+ def _handle_request (self , request : Request , handler : Union [Callable , None ]):
193
+ try :
194
+ # Check server authentications if necessary
195
+ if self ._auths :
196
+ require_authentication (request , self ._auths )
197
+
198
+ # Handler for route exists and is callable
199
+ if handler is not None and callable (handler ):
200
+ handler (request )
201
+
202
+ # Handler is not found...
203
+
204
+ # ...no root_path, access to filesystem disabled, return 404.
205
+ elif self .root_path is None :
206
+ # Response(request, status=NOT_FOUND_404).send()
207
+ raise ServingFilesDisabledError
208
+
209
+ # ..root_path is set, access to filesystem enabled...
210
+
211
+ # ...request.method is GET or HEAD, try to serve a file from the filesystem.
212
+ elif request .method in [GET , HEAD ]:
213
+ self ._serve_file_from_filesystem (request )
214
+ # ...
215
+ else :
216
+ Response (request , status = BAD_REQUEST_400 ).send ()
217
+
218
+ except AuthenticationError :
219
+ headers = {"WWW-Authenticate" : 'Basic charset="UTF-8"' }
220
+ Response (request , status = UNAUTHORIZED_401 , headers = headers ).send ()
221
+
222
+ except InvalidPathError as error :
223
+ Response (request , status = FORBIDDEN_403 ).send (str (error ))
224
+
225
+ except (FileNotExistsError , ServingFilesDisabledError ) as error :
226
+ Response (request , status = NOT_FOUND_404 ).send (str (error ))
227
+
150
228
def poll (self ):
151
229
"""
152
230
Call this method inside your main event loop to get the server to
@@ -158,67 +236,22 @@ def poll(self):
158
236
with conn :
159
237
conn .settimeout (self ._timeout )
160
238
161
- # Receiving data until empty line
162
- header_bytes = self ._receive_header_bytes (conn )
163
-
164
- # Return if no data received
165
- if not header_bytes :
239
+ # Receive the whole request
240
+ if (request := self ._receive_request (conn , client_address )) is None :
166
241
return
167
242
168
- request = Request (conn , client_address , header_bytes )
169
-
170
- content_length = int (request .headers .get ("Content-Length" , 0 ))
171
- received_body_bytes = request .body
172
-
173
- # Receiving remaining body bytes
174
- request .body = self ._receive_body_bytes (
175
- conn , received_body_bytes , content_length
176
- )
177
-
178
243
# Find a handler for the route
179
244
handler = self .routes .find_handler (_Route (request .path , request .method ))
180
245
181
- try :
182
- # Check server authentications if necessary
183
- if self ._auths :
184
- require_authentication (request , self ._auths )
185
-
186
- # If a handler for route exists and is callable, call it.
187
- if handler is not None and callable (handler ):
188
- handler (request )
189
-
190
- # If no handler exists and request method is GET or HEAD, try to serve a file.
191
- elif handler is None and request .method in [GET , HEAD ]:
192
- filename = "index.html" if request .path == "/" else request .path
193
- Response (request ).send_file (
194
- filename = filename ,
195
- root_path = self .root_path ,
196
- buffer_size = self .request_buffer_size ,
197
- head_only = (request .method == HEAD ),
198
- )
199
- else :
200
- Response (request , status = BAD_REQUEST_400 ).send ()
201
-
202
- except AuthenticationError :
203
- Response (
204
- request ,
205
- status = UNAUTHORIZED_401 ,
206
- headers = {"WWW-Authenticate" : 'Basic charset="UTF-8"' },
207
- ).send ()
208
-
209
- except InvalidPathError as error :
210
- Response (request , status = FORBIDDEN_403 ).send (str (error ))
211
-
212
- except FileNotExistsError as error :
213
- Response (request , status = NOT_FOUND_404 ).send (str (error ))
246
+ # Handle the request
247
+ self ._handle_request (request , handler )
214
248
215
249
except OSError as error :
216
- # Handle EAGAIN and ECONNRESET
250
+ # There is no data available right now, try again later.
217
251
if error .errno == EAGAIN :
218
- # There is no data available right now, try again later.
219
252
return
253
+ # Connection reset by peer, try again later.
220
254
if error .errno == ECONNRESET :
221
- # Connection reset by peer, try again later.
222
255
return
223
256
raise
224
257
0 commit comments