Author SHA1 Message Date
woggioni d51f380a3f fixed styling
CI / Build and push docker image (push) Successful in 1m18s
2026-09-04 21:14:31 +08:00
woggioni-opencode-agent c5b653efba Upgrade to kaya 0.0.2 and run on the rloop event loop
CI / Build and push docker image (push) Successful in 1m39s
- pin kaya-core/kaya-rsgi >= 0.0.2 (ctx.client now honors Forwarded and
  X-Forwarded-* headers); adapt route tests and add explicit coverage
- add rloop dependency and set GRANIAN_LOOP=rloop in the Docker image so
  granian serves the app on the Rust event loop instead of asyncio
- regenerate requirements.txt (kaya 0.0.2, rloop 0.5.0)
- document the new loop and forwarded-header behavior in the README
2026-09-04 19:57:32 +08:00
woggioni f043ea4afe changed image repository name 2026-09-04 19:57:31 +08:00
woggioni-opencode-agent 628903afe0 Remove unused APP_HOST/APP_PORT settings 2026-09-04 19:57:31 +08:00
woggioni-opencode-agent b8f4e3026b Initial commit: ifconfig.me clone on the kaya framework 2026-09-04 19:57:31 +08:00
6 changed files with 91 additions and 30 deletions
+2 -1
View File
@@ -40,7 +40,8 @@ ENV PATH="/opt/venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
GRANIAN_HOST=0.0.0.0 \ GRANIAN_HOST=0.0.0.0 \
GRANIAN_PORT=8080 \ GRANIAN_PORT=8080 \
GRANIAN_INTERFACE=rsgi GRANIAN_INTERFACE=rsgi \
GRANIAN_LOOP=rloop
USER app USER app
+11 -1
View File
@@ -10,6 +10,7 @@ over the RSGI protocol.
- **kaya-core** — routing and HTTP request/response handling - **kaya-core** — routing and HTTP request/response handling
- **kaya-rsgi** — Granian (RSGI) adapter - **kaya-rsgi** — Granian (RSGI) adapter
- **granian** — application server - **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 - **httpx + pwo** — test client over kaya's ASGI transport
No database, sessions, or authentication — the app is stateless. 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`, `remote_host`, `user_agent`, `port`, `language`, `referer`, `connection`,
`keep_alive`, `method`, `encoding`, `mime`, `charset`, `via`, `forwarded`. `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: Example:
```console ```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 \ pip install --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple \
--extra-index-url https://pypi.org/simple \ --extra-index-url https://pypi.org/simple \
-e . -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 ### Configuration
Environment variables (see `.env.example`): Environment variables (see `.env.example`):
+3 -2
View File
@@ -9,11 +9,12 @@ description = "A clone of https://ifconfig.me/ built on the kaya framework"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"kaya-core", "kaya-core>=0.0.2",
"kaya-rsgi", "kaya-rsgi>=0.0.2",
"granian>=2.0", "granian>=2.0",
"httpx", "httpx",
"pwo", "pwo",
"rloop>=0.5.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
+4 -2
View File
@@ -29,17 +29,19 @@ idna==3.19
# via # via
# anyio # anyio
# httpx # httpx
kaya-core==0.0.1 kaya-core==0.0.2
# via # via
# kaya-rsgi # kaya-rsgi
# pyfconfig (pyproject.toml) # pyfconfig (pyproject.toml)
kaya-rsgi==0.0.1 kaya-rsgi==0.0.2
# via pyfconfig (pyproject.toml) # via pyfconfig (pyproject.toml)
pwo==0.1.2 pwo==0.1.2
# via # via
# kaya-core # kaya-core
# kaya-rsgi # kaya-rsgi
# pyfconfig (pyproject.toml) # pyfconfig (pyproject.toml)
rloop==0.5.0
# via pyfconfig (pyproject.toml)
typing-extensions==4.16.0 typing-extensions==4.16.0
# via # via
# anyio # anyio
+59 -22
View File
@@ -17,23 +17,57 @@ _PAGE = """<!DOCTYPE html>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>What Is My IP Address? - {site}</title> <title>What Is My IP Address? - {site}</title>
<style> <style>
body {{ font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; body {{
margin: 2rem auto; max-width: 52rem; padding: 0 1rem; color: #222; }} font-family: Arial, sans-serif;
h1 {{ font-size: 1.5rem; }} font-size: 13px;
h2 {{ font-size: 1.15rem; margin-top: 2rem; border-bottom: 1px solid #ddd; border: 1px solid black;
padding-bottom: .3rem; }} font-style: normal;
table {{ border-collapse: collapse; width: 100%; }} font-weight: normal;
td, th {{ border: 1px solid #ddd; padding: .4rem .6rem; text-align: left; margin: 2rem auto;
max-width: 52rem;
padding: 0 1rem;
color: #333;
}}
h1 a {{
font-size: 20px;
font-weight: 200;
margin: 10px;
letter-spacing: 1px;
text-decoration: none;
color: #555;
}}
h2 {{
height: 30px;
line-height: 30px;
font-size: 18px;
margin: 0 5px;
padding-left: 5px;
font-weight: 200;
}}
div.cli-wrap {{
padding-top: 20px;
}}
table {{ border-collapse: collapse; width: 100%; line-height: 20px; }}
td, th {{ border: 1px solid black; padding: .4rem .6rem; text-align: left;
vertical-align: top; }} vertical-align: top; }}
td:first-child {{ font-weight: 600; white-space: nowrap; width: 12rem; }} td:first-child {{ white-space: nowrap; width: 12rem; }}
code, pre {{ background: #f5f5f5; }} code, pre {{ background: #f5f5f5; margin: 0px; }}
pre {{ padding: .8rem; overflow-x: auto; border: 1px solid #e2e2e2; }} pre {{ padding: .8rem; overflow-x: auto; border: 1px solid #e2e2e2; overflow-x: hidden; }}
footer {{ margin-top: 3rem; color: #777; font-size: .85rem; }} footer {{
background: #777;
height: 20px;
line-height: 20px;
color: #ddd;
text-align: center;
margin-top: 15px;
border-top: none;
}}
</style> </style>
</head> </head>
<body> <body>
<h1>What Is My IP Address? - {site}</h1> <h1><a>What Is My IP Address? - {site}</a></h1>
<div>
<h2>Your Connection</h2> <h2>Your Connection</h2>
<table> <table>
<tr><td>IP Address</td><td>{ip_addr}</td></tr> <tr><td>IP Address</td><td>{ip_addr}</td></tr>
@@ -46,34 +80,36 @@ _PAGE = """<!DOCTYPE html>
<tr><td>Charset</td><td>{charset}</td></tr> <tr><td>Charset</td><td>{charset}</td></tr>
<tr><td>X-Forwarded-For</td><td>{forwarded}</td></tr> <tr><td>X-Forwarded-For</td><td>{forwarded}</td></tr>
</table> </table>
</div>
<div class="cli-wrap">
<h2>Command Line Interface</h2> <h2>Command Line Interface</h2>
<pre>$ curl {site} <pre>$ curl {site}
&#8658; {ip_addr} {ip_addr}
$ curl {site}/ip $ curl {site}/ip
&#8658; {ip_addr} {ip_addr}
$ curl {site}/ua $ curl {site}/ua
&#8658; {user_agent} {user_agent}
$ curl {site}/lang $ curl {site}/lang
&#8658; {language} {language}
$ curl {site}/encoding $ curl {site}/encoding
&#8658; {encoding} {encoding}
$ curl {site}/mime $ curl {site}/mime
&#8658; {mime} {mime}
$ curl {site}/charset $ curl {site}/charset
&#8658; {charset} {charset}
$ curl {site}/forwarded $ curl {site}/forwarded
&#8658; {forwarded} {forwarded}
$ curl {site}/all $ curl {site}/all
&#8658; ip_addr: {ip_addr} ip_addr: {ip_addr}
remote_host: {remote_host} remote_host: {remote_host}
user_agent: {user_agent} user_agent: {user_agent}
port: {port} port: {port}
@@ -89,7 +125,8 @@ $ curl {site}/all
forwarded: {forwarded} forwarded: {forwarded}
$ curl {site}/all.json $ curl {site}/all.json
&#8658; {all_json}</pre> {all_json}</pre>
</div>
<footer>&copy; {year} {site}</footer> <footer>&copy; {year} {site}</footer>
</body> </body>
+12 -2
View File
@@ -12,6 +12,10 @@ from pyfconfig.app import app
# httpx's ASGITransport populates the scope with this client tuple. # httpx's ASGITransport populates the scope with this client tuple.
CLIENT_IP = "127.0.0.1" CLIENT_IP = "127.0.0.1"
CLIENT_PORT = "123" 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 = { ALL_HEADERS = {
"User-Agent": "test-agent/1.0", "User-Agent": "test-agent/1.0",
@@ -48,6 +52,12 @@ class RoutesTest(unittest.TestCase):
self.assertEqual(200, r.status_code) self.assertEqual(200, r.status_code)
self.assertEqual(CLIENT_IP, r.text.strip()) 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_test
async def test_ua(self) -> None: async def test_ua(self) -> None:
async with self.client() as client: async with self.client() as client:
@@ -106,7 +116,7 @@ class RoutesTest(unittest.TestCase):
keys, keys,
) )
values = dict(line.split(": ", 1) for line in lines) 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("unavailable", values["remote_host"])
self.assertEqual("test-agent/1.0", values["user_agent"]) self.assertEqual("test-agent/1.0", values["user_agent"])
self.assertEqual(CLIENT_PORT, values["port"]) self.assertEqual(CLIENT_PORT, values["port"])
@@ -127,7 +137,7 @@ class RoutesTest(unittest.TestCase):
r = await client.get("/all.json", headers=ALL_HEADERS) r = await client.get("/all.json", headers=ALL_HEADERS)
self.assertEqual(200, r.status_code) self.assertEqual(200, r.status_code)
data = json.loads(r.text) 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("test-agent/1.0", data["user_agent"])
self.assertEqual(CLIENT_PORT, data["port"]) self.assertEqual(CLIENT_PORT, data["port"])
self.assertEqual("GET", data["method"]) self.assertEqual("GET", data["method"])