From 0ad3c2cffe07a5bf7e3a46dc356c774ba4555eb1 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Thu, 26 Oct 2023 09:02:13 +0000 Subject: [PATCH 01/12] Added example for using adafruit_templateengine with adafruit_httpserver --- examples/directory_listing.tpl.html | 54 +++++++++++++++++++++++++ examples/httpserver_templates.py | 61 +++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 examples/directory_listing.tpl.html create mode 100644 examples/httpserver_templates.py diff --git a/examples/directory_listing.tpl.html b/examples/directory_listing.tpl.html new file mode 100644 index 0000000..9c62ddc --- /dev/null +++ b/examples/directory_listing.tpl.html @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2023 Michal Pokusa +# +# SPDX-License-Identifier: Unlicense + + + +{% 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/httpserver_templates.py b/examples/httpserver_templates.py new file mode 100644 index 0000000..f17f751 --- /dev/null +++ b/examples/httpserver_templates.py @@ -0,0 +1,61 @@ +# 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 + + +@server.route("/") +def directory_listing(request: Request): + path = request.query_params.get("path") or "" + + # Remove .. and . from path + path = re.sub(r"\/(\.\.|\.)\/|\/(\.\.|\.)|(\.\.|\.)\/", "/", path).strip("/") + + if path: + is_file = ( + os.stat(f"/static/{path}")[0] & 0b_11110000_00000000 + ) == 0b_10000000_00000000 + else: + is_file = False + + # If path is a file, return it as a file response + if is_file: + return FileResponse(request, path) + + # Otherwise, return a directory listing + return Response( + request, + render_template( + "directory_listing.tpl.html", + context={ + "path": path, + "items": os.listdir(f"/static/{path}"), + }, + ), + content_type="text/html", + ) + + +# Start the server. +server.serve_forever(str(wifi.radio.ipv4_address)) From 9031248b258d331a52b6eccbbd8f49468dac7a18 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Thu, 26 Oct 2023 09:14:23 +0000 Subject: [PATCH 02/12] Updated docs --- docs/examples.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/examples.rst b/docs/examples.rst index c310821..5561f64 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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: 5- + :emphasize-lines: 3-4,8,12,17-25,29 + :linenos: + +.. literalinclude:: ../examples/httpserver_templates.py + :caption: examples/httpserver_templates.py + :emphasize-lines: 12-15,49-55 + :linenos: + Form data parsing --------------------- From 46bbda72006e5ffb0ef0430de3a4d94e18bdf36f Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 29 Oct 2023 22:58:41 +0000 Subject: [PATCH 03/12] Fix: Preventing only .. in path and allowing . --- examples/httpserver_templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/httpserver_templates.py b/examples/httpserver_templates.py index f17f751..6c8b0de 100644 --- a/examples/httpserver_templates.py +++ b/examples/httpserver_templates.py @@ -29,8 +29,8 @@ def directory_listing(request: Request): path = request.query_params.get("path") or "" - # Remove .. and . from path - path = re.sub(r"\/(\.\.|\.)\/|\/(\.\.|\.)|(\.\.|\.)\/", "/", path).strip("/") + # Preventing path travelsal by removing all ../ from path + path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/") if path: is_file = ( From 85cd890f3d6afa1e8d88fd958a78fbd04ee34886 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 29 Oct 2023 23:12:23 +0000 Subject: [PATCH 04/12] Fix: typo in traversal --- examples/httpserver_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/httpserver_templates.py b/examples/httpserver_templates.py index 6c8b0de..92fc8bd 100644 --- a/examples/httpserver_templates.py +++ b/examples/httpserver_templates.py @@ -29,7 +29,7 @@ def directory_listing(request: Request): path = request.query_params.get("path") or "" - # Preventing path travelsal by removing all ../ from path + # Preventing path traversal by removing all ../ from path path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/") if path: From f0a157522d5249de5724d8e1389e433c258d01dc Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 5 Nov 2023 11:58:41 +0000 Subject: [PATCH 05/12] Changed copyright info to HTML comment --- examples/directory_listing.tpl.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/directory_listing.tpl.html b/examples/directory_listing.tpl.html index 9c62ddc..026e2d4 100644 --- a/examples/directory_listing.tpl.html +++ b/examples/directory_listing.tpl.html @@ -1,6 +1,8 @@ -# SPDX-FileCopyrightText: 2023 Michal Pokusa -# -# SPDX-License-Identifier: Unlicense + From 84794f2866b4fc213cb9c293add0f57831ac4fce Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:01:42 +0000 Subject: [PATCH 06/12] Added support for files/directories that have space in their name --- examples/httpserver_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/httpserver_templates.py b/examples/httpserver_templates.py index 92fc8bd..2c5257a 100644 --- a/examples/httpserver_templates.py +++ b/examples/httpserver_templates.py @@ -27,7 +27,7 @@ @server.route("/") def directory_listing(request: Request): - path = request.query_params.get("path") or "" + path = request.query_params.get("path", "").replace("%20", " ") # Preventing path traversal by removing all ../ from path path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/") From 6a5d1425f9f943091bfcd61135c9782177b96fae Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:04:57 +0000 Subject: [PATCH 07/12] Refactor and added trailing / for directories --- examples/httpserver_templates.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/httpserver_templates.py b/examples/httpserver_templates.py index 2c5257a..54f7a6b 100644 --- a/examples/httpserver_templates.py +++ b/examples/httpserver_templates.py @@ -25,6 +25,10 @@ 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", " ") @@ -32,26 +36,24 @@ def directory_listing(request: Request): # Preventing path traversal by removing all ../ from path path = re.sub(r"\/(\.\.)\/|\/(\.\.)|(\.\.)\/", "/", path).strip("/") - if path: - is_file = ( - os.stat(f"/static/{path}")[0] & 0b_11110000_00000000 - ) == 0b_10000000_00000000 - else: - is_file = False - # If path is a file, return it as a file response - if is_file: + 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": os.listdir(f"/static/{path}"), - }, + context={"path": path, "items": items}, ), content_type="text/html", ) From de8d0fc84e6938a45f26e4148c9b8098f507ee61 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:11:17 +0000 Subject: [PATCH 08/12] Updated docs line references --- docs/examples.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 5561f64..7670dcb 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -186,8 +186,8 @@ You can find more information about the template syntax in the .. literalinclude:: ../examples/directory_listing.tpl.html :caption: examples/directory_listing.tpl.html :language: django - :lines: 5- - :emphasize-lines: 3-4,8,12,17-25,29 + :lines: 9- + :emphasize-lines: 1-2,6,10,15-23,27 :linenos: .. literalinclude:: ../examples/httpserver_templates.py From 00faa249c579d48be067a38648ce67a79f195df2 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:16:14 +0000 Subject: [PATCH 09/12] Added query params to debug message after sending response --- adafruit_httpserver/request.py | 3 +++ adafruit_httpserver/server.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index 0f1bba7..f733b30 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -64,6 +64,9 @@ 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"{key}={value}" for key, value in self.items()) + class File: """ diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 40190f6..4d92923 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -512,7 +512,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 or ''}" req_size = len(response._request.raw_request) status = response._status res_size = response._size From 1be6879fe167cf7849d9a3e54d7943308bb12855 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:16:14 +0000 Subject: [PATCH 10/12] Minor refactor --- adafruit_httpserver/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 4d92923..c3c871f 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, ) From b3d557756d6704c6a5995e2c5873b663f3e1132b Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:16:14 +0000 Subject: [PATCH 11/12] Updated copyright info in home.html to use HTML comments --- docs/examples.rst | 2 +- examples/home.html | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 7670dcb..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 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 + From 3d01ec4a7c6bad0efc6c099a37acea217b4f04ec Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:41:51 +0000 Subject: [PATCH 12/12] Fix: ? always in debug messages and only first value was displayed --- adafruit_httpserver/request.py | 10 ++++++---- adafruit_httpserver/server.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/adafruit_httpserver/request.py b/adafruit_httpserver/request.py index f733b30..d8b0c26 100644 --- a/adafruit_httpserver/request.py +++ b/adafruit_httpserver/request.py @@ -65,7 +65,11 @@ 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"{key}={value}" for key, value in self.items()) + return "&".join( + f"{field_name}={value}" + for field_name in self.fields + for value in self.get_list(field_name) + ) class File: @@ -469,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 c3c871f..183c646 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -512,7 +512,7 @@ def _debug_response_sent(response: "Response", time_elapsed: float): client_ip = response._request.client_address[0] method = response._request.method query_params = response._request.query_params - path = response._request.path + f"?{query_params or ''}" + 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