diff --git a/Dockerfile b/Dockerfile index e95f0a3..9b6193d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,8 @@ ENV PATH="/opt/venv/bin:$PATH" \ PYTHONDONTWRITEBYTECODE=1 \ GRANIAN_HOST=0.0.0.0 \ GRANIAN_PORT=8080 \ - GRANIAN_INTERFACE=rsgi + GRANIAN_INTERFACE=rsgi \ + GRANIAN_LOOP=rloop USER app diff --git a/README.md b/README.md index 17daad4..d9c6d2e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ over the RSGI protocol. - **kaya-core** — routing and HTTP request/response handling - **kaya-rsgi** — Granian (RSGI) adapter - **granian** — application server +- **rloop** — Rust event loop used by Granian instead of the stdlib asyncio loop - **httpx + pwo** — test client over kaya's ASGI transport No database, sessions, or authentication — the app is stateless. @@ -34,6 +35,10 @@ The `/all` field order mirrors the reference site: `ip_addr`, `remote_host`, `user_agent`, `port`, `language`, `referer`, `connection`, `keep_alive`, `method`, `encoding`, `mime`, `charset`, `via`, `forwarded`. +The reported client IP/port honor the `Forwarded`, `X-Forwarded-For`, +`X-Forwarded-Host` and `X-Forwarded-Port` proxy headers (kaya ≥ 0.0.2); +without them the socket peer address is used. + Example: ```console @@ -60,9 +65,14 @@ python -m venv .venv && . .venv/bin/activate pip install --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple \ --extra-index-url https://pypi.org/simple \ -e . -granian --interface rsgi --host 0.0.0.0 --port 8000 pyfconfig.app:app +granian --interface rsgi --loop rloop --host 0.0.0.0 --port 8000 pyfconfig.app:app ``` +The `--loop rloop` flag (or `GRANIAN_LOOP=rloop`, already set in the +Docker image) makes Granian run the app on the +[rloop](https://github.com/gi0baro/rloop) Rust event loop instead of the +stdlib asyncio loop. + ### Configuration Environment variables (see `.env.example`): diff --git a/pyproject.toml b/pyproject.toml index 56dc518..ea0057f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,12 @@ description = "A clone of https://ifconfig.me/ built on the kaya framework" readme = "README.md" requires-python = ">=3.10" dependencies = [ - "kaya-core", - "kaya-rsgi", + "kaya-core>=0.0.2", + "kaya-rsgi>=0.0.2", "granian>=2.0", "httpx", "pwo", + "rloop>=0.5.0", ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index 4299bc8..6613247 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,17 +29,19 @@ idna==3.19 # via # anyio # httpx -kaya-core==0.0.1 +kaya-core==0.0.2 # via # kaya-rsgi # pyfconfig (pyproject.toml) -kaya-rsgi==0.0.1 +kaya-rsgi==0.0.2 # via pyfconfig (pyproject.toml) pwo==0.1.2 # via # kaya-core # kaya-rsgi # pyfconfig (pyproject.toml) +rloop==0.5.0 + # via pyfconfig (pyproject.toml) typing-extensions==4.16.0 # via # anyio diff --git a/tests/test_routes.py b/tests/test_routes.py index 28e7bd7..5852d31 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -12,6 +12,10 @@ from pyfconfig.app import app # httpx's ASGITransport populates the scope with this client tuple. CLIENT_IP = "127.0.0.1" CLIENT_PORT = "123" +# First entry of the X-Forwarded-For header in ALL_HEADERS; since kaya 0.0.2 +# ctx.client honors forwarded headers, ip_addr resolves to this instead of +# the socket peer address. +FORWARDED_IP = "203.0.113.7" ALL_HEADERS = { "User-Agent": "test-agent/1.0", @@ -48,6 +52,12 @@ class RoutesTest(unittest.TestCase): self.assertEqual(200, r.status_code) self.assertEqual(CLIENT_IP, r.text.strip()) + @async_test + async def test_ip_honors_x_forwarded_for(self) -> None: + async with self.client() as client: + r = await client.get("/ip", headers={"X-Forwarded-For": "203.0.113.7"}) + self.assertEqual("203.0.113.7", r.text.strip()) + @async_test async def test_ua(self) -> None: async with self.client() as client: @@ -106,7 +116,7 @@ class RoutesTest(unittest.TestCase): keys, ) values = dict(line.split(": ", 1) for line in lines) - self.assertEqual(CLIENT_IP, values["ip_addr"]) + self.assertEqual(FORWARDED_IP, values["ip_addr"]) self.assertEqual("unavailable", values["remote_host"]) self.assertEqual("test-agent/1.0", values["user_agent"]) self.assertEqual(CLIENT_PORT, values["port"]) @@ -127,7 +137,7 @@ class RoutesTest(unittest.TestCase): r = await client.get("/all.json", headers=ALL_HEADERS) self.assertEqual(200, r.status_code) data = json.loads(r.text) - self.assertEqual(CLIENT_IP, data["ip_addr"]) + self.assertEqual(FORWARDED_IP, data["ip_addr"]) self.assertEqual("test-agent/1.0", data["user_agent"]) self.assertEqual(CLIENT_PORT, data["port"]) self.assertEqual("GET", data["method"])