Skip to content

Commit de1ea8f

Browse files
committed
fix(aws): ensure explicit support for AWS context
1 parent daa2db0 commit de1ea8f

File tree

2 files changed

+166
-15
lines changed

2 files changed

+166
-15
lines changed

index.js

Lines changed: 40 additions & 14 deletions
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

Lines changed: 126 additions & 1 deletion
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,131 @@ 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+
done()
188+
}
189+
190+
t.plan(8)
191+
192+
app.register(childscope1, { prefix: '/fallback' })
193+
app.register(childscope2, { prefix: '/no-fallback' })
194+
app.register(childscope3, { prefix: '/soft' })
195+
196+
await app.inject({
197+
path: '/fallback/first',
198+
headers: {
199+
'x-custom-remote-ip': expectedIP,
200+
'x-forwarded-for': secondaryIP
201+
}
202+
})
203+
204+
await app.inject({
205+
path: '/fallback/second',
206+
headers: {
207+
'x-forwarded-for': secondaryIP
208+
}
209+
})
210+
211+
await app.inject({
212+
path: '/no-fallback',
213+
headers: {
214+
'x-custom-remote-ip': expectedIP,
215+
'x-forwarded-for': secondaryIP
216+
}
217+
})
218+
219+
await app.inject({
220+
path: '/soft',
221+
headers: {
222+
'x-appengine-user-ip': secondaryIP,
223+
'x-real-ip': expectedIP
224+
}
225+
})
226+
})
227+
228+
scope.test('Should infer the header based on if is AWS context', async t => {
229+
const app = fastify()
230+
const expectedIP = faker.internet.ip()
231+
const fallbackIP = faker.internet.ipv6()
232+
233+
app.register(plugin, { isAWS: true })
234+
235+
app.get(
236+
'/',
237+
{
238+
preHandler: function (req, reply, done) {
239+
req.raw.requestContext = {
240+
identity: {
241+
sourceIp: expectedIP
242+
}
243+
}
244+
done()
245+
}
246+
},
247+
(req, reply) => {
248+
t.equal(req.ip, expectedIP)
249+
t.equal(req._fastifyip, expectedIP)
250+
251+
reply.send('')
252+
}
253+
)
254+
255+
t.plan(2)
256+
257+
await app.inject({
258+
path: '/',
259+
headers: {
260+
'cf-connecting-ip': fallbackIP,
261+
'x-client-ip': faker.internet.ipv6(),
262+
'x-custom-remote-ip': expectedIP
263+
}
264+
})
265+
})
266+
142267
scope.test(
143268
'Should infer the header based on custom priority <Array>',
144269
async t => {

0 commit comments

Comments
 (0)