Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d51f380a3f
|
||
|
|
c5b653efba
|
||
|
|
f043ea4afe
|
||
|
|
628903afe0
|
||
|
|
b8f4e3026b
|
+2
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||||
⇒ {ip_addr}
|
{ip_addr}
|
||||||
|
|
||||||
$ curl {site}/ip
|
$ curl {site}/ip
|
||||||
⇒ {ip_addr}
|
{ip_addr}
|
||||||
|
|
||||||
$ curl {site}/ua
|
$ curl {site}/ua
|
||||||
⇒ {user_agent}
|
{user_agent}
|
||||||
|
|
||||||
$ curl {site}/lang
|
$ curl {site}/lang
|
||||||
⇒ {language}
|
{language}
|
||||||
|
|
||||||
$ curl {site}/encoding
|
$ curl {site}/encoding
|
||||||
⇒ {encoding}
|
{encoding}
|
||||||
|
|
||||||
$ curl {site}/mime
|
$ curl {site}/mime
|
||||||
⇒ {mime}
|
{mime}
|
||||||
|
|
||||||
$ curl {site}/charset
|
$ curl {site}/charset
|
||||||
⇒ {charset}
|
{charset}
|
||||||
|
|
||||||
$ curl {site}/forwarded
|
$ curl {site}/forwarded
|
||||||
⇒ {forwarded}
|
{forwarded}
|
||||||
|
|
||||||
$ curl {site}/all
|
$ curl {site}/all
|
||||||
⇒ 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
|
||||||
⇒ {all_json}</pre>
|
{all_json}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>© {year} {site}</footer>
|
<footer>© {year} {site}</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+12
-2
@@ -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"])
|
||||||
|
|||||||
Reference in New Issue
Block a user