From d4f8109663f25cf5de96679314648c6581ff5da1 Mon Sep 17 00:00:00 2001 From: Joe Kaufeld Date: Wed, 28 Aug 2024 17:39:22 -0400 Subject: [PATCH] :memo: finish sessions and custom middleware docs --- docs/_sidebar.md | 3 +- docs/middleware/csrf.md | 6 +- docs/middleware/custom_middleware.md | 62 ++++++++++++++++++++ docs/middleware/sessions.md | 87 ++++++++++++++++++++++++++++ spiderweb/middleware/sessions.py | 2 +- 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 docs/middleware/custom_middleware.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 240e30b..ce8cf8b 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -3,6 +3,7 @@ - [responses](responses.md) - middleware - [overview](middleware/overview.md) + - [session](middleware/sessions.md) - [csrf](middleware/csrf.md) - [pydantic](middleware/pydantic.md) - - [session](middleware/sessions.md) + - [writing your own](middleware/custom_middleware.md) diff --git a/docs/middleware/csrf.md b/docs/middleware/csrf.md index c1b36fc..8458dd3 100644 --- a/docs/middleware/csrf.md +++ b/docs/middleware/csrf.md @@ -16,7 +16,8 @@ app = SpiderwebRouter( Cross-site request forgery, put simply, is a method for attackers to make legitimate-looking requests in your name to a service or system that you've previously authenticated to. Ways that we can protect against this involve aggressively expiring session cookies, special IDs for forms that are keyed to a specific user, and more. -Notice that in the example above, SessionMiddleware is also included in the middleware list. The CSRF middleware requires the SessionMiddleware to function, and SessionMiddleware must be placed above it in the middleware list. +> [!TIP] +> Notice that in the example above, SessionMiddleware is also included in the middleware list. The CSRF middleware requires the SessionMiddleware to function, and SessionMiddleware must be placed above it in the middleware list. ## CSRF and Forms @@ -52,12 +53,13 @@ def form(request): - Bootstrap demo + Form Demo
+

Example Form

diff --git a/docs/middleware/custom_middleware.md b/docs/middleware/custom_middleware.md new file mode 100644 index 0000000..c6991fe --- /dev/null +++ b/docs/middleware/custom_middleware.md @@ -0,0 +1,62 @@ +from spiderweb import HttpResponse + +# writing your own middleware + +Sometimes you want to run the same code on every request or every response (or both!). Lots of processing happens in the middleware layer, and if you want to write your own, all you have to do is write a quick class and put it in a place that Spiderweb can find it. A piece of middleware only needs two things to be successful: + +- it must be a class that inherits from SpiderwebMiddleware +- it must handle either requests, responses, or both! + +That's really all there is to it. Here's a template you can copy: + +```python +from spiderweb.middleware import SpiderwebMiddleware +from spiderweb.request import Request +from spiderweb.response import HttpResponse + + +class TestMiddleware(SpiderwebMiddleware): + 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) -> None: + # example of a middleware that sets a header on the resp + if hasattr(request, "spiderweb"): + response.headers["X-Spiderweb"] = "true" +``` + +Middleware is run twice: once for the incoming request and once for the outgoing response. You only need to include whichever function is required for the functionality you need. + +## process_request(self, request): + +`process_request` is called before the view is reached in the execution order. You will receive the assembled Request object, and any middleware declared above this one will have already run. Because the request is the single instantiation of a class, you can modify it in-place without returning anything and your changes will stick. + +This function also has a special ability; it can stop execution before the view is called by returning a response. If a response is returned, Spiderweb will immediately skip to applying the response middleware and sending the response back to the client. Here's an example of what that might look like: + +```python +class JohnMiddleware(SpiderwebMiddleware): + def process_request(self, request: Request) -> Optional[HttpResponse]: + if ( + hasattr(request, "user") + and user.name == "John" + and request.path.startswith("/admin") + ): + return HttpResponse("Go away, John!", status_code=403) +``` + +In this case, if the user John tries to access any route that starts with "/admin", he'll immediately get denied and the view will never be called. If the request does not have a user attached to it (or the user is not John), then the middleware will return None and Spiderweb will continue processing. + +## process_response(self, request, response): + +This function is called after the view has run and returned a response. You will receive the request object and the response object; like with the request object, the response is also a single instantiation of a class, so any changes you make will stick automatically. + +Unlike `process_request`, returning a value here doesn't change anything. We're already processing a request, and there are opportunities to turn away requests / change the response at both the `process_request` layer and the view layer, so Spiderweb assumes that whatever it is working on here is what you mean to return to the user. The response object that you receive in the middleware is still prerendered, so any changes you make to it will take effect after it finishes the middleware and renders the response. + +## UnusedMiddleware + +```python +from spiderweb.exceptions import UnusedMiddleware +``` + +If you don't want your middleware to run for some reason, either `process_request` or `process_response` can raise the UnusedMiddleware exception. If this happens, Spiderweb will kick your middleware out of the processing order for the rest of the life of the server. Note that this applies to the middleware as a whole, so both functions will not be run if an UnusedMiddleware is raised. This is a great way to mark debug middleware that shouldn't run or create time-delay middleware that runs until a certain condition is met! diff --git a/docs/middleware/sessions.md b/docs/middleware/sessions.md index e69de29..56df39d 100644 --- a/docs/middleware/sessions.md +++ b/docs/middleware/sessions.md @@ -0,0 +1,87 @@ +# sessions middleware + +```python +from spiderweb import SpiderwebRouter + +app = SpiderwebRouter( + middleware=[ + "spiderweb.middleware.sessions.SessionMiddleware", + ], +) +``` + +Arguably one of the more important things that a server-side web framework can do, besides take in requests and serve responses, is keep track of folks as they navigate your website. That's what the sessions middleware is for! + +Visitors are assigned a random value when they visit for the first time, and that value will follow them around until it either expires or it's deleted. The total amount of time that it's around is configurable, as are the various settings for the session cookie. + +## request.SESSION + +When the sessions middleware is enabled, the request object will have a new attribute labeled `SESSION`. This is a dictionary, and you can put pretty much anything you want in it as long as its serializable to JSON! When the user visits again with an active session, the data will automatically be available on the `SESSION` object again. Here's an example of a complete server using sessions: + +```python +from spiderweb import SpiderwebRouter, HttpResponse + +app = SpiderwebRouter( + middleware=["spiderweb.middleware.sessions.SessionMiddleware"], +) + +@app.route("/") +def session(request): + if "val" not in request.SESSION: + request.SESSION["val"] = 0 + else: + request.SESSION["val"] += 1 + return HttpResponse(body=f"Session value: {request.SESSION['val']}") + +if __name__ == "__main__": + app.start() +``` + +If you drop this into a new file and call it with `python yourfile.py`, you should see two things: + +- there is a new file created called `spiderweb.db` +- if you open your browser and navigate to http://localhost:8000 and refresh the page a few times, the number should increment + +Use the session object to keep track of anything you need to! + +> Read more [about the database here!](../db.md) + +## Settings + +There are a few configurable things with the settings middleware, and they all have to do with the cookie itself. + +```python +app = SpiderwebRouter( + session_cookie_name="swsession", + session_cookie_secure=False, + session_cookie_http_only=True, + session_cookie_same_site="lax", + session_cookie_path="/", +) +``` + +### session_cookie_name + +Any valid cookie name is acceptable here; the default is `swsession`. You can [read more about valid names for cookies here][cookienames]. + +### session_cookie_secure + +This marks that the cookie will only be sent back to the server with a valid HTTPS session. By default, this is set to `False`, but should be manually set to `True` if the server is deployed. + +### session_cookie_http_only + +This marks whether the session cookie will have the `HttpOnly` attribute. This makes it invisible to client-side javascript. The default is `False`. + +### session_cookie_same_site + +There are three valid values for this: "strict", "lax", and "none". + +- `strict`: the browser will only send the cookie when the user performs a request on the same site that sent the cookie, and notably not on the first request to the server when navigating to the site from a different origin. +- `lax`: the browser will send the cookie when the user performs a request on the same site that sent the cookie, and also on the first request to the server when navigating to the site from a different origin. This is the default setting. +- `none`: the browser will send the cookie regardless of the origin of the request. However, you must also set `session_cookie_secure` to `True` if you want to use this setting, otherwise the browser will refuse to send it. + +### session_cookie_path + +This is the path that the cookie is valid for. By default, it's set to `/`, which means that the cookie is valid for the entire domain. If you want to restrict the cookie to a specific path, you can set it here. + +[cookienames]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes diff --git a/spiderweb/middleware/sessions.py b/spiderweb/middleware/sessions.py index c2979bd..6d96fe9 100644 --- a/spiderweb/middleware/sessions.py +++ b/spiderweb/middleware/sessions.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta import json -from peewee import CharField, TextField, DateTimeField, BooleanField +from peewee import CharField, TextField, DateTimeField from spiderweb.middleware import SpiderwebMiddleware from spiderweb.request import Request