diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 0f1bba7..d8b0c26 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -64,6 +64,13 @@ def get( def get_list(self, field_name: str, *, safe=True) -> List[str]: return super().get_list(field_name, safe=safe) + def __str__(self) -> str: + return "&".join( + f"{field_name}={value}" + for field_name in self.fields + for value in self.get_list(field_name) + ) + class File: """ @@ -466,9 +473,7 @@ def _parse_request_header( method, path, http_version = start_line.strip().split() - if "?" not in path: - path += "?" - + path = path if "?" in path else path + "?" path, query_string = path.split("?", 1) query_params = QueryParams(query_string) diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 40190f6..183c646 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -313,11 +313,10 @@ def _handle_request( raise ServingFilesDisabledError # Method is GET or HEAD, try to serve a file from the filesystem. - if request.method in [GET, HEAD]: + if request.method in (GET, HEAD): return FileResponse( request, filename=request.path, - root_path=self.root_path, head_only=request.method == HEAD, ) @@ -512,7 +511,8 @@ def _debug_response_sent(response: "Response", time_elapsed: float): # pylint: disable=protected-access client_ip = response._request.client_address[0] method = response._request.method - path = response._request.path + query_params = response._request.query_params + path = response._request.path + (f"?{query_params}" if query_params else "") req_size = len(response._request.raw_request) status = response._status res_size = response._size diff --git a/docs/examples.rst b/docs/examples.rst index c310821..f83392b 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -68,7 +68,7 @@ By default ``FileResponse`` looks for the file in the server's ``root_path`` dir .. literalinclude:: ../examples/home.html :language: html :caption: www/home.html - :lines: 5- + :lines: 7- :linenos: Tasks between requests @@ -170,6 +170,31 @@ Tested on ESP32-S2 Feather. :emphasize-lines: 26-28,41,52,68,74 :linenos: +Templates +--------- + +With the help of the ``adafruit_templateengine`` library, it is possible to achieve somewhat of a +server-side rendering of HTML pages. + +Instead of using string formatting, you can use templates, which can include more complex logic like loops and conditionals. +This makes it very easy to create dynamic pages, witout using JavaScript and exposing any API endpoints. + +Templates also allow splitting the code into multiple files, that can be reused in different places. +You can find more information about the template syntax in the +`adafruit_templateengine documentation `_. + +.. literalinclude:: ../examples/directory_listing.tpl.html + :caption: examples/directory_listing.tpl.html + :language: django + :lines: 9- + :emphasize-lines: 1-2,6,10,15-23,27 + :linenos: + +.. literalinclude:: ../examples/httpserver_templates.py + :caption: examples/httpserver_templates.py + :emphasize-lines: 12-15,49-55 + :linenos: + Form data parsing --------------------- diff --git a/examples/directory_listing.tpl.html b/examples/directory_listing.tpl.html new file mode 100644 index 0000000..026e2d4 --- /dev/null +++ b/examples/directory_listing.tpl.html @@ -0,0 +1,56 @@ + + + + +{% exec path = context.get("path") %} +{% exec items = context.get("items") %} + + + + Codestin Search App + + + +

Directory listing for /{{ path }}

+ + + + + + {# Script for filtering items #} + + + + diff --git a/examples/home.html b/examples/home.html index 635aa50..a403688 100644 --- a/examples/home.html +++ b/examples/home.html @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2023 MichaƂ Pokusa -# -# SPDX-License-Identifier: Unlicense + diff --git a/examples/httpserver_templates.py b/examples/httpserver_templates.py new file mode 100644 index 0000000..54f7a6b --- /dev/null +++ b/examples/httpserver_templates.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2023 Michal Pokusa +# +# SPDX-License-Identifier: Unlicense +import os +import re + +import socketpool +import wifi + +from adafruit_httpserver import Server, Request, Response, FileResponse + +try: + from adafruit_templateengine import render_template +except ImportError as e: + raise ImportError("This example requires adafruit_templateengine library.") from e + + +pool = socketpool.SocketPool(wifi.radio) +server = Server(pool, "/static", debug=True) + +# Create /static directory if it doesn't exist +try: + os.listdir("/static") +except OSError as e: + raise OSError("Please create a /static directory on the CIRCUITPY drive.") from e + + +def is_file(path: str): + return (os.stat(path.rstrip("/"))[0] & 0b_11110000_00000000) == 0b_10000000_00000000 + + +@server.route("/") +def directory_listing(request: Request): + path = request.query_params.get("path", "").replace("%20", " ") + + # Preventing path traversal by removing all ../ from path + path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/") + + # If path is a file, return it as a file response + if is_file(f"/static/{path}"): + return FileResponse(request, path) + + items = sorted( + [ + item + ("" if is_file(f"/static/{path}/{item}") else "/") + for item in os.listdir(f"/static/{path}") + ], + key=lambda item: not item.endswith("/"), + ) + + # Otherwise, return a directory listing + return Response( + request, + render_template( + "directory_listing.tpl.html", + context={"path": path, "items": items}, + ), + content_type="text/html", + ) + + +# Start the server. +server.serve_forever(str(wifi.radio.ipv4_address))