🐛 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(
templates_dirs=["templates"],
middleware=[
"spiderweb.middleware.csrf.CSRFMiddleware",
"example_middleware.TestMiddleware",
"example_middleware.RedirectMiddleware",
"example_middleware.ExplodingMiddleware",
@ -46,6 +47,19 @@ def example(request, 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__":
# can also add routes like this:
# app.add_route("/", index)

View File

@ -5,20 +5,20 @@ from spiderweb.response import HttpResponse, RedirectResponse
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
request.spiderweb = True
def process_response(
self, request: Request, response: HttpResponse
) -> HttpResponse | None:
) -> None:
# example of a middleware that sets a header on the resp
if hasattr(request, "spiderweb"):
response.headers["X-Spiderweb"] = "true"
class RedirectMiddleware(SpiderwebMiddleware):
def process_request(self, request: Request) -> HttpResponse | None:
def process_request(self, request: Request) -> HttpResponse:
if request.path == "/middleware":
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]]
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)"]
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]]
name = "click"
version = "8.1.7"
@ -69,6 +148,55 @@ files = [
{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]]
name = "iniconfig"
version = "2.0.0"
@ -240,6 +368,17 @@ files = [
dev = ["pre-commit", "tox"]
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]]
name = "pytest"
version = "8.3.2"
@ -290,4 +429,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "f7533dcd984fcec36a80d3523af159bc6ebe71edb3c315d8bfd01690d910fb76"
content-hash = "96cb529cc8a301c9ac0920582fb7ccb26bc789b0c5ccbc4135fc2d8d6936bb75"

View File

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

View File

@ -1 +1,2 @@
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):
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"
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):
pass

View File

@ -2,7 +2,7 @@
# https://gist.github.com/earonesty/ab07b4c0fea2c226e75b3d538cc0dc55
#
# Extensively modified by @itsthejoker
import json
from datetime import datetime, timedelta
import re
import signal
import time
@ -13,10 +13,11 @@ import threading
import logging
from typing import Callable, Any, NoReturn
from cryptography.fernet import Fernet
from jinja2 import Environment, FileSystemLoader
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 (
APIError,
ConfigError,
@ -32,6 +33,9 @@ from spiderweb.utils import import_by_string
log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
DEFAULT_ALLOWED_METHODS = ["GET"]
DEFAULT_ENCODING = "utf-8"
def route(path):
def outer(func):
@ -68,6 +72,7 @@ class WebServer(HTTPServer):
templates_dirs: list[str] = None,
middleware: list[str] = None,
append_slash: bool = False,
secret_key: str = None,
):
"""
Create a new server on address, port. Port can be zero.
@ -83,13 +88,17 @@ class WebServer(HTTPServer):
self.append_slash = append_slash
self.templates_dirs = templates_dirs
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
if self.middleware:
middleware_by_reference = []
for m in self.middleware:
try:
middleware_by_reference.append(import_by_string(m)())
middleware_by_reference.append(import_by_string(m)(server=self))
except ImportError:
raise ConfigError(f"Middleware '{m}' not found.")
self.middleware = middleware_by_reference
@ -105,11 +114,8 @@ class WebServer(HTTPServer):
class HandlerClass(RequestHandler):
pass
# inject template loader, middleware, and other important things into handler
self.handler_class = custom_handler if custom_handler else HandlerClass
self.handler_class.env = self.env
self.handler_class.middleware = self.middleware
self.handler_class.append_slash = self.append_slash
self.handler_class.server = self
# routed methods map into handler
for method in type(self).__dict__.values():
@ -148,22 +154,32 @@ class WebServer(HTTPServer):
if self.convert_path(path) in self.handler_class._routes:
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."""
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("/"):
updated_path = path + "/"
self.check_for_route_duplicates(updated_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(updated_path)] = method
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)] = {'func': method, 'allowed_methods': allowed_methods}
else:
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.
@ -176,15 +192,26 @@ class WebServer(HTTPServer):
return HttpResponse(content="Hello, world!")
:param path: str
:param allowed_methods: list[str]
:return: Callable
"""
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 outer
def error(self, code: int) -> Callable:
def outer(func):
self.add_error_route(code, func)
return func
return outer
@property
def port(self):
"""Return current port."""
@ -225,15 +252,25 @@ class WebServer(HTTPServer):
super().shutdown()
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):
# I can't help the naming convention of these because that's what
# BaseHTTPRequestHandler uses for some weird reason
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# These stop pycharm from complaining about these not existing. They're
# injected by the WebServer class at runtime
_routes = {}
middleware = []
self._routes = {}
self._error_routes = {}
self.server = None
def get_request(self):
return Request(
@ -244,8 +281,11 @@ class RequestHandler(BaseHTTPRequestHandler):
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):
request = self.get_request()
request.method = "GET"
self.handle_request(request)
def do_POST(self):
@ -254,28 +294,23 @@ class RequestHandler(BaseHTTPRequestHandler):
length = int(self.headers["Content-Length"])
content = self.rfile.read(length)
request = self.get_request()
request.method = "POST"
request.content = content
if content:
try:
request.json()
except json.JSONDecodeError:
raise APIError(400, "Invalid JSON", content)
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():
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()
)
), self._routes[option]['allowed_methods']
raise NotFound()
def get_error_route(self, code: int) -> Callable:
try:
view = globals()[f"http{code}"]
return view
except KeyError:
view = self._error_routes.get(code) or globals().get(f"http{code}")
if not view:
return http500
return view
def _fire_response(self, resp: HttpResponse):
self.send_response(resp.status_code)
@ -285,7 +320,7 @@ class RequestHandler(BaseHTTPRequestHandler):
for key, value in resp.headers.items():
self.send_header(key, value)
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):
try:
@ -299,11 +334,11 @@ class RequestHandler(BaseHTTPRequestHandler):
self.fire_response(request, self.get_error_route(500)(request))
def process_request_middleware(self, request: Request) -> None | bool:
for middleware in self.middleware:
for middleware in self.server.middleware:
try:
resp = middleware.process_request(request)
except UnusedMiddleware:
self.middleware.remove(middleware)
self.server.middleware.remove(middleware)
continue
if resp:
self.process_response_middleware(request, resp)
@ -311,11 +346,11 @@ class RequestHandler(BaseHTTPRequestHandler):
return True # abort further processing
def process_response_middleware(self, request: Request, response: HttpResponse) -> None:
for middleware in self.middleware:
for middleware in self.server.middleware:
try:
middleware.process_response(request, response)
except UnusedMiddleware:
self.middleware.remove(middleware)
self.server.middleware.remove(middleware)
continue
def prepare_and_fire_response(self, request, resp) -> None:
@ -323,10 +358,10 @@ class RequestHandler(BaseHTTPRequestHandler):
if isinstance(resp, dict):
self.fire_response(request, JsonResponse(data=resp))
if isinstance(resp, TemplateResponse):
if hasattr(self, "env"): # injected from above
resp.set_template_loader(self.env)
if hasattr(self.server, "env"):
resp.set_template_loader(self.server.env)
for middleware in self.middleware:
for middleware in self.server.middleware:
middleware.process_response(request, resp)
self.fire_response(request, resp)
@ -338,6 +373,9 @@ class RequestHandler(BaseHTTPRequestHandler):
log.error(traceback.format_exc())
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):
try:
self.send_error(e.code, e.msg, e.desc)
@ -349,17 +387,25 @@ class RequestHandler(BaseHTTPRequestHandler):
request.url = urlparse.urlparse(request.path)
try:
handler, additional_args = self.get_route(request.url.path)
handler, additional_args, allowed_methods = self.get_route(request.url.path)
except NotFound:
handler = self.get_error_route(404)
additional_args = {}
allowed_methods = DEFAULT_ALLOWED_METHODS
if request.url.query:
params = urlparse.parse_qs(request.url.query)
else:
params = {}
if request.method not in allowed_methods:
# replace the potentially valid handler with the error route
handler = self.get_error_route(405)
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:
if handler:
# 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
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:
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.url = url
self.query_params = query_params
self.GET = {}
self.POST = {}
def json(self):
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>
<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">
{% extends 'base.html' %}
{% block content %}
<h1 class="mt-5">HI, THIS IS A PAGE</h1>
<p>
@ -19,9 +12,4 @@
The value of <code>request.spiderweb</code> is {{ request.spiderweb }}. If this is True,
middleware is working.
</p>
</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>
{% endblock %}