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 }}
+
+
+
+
+ {# Going to parent directory if not alredy in #}
+ {% if path %}
+ - ..
+ {% endif %}
+
+ {# Listing items #}
+ {% for item in items %}
+ - {{ item }}
+ {% endfor %}
+
+
+
+ {# 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