Solved problem when data is sent in chunks and is not received in full. Bypassed ESP32 TCP buffer limit of 2880 bytes.
200 lines
6.5 KiB
Python
200 lines
6.5 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
"""
|
|
`adafruit_httpserver.server.HTTPServer`
|
|
====================================================
|
|
* Author(s): Dan Halbert, Michał Pokusa
|
|
"""
|
|
|
|
try:
|
|
from typing import Callable, Protocol
|
|
except ImportError:
|
|
pass
|
|
|
|
from errno import EAGAIN, ECONNRESET, ETIMEDOUT
|
|
|
|
from .methods import HTTPMethod
|
|
from .request import HTTPRequest
|
|
from .response import HTTPResponse
|
|
from .route import _HTTPRoute
|
|
from .status import CommonHTTPStatus
|
|
|
|
|
|
class HTTPServer:
|
|
"""A basic socket-based HTTP server."""
|
|
|
|
def __init__(self, socket_source: Protocol) -> None:
|
|
"""Create a server, and get it ready to run.
|
|
|
|
:param socket: An object that is a source of sockets. This could be a `socketpool`
|
|
in CircuitPython or the `socket` module in CPython.
|
|
"""
|
|
self._buffer = bytearray(1024)
|
|
self._timeout = 0
|
|
self.route_handlers = {}
|
|
self._socket_source = socket_source
|
|
self._sock = None
|
|
self.root_path = "/"
|
|
|
|
def route(self, path: str, method: HTTPMethod = HTTPMethod.GET):
|
|
"""Decorator used to add a route.
|
|
|
|
:param str path: filename path
|
|
:param HTTPMethod method: HTTP method: HTTPMethod.GET, HTTPMethod.POST, etc.
|
|
|
|
Example::
|
|
|
|
@server.route(path, method)
|
|
def route_func(request):
|
|
raw_text = request.raw_request.decode("utf8")
|
|
print("Received a request of length", len(raw_text), "bytes")
|
|
return HTTPResponse(body="hello world")
|
|
|
|
"""
|
|
|
|
def route_decorator(func: Callable) -> Callable:
|
|
self.route_handlers[_HTTPRoute(path, method)] = func
|
|
return func
|
|
|
|
return route_decorator
|
|
|
|
def serve_forever(self, host: str, port: int = 80, root_path: str = "") -> None:
|
|
"""Wait for HTTP requests at the given host and port. Does not return.
|
|
|
|
:param str host: host name or IP address
|
|
:param int port: port
|
|
:param str root: root directory to serve files from
|
|
"""
|
|
self.start(host, port, root_path)
|
|
|
|
while True:
|
|
try:
|
|
self.poll()
|
|
except OSError:
|
|
continue
|
|
|
|
def start(self, host: str, port: int = 80, root_path: str = "") -> None:
|
|
"""
|
|
Start the HTTP server at the given host and port. Requires calling
|
|
poll() in a while loop to handle incoming requests.
|
|
|
|
:param str host: host name or IP address
|
|
:param int port: port
|
|
:param str root: root directory to serve files from
|
|
"""
|
|
self.root_path = root_path
|
|
|
|
self._sock = self._socket_source.socket(
|
|
self._socket_source.AF_INET, self._socket_source.SOCK_STREAM
|
|
)
|
|
self._sock.bind((host, port))
|
|
self._sock.listen(10)
|
|
self._sock.setblocking(False) # non-blocking socket
|
|
|
|
def poll(self):
|
|
"""
|
|
Call this method inside your main event loop to get the server to
|
|
check for new incoming client requests. When a request comes in,
|
|
the application callable will be invoked.
|
|
"""
|
|
try:
|
|
conn, _ = self._sock.accept()
|
|
with conn:
|
|
conn.settimeout(self._timeout)
|
|
received_data = bytearray()
|
|
|
|
# Receiving data until timeout
|
|
while "Receiving data":
|
|
try:
|
|
length = conn.recv_into(self._buffer)
|
|
received_data += self._buffer[:length]
|
|
except OSError as ex:
|
|
if ex.errno == ETIMEDOUT:
|
|
break
|
|
except Exception as ex:
|
|
raise ex
|
|
|
|
# Return if no data received
|
|
if not received_data:
|
|
return
|
|
|
|
# Parsing received data
|
|
request = HTTPRequest(raw_request=received_data)
|
|
|
|
handler = self.route_handlers.get(
|
|
_HTTPRoute(request.path, request.method), None
|
|
)
|
|
|
|
# If a handler for route exists and is callable, call it.
|
|
if handler is not None and callable(handler):
|
|
response = handler(request)
|
|
|
|
# If no handler exists and request method is GET, try to serve a file.
|
|
elif request.method == HTTPMethod.GET:
|
|
response = HTTPResponse(
|
|
filename=request.path, root_path=self.root_path
|
|
)
|
|
|
|
# If no handler exists and request method is not GET, return 400 Bad Request.
|
|
else:
|
|
response = HTTPResponse(status=CommonHTTPStatus.BAD_REQUEST_400)
|
|
|
|
response.send(conn)
|
|
except OSError as ex:
|
|
# handle EAGAIN and ECONNRESET
|
|
if ex.errno == EAGAIN:
|
|
# there is no data available right now, try again later.
|
|
return
|
|
if ex.errno == ECONNRESET:
|
|
# connection reset by peer, try again later.
|
|
return
|
|
raise
|
|
|
|
@property
|
|
def request_buffer_size(self) -> int:
|
|
"""
|
|
The maximum size of the incoming request buffer. If the default size isn't
|
|
adequate to handle your incoming data you can set this after creating the
|
|
server instance.
|
|
|
|
Default size is 1024 bytes.
|
|
|
|
Example::
|
|
|
|
server = HTTPServer(pool)
|
|
server.request_buffer_size = 2048
|
|
|
|
server.serve_forever(str(wifi.radio.ipv4_address))
|
|
"""
|
|
return len(self._buffer)
|
|
|
|
@request_buffer_size.setter
|
|
def request_buffer_size(self, value: int) -> None:
|
|
self._buffer = bytearray(value)
|
|
|
|
@property
|
|
def socket_timeout(self) -> int:
|
|
"""
|
|
Timeout after which the socket will stop waiting for more incoming data.
|
|
When exceeded, raises `OSError` with `errno.ETIMEDOUT`.
|
|
|
|
Default timeout is 0, which means socket is in non-blocking mode.
|
|
|
|
Example::
|
|
|
|
server = HTTPServer(pool)
|
|
server.socket_timeout = 3
|
|
|
|
server.serve_forever(str(wifi.radio.ipv4_address))
|
|
"""
|
|
return self._timeout
|
|
|
|
@socket_timeout.setter
|
|
def socket_timeout(self, value: int) -> None:
|
|
if isinstance(value, (int, float)) and value >= 0:
|
|
self._timeout = value
|
|
else:
|
|
raise TypeError(
|
|
"HTTPServer.socket_timeout must be a non-negative numeric value."
|
|
)
|