Skip to content

Commit db98817

Browse files
authored
feat(aws): Set explicit inferring of AWS (#2)
1 parent daa2db0 commit db98817

File tree

4 files changed

+184
-17
lines changed

4 files changed

+184
-17
lines changed

README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,18 @@ const fastify = require('fastify')({
3939

4040
fastify.register(require('fastify-ip'), {
4141
order: ['x-my-ip-header'],
42-
strict: false
42+
strict: false,
43+
isAWS: false,
4344
})
4445
```
4546

4647
### Options
4748

4849
- `order` - `string[] | string` - **optional**: Array of custom headers or single custom header to be appended to the prior list of well-known headers. The headers passed will be prepend to the default headers list. It can also be used to alter the order of the list as deduplication of header names is made while loading the plugin.
4950

50-
- `strict` - `boolean` - **optional**: Indicates whether to override the default list of well-known headers and replace it with the header(s) passed through the `order` option. If set to `true` without `order` property being provided, will not take any effect on the plugin. Default `false`.
51+
- `strict` - `boolean` - **optional**: Indicates whether to override the default list of well-known headers and replace it with the header(s) passed through the `order` option. If set to `true` without `order` or `isAWS` properties provided, it will lead to throwing an exception. Default `false`.
52+
53+
- `isAWS` - `boolean` - **optional**: Indicates wether the plugin should explicitly try to infer the IP from the decorations made at the native Node.js HTTP Request object included in the Fastify Request. If set to `true` the plugin will treat this approach as a first option. Otherwise it will use it just as a fallback. Default `false`.
5154

5255

5356
### API
@@ -99,6 +102,7 @@ app.post('/', (request: FastifyRequest, reply: FastifyReply) => {
99102
export interface FastifyIPOptions {
100103
order?: string[] | string;
101104
strict?: boolean;
105+
isAWS?: boolean;
102106
}
103107

104108
declare module 'fastify' {

index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FastifyPluginCallback } from 'fastify';
44
export interface FastifyIPOptions {
55
order?: string[] | string;
66
strict?: boolean;
7+
isAWS?: boolean
78
}
89

910
declare module 'fastify' {

index.js

+40-14
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ const plugin = fp(fastifyIp, {
77
name: 'fastify-ip'
88
})
99

10-
function fastifyIp (instance, options, done) {
11-
const { order: inputOrder, strict } = options
10+
function fastifyIp (
11+
instance,
12+
{ order: inputOrder, strict, isAWS } = {
13+
order: null,
14+
strict: false,
15+
isAWS: false
16+
},
17+
done
18+
) {
1219
/*! Based on request-ip#https://github.com/pbojinov/request-ip/blob/9501cdf6e73059cc70fc6890adb086348d7cca46/src/index.js.
1320
MIT License. 2022 Petar Bojinov - [email protected] */
1421
// Default headers
@@ -27,19 +34,26 @@ function fastifyIp (instance, options, done) {
2734
'forwarded',
2835
'x-appengine-user-ip' // GCP App Engine
2936
]
37+
let error
3038

31-
if (inputOrder != null) {
39+
if (strict && inputOrder == null && !isAWS) {
40+
error = new Error('If strict provided, order or isAWS are mandatory')
41+
} else if (inputOrder != null) {
3242
if (Array.isArray(inputOrder) && inputOrder.length > 0) {
3343
headersOrder = strict
3444
? [].concat(inputOrder)
3545
: [...new Set([].concat(inputOrder, headersOrder))]
3646
} else if (typeof inputOrder === 'string' && inputOrder.length > 0) {
37-
headersOrder = strict ? [inputOrder] : (headersOrder.unshift(inputOrder), headersOrder)
47+
headersOrder = strict
48+
? [inputOrder]
49+
: (headersOrder.unshift(inputOrder), headersOrder)
3850
} else {
39-
done(new Error('invalid order option'))
51+
error = new Error('invalid order option')
4052
}
4153
}
4254

55+
if (error != null) return done(error)
56+
4357
// Utility methods
4458
instance.decorateRequest('isIP', isIP)
4559
instance.decorateRequest('isIPv4', isIPv4)
@@ -50,29 +64,41 @@ function fastifyIp (instance, options, done) {
5064
// Core method
5165
instance.decorateRequest('ip', {
5266
getter: function () {
53-
if (this._fastifyip !== '') return this._fastifyip
67+
let ip = this._fastifyip
68+
if (ip !== '') return ip
5469

55-
// AWS Api Gateway + Lambda
56-
if (this.raw.requestContext != null) {
57-
const pseudoIP = this.raw.requestContext.identity?.sourceIp
58-
if (pseudoIP != null && this.isIP(pseudoIP)) {
59-
this._fastifyip = pseudoIP
60-
}
61-
} else {
70+
// If is AWS context or the rules are not strict
71+
// infer first from AWS monkey-patching
72+
if (isAWS || !strict) {
73+
this._fastifyip = inferFromAWSContext.apply(this)
74+
ip = this._fastifyip
75+
}
76+
77+
// If is an AWS context, the rules are soft
78+
// or is not AWS context and the ip has not been
79+
// inferred yet, try using the request headers
80+
if (((isAWS && !strict) || !isAWS) && ip === '') {
6281
for (const headerKey of headersOrder) {
6382
const value = this.headers[headerKey]
6483
if (value != null && this.isIP(value)) {
6584
this._fastifyip = value
85+
ip = this._fastifyip
6686
break
6787
}
6888
}
6989
}
7090

71-
return this._fastifyip
91+
return ip
7292
}
7393
})
7494

7595
done()
96+
97+
// AWS Api Gateway + Lambda
98+
function inferFromAWSContext () {
99+
const pseudoIP = this.raw.requestContext?.identity?.sourceIp
100+
return pseudoIP != null && this.isIP(pseudoIP) ? pseudoIP : ''
101+
}
76102
}
77103

78104
module.exports = plugin

test/index.test.js

+137-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ tap.test('Plugin#Decoration', scope => {
112112
})
113113

114114
tap.test('Plugin#Request IP', scope => {
115-
scope.plan(6)
115+
scope.plan(8)
116116

117117
scope.test('Should infer the header based on default priority', async t => {
118118
const app = fastify()
@@ -139,6 +139,142 @@ tap.test('Plugin#Request IP', scope => {
139139
})
140140
})
141141

142+
scope.test('Fallback behavior on AWS Context', async t => {
143+
const app = fastify()
144+
const expectedIP = faker.internet.ip()
145+
const secondaryIP = faker.internet.ip()
146+
const childscope1 = (instance, _, done) => {
147+
instance.register(plugin, { isAWS: true, order: ['x-custom-remote-ip'] })
148+
149+
instance.get('/first', (req, reply) => {
150+
t.equal(req.ip, expectedIP)
151+
t.equal(req._fastifyip, expectedIP)
152+
153+
reply.send('')
154+
})
155+
156+
instance.get('/second', (req, reply) => {
157+
t.equal(req.ip, secondaryIP)
158+
t.equal(req._fastifyip, secondaryIP)
159+
160+
reply.send('')
161+
})
162+
163+
done()
164+
}
165+
const childscope2 = (instance, _, done) => {
166+
instance.register(plugin, { isAWS: true, strict: true })
167+
168+
instance.get('/', (req, reply) => {
169+
t.equal(req.ip, '')
170+
t.equal(req._fastifyip, '')
171+
172+
reply.send('')
173+
})
174+
175+
done()
176+
}
177+
const childscope3 = (instance, _, done) => {
178+
instance.register(plugin, { isAWS: true })
179+
180+
instance.get('/', (req, reply) => {
181+
t.equal(req.ip, expectedIP)
182+
t.equal(req._fastifyip, expectedIP)
183+
184+
reply.send('')
185+
})
186+
187+
instance.get('/none', (req, reply) => {
188+
t.equal(req.ip, '')
189+
t.equal(req._fastifyip, '')
190+
191+
reply.send('')
192+
})
193+
194+
done()
195+
}
196+
197+
t.plan(10)
198+
199+
app.register(childscope1, { prefix: '/fallback' })
200+
app.register(childscope2, { prefix: '/no-fallback' })
201+
app.register(childscope3, { prefix: '/soft' })
202+
203+
await app.inject({
204+
path: '/fallback/first',
205+
headers: {
206+
'x-custom-remote-ip': expectedIP,
207+
'x-forwarded-for': secondaryIP
208+
}
209+
})
210+
211+
await app.inject({
212+
path: '/fallback/second',
213+
headers: {
214+
'x-forwarded-for': secondaryIP
215+
}
216+
})
217+
218+
await app.inject({
219+
path: '/no-fallback',
220+
headers: {
221+
'x-custom-remote-ip': expectedIP,
222+
'x-forwarded-for': secondaryIP
223+
}
224+
})
225+
226+
await app.inject({
227+
path: '/soft',
228+
headers: {
229+
'x-appengine-user-ip': secondaryIP,
230+
'x-real-ip': expectedIP
231+
}
232+
})
233+
234+
await app.inject({
235+
path: '/soft/none'
236+
})
237+
})
238+
239+
scope.test('Should infer the header based on if is AWS context', async t => {
240+
const app = fastify()
241+
const expectedIP = faker.internet.ip()
242+
const fallbackIP = faker.internet.ipv6()
243+
244+
app.register(plugin, { isAWS: true })
245+
246+
app.get(
247+
'/',
248+
{
249+
preHandler: function (req, reply, done) {
250+
req.raw.requestContext = {
251+
identity: {
252+
sourceIp: expectedIP
253+
}
254+
}
255+
done()
256+
}
257+
},
258+
(req, reply) => {
259+
t.equal(req.ip, expectedIP)
260+
t.equal(req._fastifyip, expectedIP)
261+
262+
reply.send('')
263+
}
264+
)
265+
266+
t.plan(2)
267+
268+
await app.inject({
269+
path: '/',
270+
headers: {
271+
'cf-connecting-ip': fallbackIP,
272+
'x-client-ip': faker.internet.ipv6(),
273+
'x-custom-remote-ip': expectedIP
274+
}
275+
})
276+
})
277+
142278
scope.test(
143279
'Should infer the header based on custom priority <Array>',
144280
async t => {

0 commit comments

Comments
 (0)