hono-ip 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +10 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# hono-ip
|
|
2
|
+
|
|
3
|
+
A tiny, fast middleware for [Hono](https://hono.dev) that figures out your client's real IP address - even when your app sits behind proxies, load balancers, or CDNs.
|
|
4
|
+
|
|
5
|
+
Works on **Node.js** and **Bun** out of the box. No regex. No dependencies.
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
Getting the "real" client IP in a web app is surprisingly annoying. Your server sees the proxy's address, not the user's. Different infrastructure stacks stuff the original IP into different headers - Cloudflare uses `CF-Connecting-IP`, AWS might use `X-Forwarded-For`, Fastly has its own thing, and so on.
|
|
10
|
+
|
|
11
|
+
This middleware checks all the common headers in a sensible order, validates every candidate with native `net.isIP` and hands you back a single, trustworthy string.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install hono-ip
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
import { ipMiddleware } from "hono-ip";
|
|
22
|
+
|
|
23
|
+
const app = new Hono();
|
|
24
|
+
|
|
25
|
+
app.use(ipMiddleware());
|
|
26
|
+
|
|
27
|
+
app.get("/", (c) => {
|
|
28
|
+
const ip = c.get("ip"); // string | null
|
|
29
|
+
return c.text(`Hello, ${ip ?? "stranger"}`);
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it. Every route after the middleware can read `c.get("ip")` or `c.var.ip`.
|
|
34
|
+
|
|
35
|
+
## Going deeper
|
|
36
|
+
|
|
37
|
+
### Trusted proxies
|
|
38
|
+
|
|
39
|
+
By default, the middleware returns the **leftmost** IP from `X-Forwarded-For` - the classic approach. The problem is that a malicious client can prepend whatever they want to that header:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
X-Forwarded-For: 6.6.6.6, <actual client>, <your proxy>
|
|
43
|
+
↑ attacker injected this
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If you know your proxy IPs, pass them in. The middleware will then walk `X-Forwarded-For` **from the right**, skipping trusted addresses, and return the first IP it doesn't recognise - which is the real client:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
app.use(
|
|
50
|
+
ipMiddleware({
|
|
51
|
+
trustedProxies: new Set(["10.0.0.1", "10.0.0.2"]),
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Runtime-level connection info
|
|
57
|
+
|
|
58
|
+
Headers can be spoofed. The one thing that _can't_ be faked is the TCP connection's remote address, which Hono exposes through its `getConnInfo` helper. Pass it in to use it as a final fallback:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Bun
|
|
62
|
+
import { getConnInfo } from "hono/bun";
|
|
63
|
+
|
|
64
|
+
// Node.js
|
|
65
|
+
// import { getConnInfo } from "@hono/node-server/conninfo";
|
|
66
|
+
|
|
67
|
+
app.use(ipMiddleware({ getConnInfo }));
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
When no header yields a valid IP, the middleware will call `getConnInfo(c).remote.address` and use that instead. If `getConnInfo` throws (e.g. during tests where there's no real server), it's caught silently.
|
|
71
|
+
|
|
72
|
+
### Using `getClientIp` directly
|
|
73
|
+
|
|
74
|
+
You don't have to use the middleware. The core function is exported on its own:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { getClientIp } from "hono-ip";
|
|
78
|
+
|
|
79
|
+
app.get("/ip", (c) => {
|
|
80
|
+
const ip = getClientIp(c, {
|
|
81
|
+
trustedProxies: new Set(["10.0.0.1"]),
|
|
82
|
+
});
|
|
83
|
+
return c.json({ ip });
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom context variable name
|
|
88
|
+
|
|
89
|
+
If `"ip"` collides with something in your app:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
app.use(ipMiddleware({ attributeName: "clientIp" }));
|
|
93
|
+
|
|
94
|
+
// later
|
|
95
|
+
c.get("clientIp");
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Resolution order
|
|
99
|
+
|
|
100
|
+
The middleware checks these sources top-to-bottom and returns the first valid IP it finds:
|
|
101
|
+
|
|
102
|
+
| Priority | Source | Notes |
|
|
103
|
+
|----------|--------|-------|
|
|
104
|
+
| 1 | `X-Client-IP` | Set by some proxies and load balancers |
|
|
105
|
+
| 2 | `X-Forwarded-For` | Leftmost valid IP, or rightmost untrusted if `trustedProxies` is set |
|
|
106
|
+
| 3 | `CF-Connecting-IP` | Cloudflare |
|
|
107
|
+
| 4 | `Fastly-Client-Ip` | Fastly |
|
|
108
|
+
| 5 | `True-Client-Ip` | Akamai, Cloudflare enterprise |
|
|
109
|
+
| 6 | `X-Real-IP` | Nginx default config |
|
|
110
|
+
| 7 | `X-Cluster-Client-IP` | Rackspace, Riverbed |
|
|
111
|
+
| 8 | `X-Forwarded` | Non-standard single-IP variant |
|
|
112
|
+
| 9 | `Forwarded-For` | Non-standard single-IP variant |
|
|
113
|
+
| 10 | `Forwarded` | RFC 7239 - parses `for="..."` value, handles bracketed IPv6 |
|
|
114
|
+
| 11 | `X-Appengine-User-Ip` | Google App Engine |
|
|
115
|
+
| 12 | `getConnInfo()` | Hono runtime adapter (Bun / Node / CF Workers / Deno / …) |
|
|
116
|
+
| 13 | `Cf-Pseudo-IPv4` | Cloudflare pseudo IPv4 for IPv6 visitors |
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
Apache-2.0
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{createMiddleware as U}from"hono/factory";import{isIp as T,getFirstFromXForwardedFor as V,parseForwarded as W}from"./ip";import{isIp as v,isIpV4 as y,isIpV6 as D,extractIp as H,getFirstFromXForwardedFor as M,parseForwarded as R}from"./ip";var Y=["x-client-ip","cf-connecting-ip","fastly-client-ip","true-client-ip","x-real-ip","x-cluster-client-ip"],Z=["x-appengine-user-ip","cf-pseudo-ipv4"];function J(b,u){let q=b.req.header(u);if(!q)return null;let z=q.trim();return T(z)?z:null}function $(b,u){let q=J(b,"x-client-ip");if(q)return q;let z=V(b.req.header("x-forwarded-for"),u);if(z)return z;for(let G of Y){if(G==="x-client-ip")continue;let j=J(b,G);if(j)return j}let N=J(b,"x-forwarded");if(N)return N;let O=J(b,"forwarded-for");if(O)return O;let Q=W(b.req.header("forwarded"));if(Q)return Q;for(let G of Z){let j=J(b,G);if(j)return j}if(u?.getConnInfo)try{let j=u.getConnInfo(b)?.remote?.address;if(j&&T(j))return j}catch{}return console.log("No IP found"),null}function P(b={}){let u=b.attributeName??"ip";return U(async(q,z)=>{q.set(u,$(q,b)),await z()})}export{R as parseForwarded,D as isIpV6,y as isIpV4,v as isIp,P as ipMiddleware,M as getFirstFromXForwardedFor,$ as getClientIp,H as extractIp};
|
|
2
|
+
|
|
3
|
+
//# debugId=5CD5D6EBA955279864756E2164756E21
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["..\\src\\index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { createMiddleware } from \"hono/factory\";\nimport type { Context } from \"hono\";\nimport {\n isIp,\n extractIp,\n getFirstFromXForwardedFor,\n parseForwarded,\n type XffOptions,\n} from \"./ip\";\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n ip: string | null;\n }\n}\n\n// ──────────────────────────────────────────────\n// Header priority (same order as request-ip):\n// 1. X-Client-IP\n// 2. X-Forwarded-For (leftmost valid, or rightmost untrusted)\n// 3. CF-Connecting-IP\n// 4. Fastly-Client-Ip\n// 5. True-Client-Ip\n// 6. X-Real-IP\n// 7. X-Cluster-Client-IP\n// 8. X-Forwarded / Forwarded-For / Forwarded (RFC 7239)\n// 9. X-Appengine-User-Ip\n// 10. Hono getConnInfo (Bun / Node / CF Workers / …)\n// 11. Cf-Pseudo-IPv4\n// ──────────────────────────────────────────────\n\nconst SIMPLE_HEADERS = [\n \"x-client-ip\",\n \"cf-connecting-ip\",\n \"fastly-client-ip\",\n \"true-client-ip\",\n \"x-real-ip\",\n \"x-cluster-client-ip\",\n] as const;\n\nconst FALLBACK_HEADERS = [\n \"x-appengine-user-ip\",\n \"cf-pseudo-ipv4\",\n] as const;\n\nfunction headerIp(c: Context, name: string): string | null {\n const v = c.req.header(name);\n if (!v) return null;\n const ip = v.trim();\n return isIp(ip) ? ip : null;\n}\n\nexport interface IpMiddlewareOptions extends XffOptions {\n attributeName?: string;\n\n getConnInfo?: (c: Context) => { remote: { address?: string } };\n}\n\nexport function getClientIp(c: Context, opts?: IpMiddlewareOptions): string | null {\n const xClientIp = headerIp(c, \"x-client-ip\");\n if (xClientIp) return xClientIp;\n\n const xff = getFirstFromXForwardedFor(c.req.header(\"x-forwarded-for\"), opts);\n if (xff) return xff;\n\n for (const name of SIMPLE_HEADERS) {\n if (name === \"x-client-ip\") continue;\n const ip = headerIp(c, name);\n if (ip) return ip;\n }\n\n const xForwarded = headerIp(c, \"x-forwarded\");\n if (xForwarded) return xForwarded;\n\n const forwardedFor = headerIp(c, \"forwarded-for\");\n if (forwardedFor) return forwardedFor;\n\n const forwarded = parseForwarded(c.req.header(\"forwarded\"));\n if (forwarded) return forwarded;\n\n for (const name of FALLBACK_HEADERS) {\n const ip = headerIp(c, name);\n if (ip) return ip;\n }\n\n if (opts?.getConnInfo) {\n try {\n const info = opts.getConnInfo(c);\n const addr = info?.remote?.address;\n if (addr && isIp(addr)) return addr;\n } catch {}\n }\n\n console.log(\"No IP found\");\n return null;\n}\n\nexport function ipMiddleware(opts: IpMiddlewareOptions = {}) {\n const key = (opts.attributeName ?? \"ip\") as \"ip\";\n\n return createMiddleware(async (c, next) => {\n c.set(key, getClientIp(c, opts));\n await next();\n });\n}\n\nexport { isIp, isIpV4, isIpV6, extractIp, getFirstFromXForwardedFor, parseForwarded } from \"./ip\";"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "AAAA,2BAAS,qBAET,eACE,+BAEA,oBACA,aAoGF,eAAS,YAAM,YAAQ,eAAQ,+BAAW,oBAA2B,aA3ErE,IAAM,EAAiB,CACrB,cACA,mBACA,mBACA,iBACA,YACA,qBACF,EAEM,EAAmB,CACvB,sBACA,gBACF,EAEA,SAAS,CAAQ,CAAC,EAAY,EAA6B,CACzD,IAAM,EAAI,EAAE,IAAI,OAAO,CAAI,EAC3B,GAAI,CAAC,EAAG,OAAO,KACf,IAAM,EAAK,EAAE,KAAK,EAClB,OAAO,EAAK,CAAE,EAAI,EAAK,KASlB,SAAS,CAAW,CAAC,EAAY,EAA2C,CACjF,IAAM,EAAY,EAAS,EAAG,aAAa,EAC3C,GAAI,EAAW,OAAO,EAEtB,IAAM,EAAM,EAA0B,EAAE,IAAI,OAAO,iBAAiB,EAAG,CAAI,EAC3E,GAAI,EAAK,OAAO,EAEhB,QAAW,KAAQ,EAAgB,CACjC,GAAI,IAAS,cAAe,SAC5B,IAAM,EAAK,EAAS,EAAG,CAAI,EAC3B,GAAI,EAAI,OAAO,EAGjB,IAAM,EAAa,EAAS,EAAG,aAAa,EAC5C,GAAI,EAAY,OAAO,EAEvB,IAAM,EAAe,EAAS,EAAG,eAAe,EAChD,GAAI,EAAc,OAAO,EAEzB,IAAM,EAAY,EAAe,EAAE,IAAI,OAAO,WAAW,CAAC,EAC1D,GAAI,EAAW,OAAO,EAEtB,QAAW,KAAQ,EAAkB,CACnC,IAAM,EAAK,EAAS,EAAG,CAAI,EAC3B,GAAI,EAAI,OAAO,EAGjB,GAAI,GAAM,YACR,GAAI,CAEF,IAAM,EADO,EAAK,YAAY,CAAC,GACZ,QAAQ,QAC3B,GAAI,GAAQ,EAAK,CAAI,EAAG,OAAO,EAC/B,KAAM,EAIV,OADA,QAAQ,IAAI,aAAa,EAClB,KAGF,SAAS,CAAY,CAAC,EAA4B,CAAC,EAAG,CAC3D,IAAM,EAAO,EAAK,eAAiB,KAEnC,OAAO,EAAiB,MAAO,EAAG,IAAS,CACzC,EAAE,IAAI,EAAK,EAAY,EAAG,CAAI,CAAC,EAC/B,MAAM,EAAK,EACZ",
|
|
8
|
+
"debugId": "5CD5D6EBA955279864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hono-ip",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tiny, fast middleware for Hono that figures out your client's real IP address - even when your app sits behind proxies, load balancers, or CDNs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"hono",
|
|
7
|
+
"ip",
|
|
8
|
+
"middleware"
|
|
9
|
+
],
|
|
10
|
+
"module": "dist/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "bun run build.ts"
|
|
23
|
+
},
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"author": "orielhaim",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/orielhaim/hono-ip"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bun": "^1.3.9"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"hono": "^4.12.3"
|
|
35
|
+
}
|
|
36
|
+
}
|