🐛 fix middleware and add form handling

This commit is contained in:
Joe Kaufeld 2024-08-14 17:27:21 -04:00
parent 23360a3d91
commit 7b60e2fd32
15 changed files with 352 additions and 69 deletions

View File

@ -6,6 +6,7 @@ from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, Red
app = WebServer( app = WebServer(
templates_dirs=["templates"], templates_dirs=["templates"],
middleware=[ middleware=[
"spiderweb.middleware.csrf.CSRFMiddleware",
"example_middleware.TestMiddleware", "example_middleware.TestMiddleware",
"example_middleware.RedirectMiddleware", "example_middleware.RedirectMiddleware",
"example_middleware.ExplodingMiddleware", "example_middleware.ExplodingMiddleware",
@ -46,6 +47,19 @@ def example(request, id):
return HttpResponse(body=f"Example with id {id}") return HttpResponse(body=f"Example with id {id}")
@app.error(405)
def http405(request) -> HttpResponse:
return HttpResponse(body="Method not allowed", status_code=405)
@app.route("/form", allowed_methods=["POST"])
def form(request):
if request.method == "POST":
return JsonResponse(data=request.POST)
else:
return TemplateResponse(request, "form.html")
if __name__ == "__main__": if __name__ == "__main__":
# can also add routes like this: # can also add routes like this:
# app.add_route("/", index) # app.add_route("/", index)

View File

@ -5,20 +5,20 @@ from spiderweb.response import HttpResponse, RedirectResponse
class TestMiddleware(SpiderwebMiddleware): class TestMiddleware(SpiderwebMiddleware):
def process_request(self, request: Request) -> HttpResponse | None: def process_request(self, request: Request) -> None:
# example of a middleware that sets a flag on the request # example of a middleware that sets a flag on the request
request.spiderweb = True request.spiderweb = True
def process_response( def process_response(
self, request: Request, response: HttpResponse self, request: Request, response: HttpResponse
) -> HttpResponse | None: ) -> None:
# example of a middleware that sets a header on the resp # example of a middleware that sets a header on the resp
if hasattr(request, "spiderweb"): if hasattr(request, "spiderweb"):
response.headers["X-Spiderweb"] = "true" response.headers["X-Spiderweb"] = "true"
class RedirectMiddleware(SpiderwebMiddleware): class RedirectMiddleware(SpiderwebMiddleware):
def process_request(self, request: Request) -> HttpResponse | None: def process_request(self, request: Request) -> HttpResponse:
if request.path == "/middleware": if request.path == "/middleware":
return RedirectResponse("/") return RedirectResponse("/")

143
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]] [[package]]
name = "black" name = "black"
@ -44,6 +44,85 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"] uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cffi"
version = "1.17.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
files = [
{file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"},
{file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"},
{file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"},
{file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"},
{file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"},
{file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"},
{file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"},
{file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"},
{file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"},
{file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"},
{file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"},
{file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"},
{file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
{file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
{file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
{file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
{file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"},
{file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"},
{file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"},
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"},
{file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"},
{file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"},
{file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"},
{file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"},
{file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"},
{file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"},
{file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"},
{file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"},
{file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"},
{file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"},
{file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"},
{file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"},
{file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"},
{file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
]
[package.dependencies]
pycparser = "*"
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.7" version = "8.1.7"
@ -69,6 +148,55 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "cryptography"
version = "43.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
]
[package.dependencies]
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.0.0" version = "2.0.0"
@ -240,6 +368,17 @@ files = [
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pycparser"
version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.3.2" version = "8.3.2"
@ -290,4 +429,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "f7533dcd984fcec36a80d3523af159bc6ebe71edb3c315d8bfd01690d910fb76" content-hash = "96cb529cc8a301c9ac0920582fb7ccb26bc789b0c5ccbc4135fc2d8d6936bb75"

View File

@ -9,6 +9,7 @@ readme = "README.md"
python = "^3.11" python = "^3.11"
peewee = "^3.17.6" peewee = "^3.17.6"
jinja2 = "^3.1.4" jinja2 = "^3.1.4"
cryptography = "^43.0.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "^0.5.5" ruff = "^0.5.5"

View File

@ -1 +1,2 @@
from spiderweb.main import route, WebServer # noqa: F401 from spiderweb.main import route, WebServer # noqa: F401
from spiderweb.middleware import * # noqa: F401, F403

View File

@ -11,5 +11,9 @@ def http404(request):
) )
def http405(request):
return JsonResponse(data={"error": "Method not allowed"}, status_code=405)
def http500(request): def http500(request):
return JsonResponse(data={"error": "Internal server error"}, status_code=500) return JsonResponse(data={"error": "Internal server error"}, status_code=500)

View File

@ -52,6 +52,14 @@ class ServerError(SpiderwebNetworkException):
self.desc = desc if desc else "The server has encountered an error" self.desc = desc if desc else "The server has encountered an error"
class CSRFError(SpiderwebNetworkException):
def __init__(self, desc=None):
self.code = 403
self.msg = "Forbidden"
self.desc = desc if desc else "CSRF token is invalid"
class ConfigError(SpiderwebException): class ConfigError(SpiderwebException):
pass pass

View File

@ -2,7 +2,7 @@
# https://gist.github.com/earonesty/ab07b4c0fea2c226e75b3d538cc0dc55 # https://gist.github.com/earonesty/ab07b4c0fea2c226e75b3d538cc0dc55
# #
# Extensively modified by @itsthejoker # Extensively modified by @itsthejoker
import json from datetime import datetime, timedelta
import re import re
import signal import signal
import time import time
@ -13,10 +13,11 @@ import threading
import logging import logging
from typing import Callable, Any, NoReturn from typing import Callable, Any, NoReturn
from cryptography.fernet import Fernet
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from spiderweb.converters import * # noqa: F403 from spiderweb.converters import * # noqa: F403
from spiderweb.default_responses import http403, http404, http500 # noqa: F401 from spiderweb.default_responses import * # noqa: F403
from spiderweb.exceptions import ( from spiderweb.exceptions import (
APIError, APIError,
ConfigError, ConfigError,
@ -32,6 +33,9 @@ from spiderweb.utils import import_by_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
DEFAULT_ALLOWED_METHODS = ["GET"]
DEFAULT_ENCODING = "utf-8"
def route(path): def route(path):
def outer(func): def outer(func):
@ -68,6 +72,7 @@ class WebServer(HTTPServer):
templates_dirs: list[str] = None, templates_dirs: list[str] = None,
middleware: list[str] = None, middleware: list[str] = None,
append_slash: bool = False, append_slash: bool = False,
secret_key: str = None,
): ):
""" """
Create a new server on address, port. Port can be zero. Create a new server on address, port. Port can be zero.
@ -83,13 +88,17 @@ class WebServer(HTTPServer):
self.append_slash = append_slash self.append_slash = append_slash
self.templates_dirs = templates_dirs self.templates_dirs = templates_dirs
self.middleware = middleware if middleware else [] self.middleware = middleware if middleware else []
self.secret_key = secret_key if secret_key else self._create_secret_key()
self.fernet = Fernet(self.key)
self.DEFAULT_ENCODING = DEFAULT_ENCODING
self.DEFAULT_ALLOWED_METHODS = DEFAULT_ALLOWED_METHODS
self._thread = None self._thread = None
if self.middleware: if self.middleware:
middleware_by_reference = [] middleware_by_reference = []
for m in self.middleware: for m in self.middleware:
try: try:
middleware_by_reference.append(import_by_string(m)()) middleware_by_reference.append(import_by_string(m)(server=self))
except ImportError: except ImportError:
raise ConfigError(f"Middleware '{m}' not found.") raise ConfigError(f"Middleware '{m}' not found.")
self.middleware = middleware_by_reference self.middleware = middleware_by_reference
@ -105,11 +114,8 @@ class WebServer(HTTPServer):
class HandlerClass(RequestHandler): class HandlerClass(RequestHandler):
pass pass
# inject template loader, middleware, and other important things into handler
self.handler_class = custom_handler if custom_handler else HandlerClass self.handler_class = custom_handler if custom_handler else HandlerClass
self.handler_class.env = self.env self.handler_class.server = self
self.handler_class.middleware = self.middleware
self.handler_class.append_slash = self.append_slash
# routed methods map into handler # routed methods map into handler
for method in type(self).__dict__.values(): for method in type(self).__dict__.values():
@ -148,22 +154,32 @@ class WebServer(HTTPServer):
if self.convert_path(path) in self.handler_class._routes: if self.convert_path(path) in self.handler_class._routes:
raise ConfigError(f"Route '{path}' already exists.") raise ConfigError(f"Route '{path}' already exists.")
def add_route(self, path: str, method: Callable): def add_route(self, path: str, method: Callable, allowed_methods: list[str]):
"""Add a route to the server.""" """Add a route to the server."""
if not hasattr(self.handler_class, "_routes"): if not hasattr(self.handler_class, "_routes"):
setattr(self.handler_class, "_routes", []) setattr(self.handler_class, "_routes", {})
if self.append_slash and not path.endswith("/"): if self.append_slash and not path.endswith("/"):
updated_path = path + "/" updated_path = path + "/"
self.check_for_route_duplicates(updated_path) self.check_for_route_duplicates(updated_path)
self.check_for_route_duplicates(path) self.check_for_route_duplicates(path)
self.handler_class._routes[self.convert_path(path)] = DummyRedirectRoute(updated_path) self.handler_class._routes[self.convert_path(path)] = {'func': DummyRedirectRoute(updated_path), 'allowed_methods': allowed_methods}
self.handler_class._routes[self.convert_path(updated_path)] = method self.handler_class._routes[self.convert_path(updated_path)] = {'func': method, 'allowed_methods': allowed_methods}
else: else:
self.check_for_route_duplicates(path) self.check_for_route_duplicates(path)
self.handler_class._routes[self.convert_path(path)] = method self.handler_class._routes[self.convert_path(path)] = {'func': method, 'allowed_methods': allowed_methods}
def route(self, path) -> Callable: def add_error_route(self, code: int, method: Callable):
"""Add an error route to the server."""
if not hasattr(self.handler_class, "_error_routes"):
setattr(self.handler_class, "_error_routes", {})
if code not in self.handler_class._error_routes:
self.handler_class._error_routes[code] = method
else:
raise ConfigError(f"Error route for code {code} already exists.")
def route(self, path, allowed_methods=None) -> Callable:
""" """
Decorator for adding a route to a view. Decorator for adding a route to a view.
@ -176,15 +192,26 @@ class WebServer(HTTPServer):
return HttpResponse(content="Hello, world!") return HttpResponse(content="Hello, world!")
:param path: str :param path: str
:param allowed_methods: list[str]
:return: Callable :return: Callable
""" """
def outer(func): def outer(func):
self.add_route(path, func) self.add_route(
path,
func,
allowed_methods if allowed_methods else DEFAULT_ALLOWED_METHODS
)
return func return func
return outer return outer
def error(self, code: int) -> Callable:
def outer(func):
self.add_error_route(code, func)
return func
return outer
@property @property
def port(self): def port(self):
"""Return current port.""" """Return current port."""
@ -225,15 +252,25 @@ class WebServer(HTTPServer):
super().shutdown() super().shutdown()
self.socket.close() self.socket.close()
def _create_secret_key(self):
self.key = Fernet.generate_key()
def encrypt(self, data: str):
return self.fernet.encrypt(bytes(data, DEFAULT_ENCODING))
def decrypt(self, data: str):
if isinstance(data, bytes):
return self.fernet.decrypt(data).decode(DEFAULT_ENCODING)
return self.fernet.decrypt(bytes(data, DEFAULT_ENCODING)).decode(DEFAULT_ENCODING)
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
# I can't help the naming convention of these because that's what def __init__(self, *args, **kwargs):
# BaseHTTPRequestHandler uses for some weird reason super().__init__(*args, **kwargs)
# These stop pycharm from complaining about these not existing. They're # These stop pycharm from complaining about these not existing. They're
# injected by the WebServer class at runtime # injected by the WebServer class at runtime
_routes = {} self._routes = {}
middleware = [] self._error_routes = {}
self.server = None
def get_request(self): def get_request(self):
return Request( return Request(
@ -244,8 +281,11 @@ class RequestHandler(BaseHTTPRequestHandler):
path=self.path, path=self.path,
) )
# I can't help the naming convention of these because that's what
# BaseHTTPRequestHandler uses for some weird reason
def do_GET(self): def do_GET(self):
request = self.get_request() request = self.get_request()
request.method = "GET"
self.handle_request(request) self.handle_request(request)
def do_POST(self): def do_POST(self):
@ -254,28 +294,23 @@ class RequestHandler(BaseHTTPRequestHandler):
length = int(self.headers["Content-Length"]) length = int(self.headers["Content-Length"])
content = self.rfile.read(length) content = self.rfile.read(length)
request = self.get_request() request = self.get_request()
request.method = "POST"
request.content = content request.content = content
if content:
try:
request.json()
except json.JSONDecodeError:
raise APIError(400, "Invalid JSON", content)
self.handle_request(request) self.handle_request(request)
def get_route(self, path) -> tuple[Callable, dict[str, Any]]: def get_route(self, path) -> tuple[Callable, dict[str, Any], list[str]]:
for option in self._routes.keys(): for option in self._routes.keys():
if match_data := option.match(path): if match_data := option.match(path):
return self._routes[option], convert_match_to_dict( return self._routes[option]['func'], convert_match_to_dict(
match_data.groupdict() match_data.groupdict()
) ), self._routes[option]['allowed_methods']
raise NotFound() raise NotFound()
def get_error_route(self, code: int) -> Callable: def get_error_route(self, code: int) -> Callable:
try: view = self._error_routes.get(code) or globals().get(f"http{code}")
view = globals()[f"http{code}"] if not view:
return view
except KeyError:
return http500 return http500
return view
def _fire_response(self, resp: HttpResponse): def _fire_response(self, resp: HttpResponse):
self.send_response(resp.status_code) self.send_response(resp.status_code)
@ -285,7 +320,7 @@ class RequestHandler(BaseHTTPRequestHandler):
for key, value in resp.headers.items(): for key, value in resp.headers.items():
self.send_header(key, value) self.send_header(key, value)
self.end_headers() self.end_headers()
self.wfile.write(bytes(content, "utf-8")) self.wfile.write(bytes(content, DEFAULT_ENCODING))
def fire_response(self, request: Request, resp: HttpResponse): def fire_response(self, request: Request, resp: HttpResponse):
try: try:
@ -299,11 +334,11 @@ class RequestHandler(BaseHTTPRequestHandler):
self.fire_response(request, self.get_error_route(500)(request)) self.fire_response(request, self.get_error_route(500)(request))
def process_request_middleware(self, request: Request) -> None | bool: def process_request_middleware(self, request: Request) -> None | bool:
for middleware in self.middleware: for middleware in self.server.middleware:
try: try:
resp = middleware.process_request(request) resp = middleware.process_request(request)
except UnusedMiddleware: except UnusedMiddleware:
self.middleware.remove(middleware) self.server.middleware.remove(middleware)
continue continue
if resp: if resp:
self.process_response_middleware(request, resp) self.process_response_middleware(request, resp)
@ -311,11 +346,11 @@ class RequestHandler(BaseHTTPRequestHandler):
return True # abort further processing return True # abort further processing
def process_response_middleware(self, request: Request, response: HttpResponse) -> None: def process_response_middleware(self, request: Request, response: HttpResponse) -> None:
for middleware in self.middleware: for middleware in self.server.middleware:
try: try:
middleware.process_response(request, response) middleware.process_response(request, response)
except UnusedMiddleware: except UnusedMiddleware:
self.middleware.remove(middleware) self.server.middleware.remove(middleware)
continue continue
def prepare_and_fire_response(self, request, resp) -> None: def prepare_and_fire_response(self, request, resp) -> None:
@ -323,10 +358,10 @@ class RequestHandler(BaseHTTPRequestHandler):
if isinstance(resp, dict): if isinstance(resp, dict):
self.fire_response(request, JsonResponse(data=resp)) self.fire_response(request, JsonResponse(data=resp))
if isinstance(resp, TemplateResponse): if isinstance(resp, TemplateResponse):
if hasattr(self, "env"): # injected from above if hasattr(self.server, "env"):
resp.set_template_loader(self.env) resp.set_template_loader(self.server.env)
for middleware in self.middleware: for middleware in self.server.middleware:
middleware.process_response(request, resp) middleware.process_response(request, resp)
self.fire_response(request, resp) self.fire_response(request, resp)
@ -338,6 +373,9 @@ class RequestHandler(BaseHTTPRequestHandler):
log.error(traceback.format_exc()) log.error(traceback.format_exc())
self.fire_response(request, self.get_error_route(500)(request)) self.fire_response(request, self.get_error_route(500)(request))
def is_form_request(self, request: Request) -> bool:
return "Content-Type" in request.headers and request.headers["Content-Type"] == "application/x-www-form-urlencoded"
def send_error_response(self, request: Request, e: SpiderwebNetworkException): def send_error_response(self, request: Request, e: SpiderwebNetworkException):
try: try:
self.send_error(e.code, e.msg, e.desc) self.send_error(e.code, e.msg, e.desc)
@ -349,17 +387,25 @@ class RequestHandler(BaseHTTPRequestHandler):
request.url = urlparse.urlparse(request.path) request.url = urlparse.urlparse(request.path)
try: try:
handler, additional_args = self.get_route(request.url.path) handler, additional_args, allowed_methods = self.get_route(request.url.path)
except NotFound: except NotFound:
handler = self.get_error_route(404) handler = self.get_error_route(404)
additional_args = {} additional_args = {}
allowed_methods = DEFAULT_ALLOWED_METHODS
if request.url.query: if request.method not in allowed_methods:
params = urlparse.parse_qs(request.url.query) # replace the potentially valid handler with the error route
else: handler = self.get_error_route(405)
params = {}
request.query_params = urlparse.parse_qs(request.url.query) if request.url.query else {}
if self.is_form_request(request):
formdata = urlparse.parse_qs(request.content.decode("utf-8"))
for key, value in formdata.items():
if len(value) == 1:
formdata[key] = value[0]
setattr(request, request.method, formdata)
request.query_params = params
try: try:
if handler: if handler:
# middleware is injected from WebServer # middleware is injected from WebServer

View File

@ -0,0 +1,2 @@
from .base import SpiderwebMiddleware
from .csrf import CSRFMiddleware

View File

@ -17,6 +17,8 @@ class SpiderwebMiddleware:
If `process_request` returns a HttpResponse, the request will be short-circuited If `process_request` returns a HttpResponse, the request will be short-circuited
and the response will be returned immediately. `process_response` will not be called. and the response will be returned immediately. `process_response` will not be called.
""" """
def __init__(self, server):
self.server = server
def process_request(self, request: Request) -> HttpResponse | None: def process_request(self, request: Request) -> HttpResponse | None:
pass pass

View File

@ -0,0 +1,37 @@
from datetime import datetime, timedelta
from spiderweb.exceptions import CSRFError
from spiderweb.middleware import SpiderwebMiddleware
from spiderweb.request import Request
from spiderweb.response import HttpResponse
class CSRFMiddleware(SpiderwebMiddleware):
CSRF_EXPIRY = 60 * 60 # 1 hour
def process_request(self, request: Request) -> HttpResponse | None:
if request.method == "POST":
csrf_token = request.headers.get("X-CSRF-TOKEN") or request.GET.get("csrf_token") or request.POST.get("csrf_token")
if self.is_csrf_valid(csrf_token):
return None
else:
raise CSRFError()
return None
def process_response(self, request: Request, response: HttpResponse) -> None:
token = self.get_csrf_token()
# do we need it in both places?
response.headers["X-CSRF-TOKEN"] = token
request.csrf_token = token
def get_csrf_token(self):
return self.server.encrypt(str(datetime.now().isoformat())).decode(self.server.DEFAULT_ENCODING)
def is_csrf_valid(self, key):
try:
decoded = self.server.decrypt(key)
if datetime.now() - timedelta(seconds=self.CSRF_EXPIRY) > datetime.fromisoformat(decoded):
return False
return True
except Exception:
return False

View File

@ -19,6 +19,8 @@ class Request:
self.path: str = path self.path: str = path
self.url = url self.url = url
self.query_params = query_params self.query_params = query_params
self.GET = {}
self.POST = {}
def json(self): def json(self):
return json.loads(self.content) return json.loads(self.content)

18
templates/base.html Normal file
View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
<div class="container">
{% block content %}There's no content here.{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>

21
templates/form.html Normal file
View File

@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block content %}
<form action="" method="post">
<div class="mb-3">
<label for="exampleFormControlInput1" class="form-label">Email address</label>
<input type="email" class="form-control" name="email" id="exampleFormControlInput1" placeholder="name@example.com">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Example textarea</label>
<textarea class="form-control" name="comment" id="exampleFormControlTextarea1" rows="3"></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" name="formcheck" class="form-check-input" id="exampleCheck1">
<label class="form-check-label" for="exampleCheck1">Check me out</label>
</div>
<input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}

View File

@ -1,14 +1,7 @@
<!doctype html> {% extends 'base.html' %}
<html lang="en">
<head>
<meta charset="utf-8"> {% block content %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
<div class="container">
<h1 class="mt-5">HI, THIS IS A PAGE</h1> <h1 class="mt-5">HI, THIS IS A PAGE</h1>
<p> <p>
@ -19,9 +12,4 @@
The value of <code>request.spiderweb</code> is {{ request.spiderweb }}. If this is True, The value of <code>request.spiderweb</code> is {{ request.spiderweb }}. If this is True,
middleware is working. middleware is working.
</p> </p>
</div> {% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>