Skip to content

Commit e5ddaaf

Browse files
committed
Refactor of .poll and .server_forever, added option to disable filesystem access
1 parent 18d4a53 commit e5ddaaf

File tree

2 files changed

+96
-57
lines changed

2 files changed

+96
-57
lines changed

adafruit_httpserver/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ class ResponseAlreadySentError(Exception):
4646
"""
4747

4848

49+
class ServingFilesDisabledError(Exception):
50+
"""
51+
Raised when ``root_path`` is not set and there is no handler for `request`.
52+
"""
53+
54+
4955
class FileNotExistsError(Exception):
5056
"""
5157
Raised when a file does not exist.

adafruit_httpserver/server.py

Lines changed: 90 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99

1010
try:
11-
from typing import Callable, Protocol, Union, List
11+
from typing import Callable, Protocol, Union, List, Tuple
1212
from socket import socket
1313
from socketpool import SocketPool
1414
except ImportError:
@@ -17,7 +17,12 @@
1717
from errno import EAGAIN, ECONNRESET, ETIMEDOUT
1818

1919
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+
)
2126
from .methods import GET, HEAD
2227
from .request import Request
2328
from .response import Response
@@ -28,7 +33,7 @@
2833
class Server:
2934
"""A basic socket-based HTTP server."""
3035

31-
def __init__(self, socket_source: Protocol, root_path: str) -> None:
36+
def __init__(self, socket_source: Protocol, root_path: str = None) -> None:
3237
"""Create a server, and get it ready to run.
3338
3439
: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:
8388
return route_decorator
8489

8590
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.
8794
8895
:param str host: host name or IP address
8996
:param int port: port
9097
"""
9198
self.start(host, port)
9299

93-
while True:
100+
while "Serving forever":
94101
try:
95102
self.poll()
96-
except OSError:
103+
except: # pylint: disable=bare-except
97104
continue
98105

99106
def start(self, host: str, port: int = 80) -> None:
@@ -111,6 +118,32 @@ def start(self, host: str, port: int = 80) -> None:
111118
self._sock.listen(10)
112119
self._sock.setblocking(False) # non-blocking socket
113120

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+
114147
def _receive_header_bytes(
115148
self, sock: Union["SocketPool.Socket", "socket.socket"]
116149
) -> bytes:
@@ -147,6 +180,51 @@ def _receive_body_bytes(
147180
raise ex
148181
return received_body_bytes[:content_length]
149182

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+
150228
def poll(self):
151229
"""
152230
Call this method inside your main event loop to get the server to
@@ -158,67 +236,22 @@ def poll(self):
158236
with conn:
159237
conn.settimeout(self._timeout)
160238

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:
166241
return
167242

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-
178243
# Find a handler for the route
179244
handler = self.routes.find_handler(_Route(request.path, request.method))
180245

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)
214248

215249
except OSError as error:
216-
# Handle EAGAIN and ECONNRESET
250+
# There is no data available right now, try again later.
217251
if error.errno == EAGAIN:
218-
# There is no data available right now, try again later.
219252
return
253+
# Connection reset by peer, try again later.
220254
if error.errno == ECONNRESET:
221-
# Connection reset by peer, try again later.
222255
return
223256
raise
224257

0 commit comments

Comments
 (0)