Skip to content

Commit 949e8d4

Browse files
Merge pull request #23 from OtterJS/fix-proxied-stuff
Fix proxy stuff
2 parents 1e1c26b + a3b62f5 commit 949e8d4

17 files changed

+277
-146
lines changed

.changeset/weak-toes-refuse.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@otterhttp/proxy-address": patch
3+
"@otterhttp/forwarded": patch
4+
"@otterhttp/request": patch
5+
---
6+
7+
Fix: `req.ip`, `req.ips`, `req.proto` inaccuracies when (trusted) proxies are involved

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"eta": "2.2.0",
2020
"header-range-parser": "^1.1.3",
2121
"husky": "9.0.11",
22+
"ipaddr.js": "^2.2.0",
2223
"jsonwebtoken": "9.0.2",
2324
"regexparam": "3.0.0",
2425
"supertest-fetch": "1.5.0",

packages/forwarded/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@
2121
"build": "tsup",
2222
"prepack": "pnpm build"
2323
},
24-
"dependencies": {}
24+
"dependencies": {
25+
"ipaddr.js": "^2.2.0"
26+
}
2527
}

packages/forwarded/src/index.ts

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
1+
import assert from "node:assert/strict"
12
import type { IncomingMessage } from "node:http"
3+
import ipaddr, { type IPv6, type IPv4 } from "ipaddr.js"
4+
5+
/**
6+
* Type-guard to determine whether a parsed IP address is an IPv4 address.
7+
*/
8+
const isIPv4 = (value: IPv4 | IPv6): value is IPv4 => value.kind() === "ipv4"
9+
/**
10+
* Type-guard to determine whether a parsed IP address is an IPv6 address.
11+
*/
12+
const isIPv6 = (value: IPv4 | IPv6): value is IPv6 => value.kind() === "ipv6"
13+
14+
function parseIp(value: string): IPv4 | IPv6 {
15+
const ip = ipaddr.parse(value)
16+
if (isIPv6(ip) && ip.isIPv4MappedAddress()) return ip.toIPv4Address()
17+
return ip
18+
}
219

320
/**
421
* Get all addresses in the request, using the `X-Forwarded-For` header.
522
*/
6-
export function forwarded(req: Pick<IncomingMessage, "headers" | "socket">): Array<string | undefined> {
7-
// simple header parsing
8-
const proxyAddrs = parse((req.headers["x-forwarded-for"] as string) || "")
9-
const socketAddr = req.socket.remoteAddress
23+
export function* forwarded(req: Pick<IncomingMessage, "headers" | "socket">): Generator<IPv4 | IPv6 | undefined, void> {
24+
const socketAddress = req.socket.remoteAddress != null ? parseIp(req.socket.remoteAddress) : undefined
25+
yield socketAddress
1026

11-
// return all addresses
12-
return [socketAddr].concat(proxyAddrs)
27+
const xForwardedHeader = req.headers["x-forwarded-for"]
28+
if (!xForwardedHeader) return
29+
// https://github.com/nodejs/node/blob/58a7b0011a1858f4fde2fe553240153b39c13cd0/lib/_http_incoming.js#L381
30+
assert(!Array.isArray(xForwardedHeader))
31+
32+
yield* parse(xForwardedHeader)
1333
}
1434

1535
/**
16-
* Parse the X-Forwarded-For header.
36+
* Parse the X-Forwarded-For header, returning a {@link string} for each entry.
1737
*/
18-
export function parse(header: string): string[] {
38+
export function parseRaw(header: string): string[] {
1939
let end = header.length
2040
const list: string[] = []
2141
let start = header.length
@@ -45,3 +65,16 @@ export function parse(header: string): string[] {
4565

4666
return list
4767
}
68+
69+
/**
70+
* Parse the X-Forwarded-For header, returning an IP address (see `ipaddr.js` {@link IPv4}, {@link IPv6})
71+
* for each entry.
72+
*/
73+
export function* parse(header: string): Generator<IPv4 | IPv6, void> {
74+
const raw = parseRaw(header)
75+
for (const entry of raw) {
76+
yield parseIp(entry)
77+
}
78+
}
79+
80+
export type { IPv4, IPv6 }

packages/proxy-address/src/index.ts

+33-27
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ipaddr, { type IPv6, type IPv4 } from "ipaddr.js"
55
type Req = Pick<IncomingMessage, "headers" | "socket">
66

77
export type TrustParameter = string | number | string[]
8-
export type TrustFunction = (addr: string | undefined, i: number) => boolean
8+
export type TrustFunction = (addr: IPv4 | IPv6 | undefined, i: number) => boolean
99
export type Trust = TrustFunction | TrustParameter
1010

1111
type Subnet = {
@@ -14,8 +14,9 @@ type Subnet = {
1414
}
1515

1616
const DIGIT_REGEXP = /^[0-9]+$/
17-
const isip = ipaddr.isValid
18-
const parseip = ipaddr.parse
17+
const isIp = ipaddr.isValid
18+
const parseIp = ipaddr.parse
19+
1920
/**
2021
* Pre-defined IP ranges.
2122
*/
@@ -55,21 +56,23 @@ const trustNone: TrustFunction = () => false
5556
* @param req
5657
* @param trust
5758
*/
58-
function allAddresses(req: Req, trust?: Trust): Array<string | undefined> {
59+
function allAddresses(req: Req, trust?: Trust): Array<IPv4 | IPv6 | undefined> {
5960
// get addresses
61+
const addressGenerator = forwarded(req)
6062

61-
const addrs = forwarded(req)
62-
63-
if (trust == null) return addrs
64-
63+
if (trust == null) return Array.from(addressGenerator)
6564
if (typeof trust !== "function") trust = compile(trust)
6665

67-
for (let i = 0; i < addrs.length - 1; i++) {
68-
if (trust(addrs[i], i)) continue
69-
addrs.length = i + 1
66+
let i = 0
67+
const addresses: Array<IPv4 | IPv6 | undefined> = []
68+
for (const address of addressGenerator) {
69+
addresses.push(address)
70+
if (!trust(address, i)) break
71+
i += 1
7072
}
71-
return addrs
73+
return addresses
7274
}
75+
7376
/**
7477
* Compile argument into trust function.
7578
*
@@ -93,6 +96,7 @@ function compile(val: string | number | string[]): TrustFunction {
9396
}
9497
return compileTrust(compileRangeSubnets(trust))
9598
}
99+
96100
/**
97101
* Compile 'hops' number into trust function.
98102
*
@@ -108,6 +112,7 @@ function compileHopsTrust(hops: number): TrustFunction {
108112
function compileRangeSubnets(arr: string[]) {
109113
return arr.map((ip) => parseIPNotation(ip))
110114
}
115+
111116
/**
112117
* Compile range subnet array into trust function.
113118
*
@@ -118,6 +123,7 @@ function compileTrust(rangeSubnets: Subnet[]): TrustFunction {
118123
const len = rangeSubnets.length
119124
return len === 0 ? trustNone : len === 1 ? trustSingle(rangeSubnets[0]) : trustMulti(rangeSubnets)
120125
}
126+
121127
/**
122128
* Parse IP notation string into range subnet.
123129
*
@@ -128,9 +134,9 @@ export function parseIPNotation(note: string): Subnet {
128134
const pos = note.lastIndexOf("/")
129135
const str = pos !== -1 ? note.substring(0, pos) : note
130136

131-
if (!isip(str)) throw new TypeError(`invalid IP address: ${str}`)
137+
if (!isIp(str)) throw new TypeError(`invalid IP address: ${str}`)
132138

133-
let ip = parseip(str)
139+
let ip = parseIp(str)
134140
const max = ip.kind() === "ipv6" ? 128 : 32
135141

136142
if (pos === -1) {
@@ -142,43 +148,43 @@ export function parseIPNotation(note: string): Subnet {
142148
let range: number | null = null
143149

144150
if (DIGIT_REGEXP.test(rangeString)) range = Number.parseInt(rangeString, 10)
145-
else if (ip.kind() === "ipv4" && isip(rangeString)) range = parseNetmask(rangeString)
151+
else if (ip.kind() === "ipv4" && isIp(rangeString)) range = parseNetmask(rangeString)
146152

147153
if (range == null || range <= 0 || range > max) throw new TypeError(`invalid range on address: ${note}`)
148154
return { ip, range }
149155
}
156+
150157
/**
151158
* Parse netmask string into CIDR range.
152159
*
153160
* @param netmask
154161
* @private
155162
*/
156163
function parseNetmask(netmask: string) {
157-
const ip = parseip(netmask)
164+
const ip = parseIp(netmask)
158165
return ip.kind() === "ipv4" ? ip.prefixLengthFromSubnetMask() : null
159166
}
167+
160168
/**
161169
* Determine address of proxied request.
162170
*
163171
* @param req
164172
* @param trust
165173
* @public
166174
*/
167-
function proxyAddress(req: Req, trust: Trust): string | undefined {
175+
function proxyAddress(req: Req, trust: Trust): IPv4 | IPv6 | undefined {
168176
if (trust == null) throw new TypeError("trust argument cannot be null-ish")
169-
const addrs = allAddresses(req, trust)
177+
const addresses = allAddresses(req, trust)
170178

171-
return addrs[addrs.length - 1]
179+
return addresses[addresses.length - 1]
172180
}
173181

174182
/**
175183
* Compile trust function for multiple subnets.
176184
*/
177185
function trustMulti(subnets: Subnet[]): TrustFunction {
178-
return function trust(addr: string | undefined) {
179-
if (addr == null) return false
180-
if (!isip(addr)) return false
181-
const ip = parseip(addr)
186+
return function trust(ip: IPv4 | IPv6 | undefined) {
187+
if (ip == null) return false
182188
let ipconv: IPv4 | IPv6 | null = null
183189
const kind = ip.kind()
184190
for (let i = 0; i < subnets.length; i++) {
@@ -197,6 +203,7 @@ function trustMulti(subnets: Subnet[]): TrustFunction {
197203
return false
198204
}
199205
}
206+
200207
/**
201208
* Compile trust function for single subnet.
202209
*
@@ -205,10 +212,8 @@ function trustMulti(subnets: Subnet[]): TrustFunction {
205212
function trustSingle(subnet: Subnet): TrustFunction {
206213
const subnetKind = subnet.ip.kind()
207214
const subnetIsIPv4 = subnetKind === "ipv4"
208-
return function trust(addr: string | undefined) {
209-
if (addr == null) return false
210-
if (!isip(addr)) return false
211-
let ip = parseip(addr)
215+
return function trust(ip: IPv4 | IPv6 | undefined) {
216+
if (ip == null) return false
212217
const kind = ip.kind()
213218
if (kind !== subnetKind) {
214219
if (subnetIsIPv4 && !(ip as IPv6).isIPv4MappedAddress()) return false
@@ -220,3 +225,4 @@ function trustSingle(subnet: Subnet): TrustFunction {
220225
}
221226

222227
export { allAddresses, compile, proxyAddress }
228+
export type { IPv4, IPv6 }

packages/request/src/addresses.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { type Trust, allAddresses, proxyAddress } from "@otterhttp/proxy-address"
1+
import { type IPv4, type IPv6, type Trust, allAddresses } from "@otterhttp/proxy-address"
22

33
import type { HasHeaders, HasSocket } from "./types"
44

5-
export const getIP = (req: HasHeaders & HasSocket, trust: Trust): string | undefined =>
6-
proxyAddress(req, trust)?.replace(/^.*:/, "") // stripping the redundant prefix added by OS to IPv4 address
7-
8-
export const getIPs = (req: HasHeaders & HasSocket, trust: Trust): Array<string | undefined> => allAddresses(req, trust)
5+
export const getIPs = (req: HasHeaders & HasSocket, trust: Trust): Array<IPv4 | IPv6 | undefined> =>
6+
allAddresses(req, trust)

packages/request/src/host.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Trust } from "@otterhttp/proxy-address"
22

33
import { isIP } from "node:net"
44
import { getRequestHeader } from "./get-header"
5-
import type { HasHeaders, HasSocket } from "./types"
5+
import type { HasHeaders, HasIpAddresses, HasSocket } from "./types"
66
import { trustRemoteAddress } from "./util/trust-remote-address"
77

88
export type Host = {
@@ -38,7 +38,7 @@ const getDefaultHeaderHostString = (req: HasHeaders): string | undefined => {
3838
return normalizeHostString(host)
3939
}
4040

41-
const getHostString = (req: HasHeaders & HasSocket, trust: Trust): string | undefined => {
41+
const getHostString = (req: HasHeaders & HasIpAddresses, trust: Trust): string | undefined => {
4242
if (trustRemoteAddress(req, trust)) {
4343
const forwardedHost = getForwardedHeaderHostString(req)
4444
if (forwardedHost) return forwardedHost
@@ -56,7 +56,7 @@ const getHostString = (req: HasHeaders & HasSocket, trust: Trust): string | unde
5656
return authorityHost ?? defaultHost ?? undefined
5757
}
5858

59-
export const getHost = (req: HasHeaders & HasSocket, trust: Trust): Host => {
59+
export const getHost = (req: HasHeaders & HasIpAddresses, trust: Trust): Host => {
6060
const host = getHostString(req, trust)
6161
if (!host) throw new Error("Request does not include valid host information")
6262

packages/request/src/protocol.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TLSSocket } from "node:tls"
22
import type { Trust } from "@otterhttp/proxy-address"
33

4-
import type { HasHeaders, HasSocket } from "./types"
4+
import type { HasHeaders, HasIpAddresses, HasSocket } from "./types"
55
import { trustRemoteAddress } from "./util/trust-remote-address"
66

77
export type Protocol = "http" | "https" | string
@@ -10,12 +10,12 @@ const hasSecureConnection = (req: HasHeaders & HasSocket): boolean => {
1010
return req.socket instanceof TLSSocket && req.socket.encrypted
1111
}
1212

13-
export const getProtocol = (req: HasHeaders & HasSocket, trust: Trust): Protocol => {
13+
export const getProtocol = (req: HasHeaders & HasSocket & HasIpAddresses, trust: Trust): Protocol => {
1414
const proto = `http${hasSecureConnection(req) ? "s" : ""}`
1515

1616
if (!trustRemoteAddress(req, trust)) return proto
1717

18-
const header = (req.headers["X-Forwarded-Proto"] as string) || proto
18+
const header = (req.headers["x-forwarded-proto"] as string) || proto
1919

2020
const index = header.indexOf(",")
2121

packages/request/src/prototype.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { IncomingMessage } from "node:http"
22
import type { ParsedUrlQuery } from "node:querystring"
33
import { Accepts } from "@otterhttp/accepts"
44
import { ContentType } from "@otterhttp/content-type"
5-
import type { Trust } from "@otterhttp/proxy-address"
5+
import type { IPv4, IPv6, Trust } from "@otterhttp/proxy-address"
66
import type { Middleware } from "@otterhttp/router"
77
import { type URLParams, getQueryParams } from "@otterhttp/url"
88
import type { Result as RangeParseResult, Options as RangeParsingOptions, Ranges } from "header-range-parser"
99

10-
import { getIP, getIPs } from "./addresses"
10+
import { getIPs } from "./addresses"
1111
import { type Cookie, parseCookieHeader } from "./cookies"
1212
import { getRequestHeader } from "./get-header"
1313
import { getHost, getSubdomains } from "./host"
@@ -36,8 +36,8 @@ export class Request<Body = unknown> extends IncomingMessage {
3636
private declare _hostname: string
3737
private declare _port: number | undefined
3838
private declare _subdomains: string[]
39-
private declare _ip: string | undefined
40-
private declare _ips: (string | undefined)[]
39+
private declare _ip: IPv4 | IPv6 | undefined
40+
private declare _ips: (IPv4 | IPv6 | undefined)[]
4141

4242
// own members
4343
private _cookies?: Record<string, Cookie>
@@ -47,13 +47,13 @@ export class Request<Body = unknown> extends IncomingMessage {
4747
populate({ trust, subdomainOffset }: { trust: Trust; subdomainOffset: number | undefined }) {
4848
this._acceptsMeta = new Accepts(this)
4949
this._query = getQueryParams(this.url)
50+
this._ips = getIPs(this, trust)
51+
this._ip = this._ips[this._ips.length - 1]
5052
this._protocol = getProtocol(this, trust)
5153
const host = getHost(this, trust)
5254
this._hostname = host.hostname
5355
this._port = host.port
5456
this._subdomains = getSubdomains(host, subdomainOffset)
55-
this._ip = getIP(this, trust)
56-
this._ips = getIPs(this, trust)
5757
}
5858

5959
getHeader<HeaderName extends Lowercase<string>>(header: HeaderName): Headers[HeaderName] {
@@ -109,10 +109,10 @@ export class Request<Body = unknown> extends IncomingMessage {
109109
get subdomains(): readonly string[] {
110110
return this._subdomains
111111
}
112-
get ip(): string | undefined {
112+
get ip(): IPv4 | IPv6 | undefined {
113113
return this._ip
114114
}
115-
get ips(): readonly (string | undefined)[] {
115+
get ips(): readonly (IPv4 | IPv6 | undefined)[] {
116116
return this._ips
117117
}
118118
get xhr(): boolean {

packages/request/src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { IncomingHttpHeaders } from "node:http"
22
import type { Socket } from "node:net"
33

4+
import type { IPv4, IPv6 } from "@otterhttp/proxy-address"
5+
6+
export type HasIpAddresses = { ips: readonly (IPv4 | IPv6 | undefined)[] }
47
export type HasHeaders = { headers: IncomingHttpHeaders }
58
export type HasSocket = { socket: Socket }
69

0 commit comments

Comments
 (0)