Metadata-Version: 2.4
Name: kaya-forwarded
Version: 0.0.4
Summary: Trusted-proxy forwarded header handling for the Kaya lightweight ASGI web framework
Author-email: Walter Oggioni <oggioni.walter@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/woggioni/kaya
Project-URL: Bug Tracker, https://github.com/woggioni/kaya/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Topic :: Utilities
Classifier: Intended Audience :: System Administrators
Classifier: Intended Audience :: Developers
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: kaya-core
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: ipdb; extra == "dev"
Requires-Dist: twine; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Requires-Dist: httpx-ws; extra == "dev"
Requires-Dist: kaya-rsgi; extra == "dev"

# kaya-forwarded

Trusted-proxy forwarded header handling for the Kaya web framework.

Without this package, Kaya exposes the raw socket peer address as
`ctx.client` / `ws.client` and ignores `Forwarded` / `X-Forwarded-*` headers
entirely (they are client-controllable and trivially spoofable when the app is
directly exposed).

`ForwardedHeadersMixin` opts the application into honoring those headers, but
only when the direct socket peer is a trusted proxy, identified by a list of
trusted CIDRs/IPs.

## Usage

```python
from kaya.core import KayaApp, HttpContext
from kaya.forwarded import ForwardedHeadersMixin

app = KayaApp(mixins=[
    ForwardedHeadersMixin(trusted_proxies=['127.0.0.1', '::1', '10.0.0.0/8'])
])

@app.GET('/whoami')
async def whoami(ctx: HttpContext):
    host, port = ctx.client
    await ctx.send_str(200, f'{host}:{port}')
```

## How it works

When a request arrives:

1. If the socket peer IP does not belong to any trusted CIDR (or there is no
   peer address), the mixin leaves the context untouched — `client` remains
   the socket peer and all proxy headers are ignored.
2. Otherwise the client address is resolved from the headers, in order:
   - `Forwarded` (RFC 7239): the `for=` entries are walked **from right to
     left**, skipping entries that are themselves trusted proxies (and
     `unknown`); the first untrusted entry is the client. This defeats
     spoofing when the edge proxy *appends* to the header (e.g. nginx with
     `$proxy_add_x_forwarded_for`), because attacker-supplied leftmost entries
     are never selected. A `:port` in the selected `for=` value also
     populates the port.
   - `X-Forwarded-For`: same right-to-left trusted-proxy walk; the port comes
     from `X-Forwarded-Port` when present and valid.
   - `X-Forwarded-Host`: first entry; port from `X-Forwarded-Port` as above.
3. If none of the headers are present or usable, the socket peer is kept.

If every entry in the chain is a trusted proxy, the leftmost entry is used
(the whole chain is trusted, so the leftmost is the original client).

The resolved address is exposed by wrapping the request context /
websocket (the same pattern as `kaya-session`), so both ASGI and RSGI keep
working and `ctx.session` from other mixins is preserved.

## Note

Even with this mixin, the edge proxy should still strip or overwrite inbound
`Forwarded` / `X-Forwarded-*` headers from clients — the mixin protects the
application, the proxy protects the chain.
