Skip to content

Commit

Permalink
[middleware] Implement middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
nhairs committed Dec 18, 2023
1 parent 620d87c commit 76531a5
Show file tree
Hide file tree
Showing 10 changed files with 897 additions and 117 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ NServer is a Python framework for building customised DNS name servers with a fo

## Documentation

[Documentation](https://nhairs.github.io/nserver/latest/)

[Quickstart Guide](https://nhairs.github.io/nserver/latest/quickstart/)
- [Documentation](https://nhairs.github.io/nserver/latest/)
- [Quickstart Guide](https://nhairs.github.io/nserver/latest/quickstart/)
- [Change Log](https://nhairs.github.io/nserver/latest/changelog/)


## Licence
Expand Down
10 changes: 10 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Change Log

## 1.1.0

- Implement [Middleware][middleware]
- This includes adding error handling middleware that facilitates [error handling][error-handling].

## 1.0.0

- Beta release
34 changes: 34 additions & 0 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Error Handling

Custom exception handling is handled through the [`ExceptionHandlerMiddleware`][nserver.middleware.ExceptionHandlerMiddleware] and [`RawRecordExceptionHandlerMiddleware`][nserver.middleware.RawRecordExceptionHandlerMiddleware] [Middleware][middleware]. These middleware will catch any `Exception`s raised by their respective middleware stacks.

Error handling requires `nserver>=1.1.0`

In general you are probably able to use the `ExceptionHandlerMiddleware` as the `RawRecordExceptionHandlerMiddleware` is only needed to catch exceptions resulting from `RawRecordMiddleware` or broken exception handlers in the `ExceptionHandlerMiddleware`. If you only write `QueryMiddleware` and your `ExceptionHandlerMiddleware` handlers never raise exceptions then you'll be good to go with just the `ExceptionHandlerMiddleware`.

Both of these middleware have a default exception handler that will be used for anything not matching a registered handler. The default handler can be overwritten by registering a handler for the `Exception` class.

Handlers are chosen by finding a handler for the most specific parent class of the thrown exception (including the class of the exception). These classes are searched in method resolution order.

!!! note
These handlers only handle exceptions that are subclasses of (and including) `Exception`. Exceptions that are only children of `BaseException` (e.g. `SystemExit`) will not be caught by these handlers.

## Registering Exception Handlers

```python
import dnslib
from nserver import NameServer, Query, Response

server = NameServer("example")

@server.exception_handler(NotImplementedError)
def not_implemented_handler(exception: NotImplementedError, query: Query) -> Response:
return Response(error_code=dnslib.RCODE.NOTIMPL)

@server.raw_exception_handler(Exception)
def print_debugger(exception: Exception, record: dnslib.DNSRecord) -> dnslib.DNSRecord:
print(f"failed to process {record} due to {exception!r})
response = record.reply()
response.header.rcode =dnslib.RCODE.SERVFAIL
return response
```
123 changes: 123 additions & 0 deletions docs/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Middleware

Middleware can be used to modify the behaviour of a server seperate to the individual rules that are registered to the server. Middleware is run on all requests and can modify both the input and response of a request.

Middleware requires `nserver>=1.1.0`

## Middleware Stacks

Middleware operates in a stack with each middleware calling the middleware below it until one returns and the result is propagated back up the chain. NServer uses two stacks, the outmost stack deals with raw DNS records (`RawRecordMiddleware`), which will eventually convert the record to a `Query` which will then be passed to the main `QueryMiddleware` stack.

Middleware can be added to the application until it is run. Once the server begins running the middleware cannot be modified. The ordering of middleware is kept in the order in which it is added to the server; that is the first middleware registered will be called before the second and so on.

Some middleware is automatically added when the stacks are processed.

## `QueryMiddleware`

For most use cases you likely want to use [`QueryMiddleware`][nserver.middleware.QueryMiddleware]. This middleware uses the high-level `Query` and `Response` objects.

### Registering `QueryMiddleware`

```python
from nserver import NameServer
from nserver.middleware import QueryMiddleware

server = NameServer("example")
server.register_middleware(QueryMiddleware())
```

### Creating your own `QueryMiddleware`

Using an unmodified `QueryMiddleware` isn't very interesting as it just passes the request onto the next middleware. To add your own middleware you should subclass `QueryMiddleware` and override the `process_query` method.

```python
# ...
from typing import Callable
from nserver import Query, Response

class MyLoggingMiddleware(QueryMiddleware):
def __init__(self, logging_name: str):
super().__init__()
self.logger = logging.getLogger(f"my-awesome-app.{name}")
return

def process_query(
query: Query, call_next: Callable[[Query], Response]
) -> Response:
self.logger.info(f"processing {query.name}")
response = call_next(query)
self.logger.info(f"done processing, returning {response.error_code}")
return response

server.register_middleware(MyLoggingMiddleware("foo"))
server.register_middleware(MyLoggingMiddleware("bar"))
```

### Default `QueryMiddleware` stack

Once processed the `QueryMiddleware` stack will look as follows:

- [`ExceptionHandlerMiddleware`][nserver.middleware.ExceptionHandlerMiddleware]
- Customisable error handler for `Exception`s originating from within the stack.
- `<registered middleware>`
- [`HookMiddleware`][nserver.middleware.HookMiddleware]
- Runs hooks registered to the server. This can be considered a simplified version of middleware.
- [`RuleProcessor`][nserver.middleware.RuleProcessor]
- The entry point into our rule processing.


## `RawRecordMiddleware`

[`RawRecordMiddleware`][nserver.middleware.RawRecordMiddleware] allows for modifying the raw `dnslib.DNSRecord`s that are recevied and sent by the server.

### Registering `RawRecordMiddleware`

```python
# ...
from nserver.middleware import RawRecordMiddleware

server.register_raw_middleware(RawRecordMiddleware())
```

### Creating your own `RawRecordMiddleware`

Using an unmodified `RawRecordMiddleware` isn't very interesting as it just passes the request onto the next middleware. To add your own middleware you should subclass `RawRecordMiddleware` and override the `process_record` method.

```python
# ...

class SizeLimiterMiddleware(RawRecordMiddleware):
def __init__(self, max_size: int):
super().__init__()
self.max_size = max_size
return

def process_record(
record: dnslib.DNSRecord,
call_next: Callable[[dnslib.DNSRecord], dnslib.DNSRecord],
) -> dnslib.DNSRecord:
refused = record.reply()
refused.header.rcode = dnslib.RCODE.REFUSED

if len(record.pack()) > self.max_size:
return refused

response = call_next(query)

if len(response.pack()) > self.max_size:
return refused

return response

server.register_raw_middleware(SizeLimiterMiddleware(1400))
```

### Default `RawRecordMiddleware` stack

Once processed the `RawRecordMiddleware` stack will look as follows:

- [`RawRecordExceptionHandlerMiddleware`][nserver.middleware.RawRecordExceptionHandlerMiddleware]
- Customisable error handler for `Exception`s originating from within the stack.
- `<registered raw middleware>`
- [`QueryMiddlewareProcessor`][nserver.middleware.QueryMiddlewareProcessor]
- entry point into the `QueryMiddleware` stack.
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ watch:
nav:
- "Home": index.md
- quickstart.md
- middleware.md
- error-handling.md
- production-deployment.md
- changelog.md
- external-resources.md
- API Reference:
- ... | reference/nserver/*
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "nserver"
version = "1.0.1"
version = "1.1.0.dev1"
description = "DNS Name Server Framework"
authors = [
{name = "Nicholas Hairs", email = "[email protected]"},
Expand Down
Loading

0 comments on commit 76531a5

Please sign in to comment.