Skip to content

Commit

Permalink
wip for #73 - request header parsing support
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydaly committed Dec 23, 2018
1 parent 3fce7ef commit c2e0f2f
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 6 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,9 @@ The `REQUEST` object contains a parsed and normalized request from API Gateway.
- `path`: The path passed in by the request including the `base` and any `prefix` assigned to routes
- `query`: Querystring parameters parsed into an object
- `multiValueQuery`: Querystring parameters with multiple values parsed into an object with array values
- `headers`: An object containing the request headers (properties converted to lowercase for HTTP/2, see [rfc7540 8.1.2. HTTP Header Fields](https://tools.ietf.org/html/rfc7540))
- `headers`: An object containing the request headers (properties converted to lowercase for HTTP/2, see [rfc7540 8.1.2. HTTP Header Fields](https://tools.ietf.org/html/rfc7540)). Note that multi-value headers are concatenated with a comma per [rfc2616 4.2. Message Headers](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
- `rawHeaders`: An object containing the original request headers (property case preserved)
- `multiValueHeaders`: An object containing header values as multi-value arrays
- `body`: The body of the request. If the `isBase64Encoded` flag is `true`, it will be decoded automatically.
- If the `content-type` header is `application/json`, it will attempt to parse the request using `JSON.parse()`
- If the `content-type` header is `application/x-www-form-urlencoded`, it will attempt to parse a URL encoded string using `querystring`
Expand Down
14 changes: 10 additions & 4 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,21 @@ class REQUEST {
.reduce((qs,key) => Object.assign(qs, { [key]: [this.query[key]] }), {}),
this.app._event.multiValueQueryStringParameters)

// Set the raw headers
this.rawHeaders = this.app._event.headers || {}
// this.rawHeaders = this._multiValueSupport ? this.app._event.multiValueHeaders
// : this.app._event.headers
// Set the raw headers (normalize multi-values)
// per https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
this.rawHeaders = this._multiValueSupport ?
Object.keys(this.app._event.multiValueHeaders).reduce((headers,key) =>
Object.assign(headers,{ [key]: UTILS.fromArray(this.app._event.multiValueHeaders[key]) }),{})
: this.app._event.headers || {}

// Set the headers to lowercase
this.headers = Object.keys(this.rawHeaders).reduce((acc,header) =>
Object.assign(acc,{[header.toLowerCase()]:this.rawHeaders[header]}), {})

this.multiValueHeaders = this._multiValueSupport ? this.app._event.multiValueHeaders
: Object.keys(this.headers).reduce((headers,key) =>
Object.assign(headers,{ [key.toLowerCase()]: [this.headers[key]] }),{})

// Extract user agent
this.userAgent = this.headers['user-agent']

Expand Down
4 changes: 4 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,7 @@ exports.deepMerge = (a,b) => {
this.deepMerge(a[key],b[key]) : Object.assign(a,b) )
return a
}

// Concats values from an array to ',' separated string
exports.fromArray = val =>
val && val instanceof Array ? val.toString() : undefined
34 changes: 34 additions & 0 deletions test/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('Request Tests:', function() {
let result = await new Promise(r => api.run(_event,_context,(e,res) => { r(res) }))
let body = JSON.parse(result.body)
// console.log(body);
// console.log(body.request.multiValueHeaders);
expect(result.headers).to.deep.equal({ 'content-type': 'application/json' })
expect(body).to.have.property('request')
expect(body.request.id).is.not.null
Expand All @@ -46,12 +47,15 @@ describe('Request Tests:', function() {
expect(body.request.query.qs2).to.equal('bar')
expect(body.request.multiValueQuery.qs2).to.deep.equal(['foo','bar'])
expect(body.request.multiValueQuery.qs3).to.deep.equal(['bat','baz'])
expect(body.request.headers['test-header']).to.equal('val1,val2')
expect(body.request.multiValueHeaders['test-header']).to.deep.equal(['val1','val2'])
})

it('Missing X-Forwarded-For (sourceIp fallback)', async function() {
let _event = require('./sample-event-apigateway1.json')
let _context = require('./sample-context-apigateway1.json')
delete _event.headers['X-Forwarded-For'] // remove the header
delete _event.multiValueHeaders['x-forwarded-for'] // remove the header
let result = await new Promise(r => api.run(_event,_context,(e,res) => { r(res) }))
let body = JSON.parse(result.body)
expect(result.headers).to.deep.equal({ 'content-type': 'application/json' })
Expand All @@ -71,6 +75,8 @@ describe('Request Tests:', function() {
expect(body.request.query.qs2).to.equal('bar')
expect(body.request.multiValueQuery.qs2).to.deep.equal(['foo','bar'])
expect(body.request.multiValueQuery.qs3).to.deep.equal(['bat','baz'])
expect(body.request.headers['test-header']).to.equal('val1,val2')
expect(body.request.multiValueHeaders['test-header']).to.deep.equal(['val1','val2'])
// console.log(body);
})
})
Expand All @@ -94,12 +100,40 @@ describe('Request Tests:', function() {
expect(body.request.clientCountry).to.equal('unknown')
expect(body.request.route).to.equal('/test/hello')
expect(body.request.query.qs1).to.equal('foo')
expect(body.request.multiValueQuery.qs1).to.deep.equal(['foo'])
console.log(body.request.multiValueHeaders)
// expect(body.request.query.qs2).to.equal('bar')
// expect(body.request.multiValueQuery.qs2).to.deep.equal(['foo','bar'])
// expect(body.request.multiValueQuery.qs3).to.deep.equal(['bat','baz'])

})


it('With multi-value support', async function() {
let _event = require('./sample-event-alb2.json')
let _context = require('./sample-context-alb1.json')
let result = await new Promise(r => api.run(_event,_context,(e,res) => { r(res) }))
let body = JSON.parse(result.body)
// console.log(body);
expect(result.headers).to.deep.equal({ 'content-type': 'application/json' })
expect(body).to.have.property('request')
expect(body.request.id).is.not.null
expect(body.request.interface).to.equal('alb')
expect(body.request.userAgent).to.equal('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48')
expect(body.request).to.have.property('requestContext')
expect(body.request.ip).to.equal('192.168.100.1')
expect(body.request.isBase64Encoded).to.equal(true)
expect(body.request.clientType).to.equal('unknown')
expect(body.request.clientCountry).to.equal('unknown')
expect(body.request.route).to.equal('/test/hello')
expect(body.request.query.qs1).to.equal('foo')
expect(body.request.multiValueQuery.qs1).to.deep.equal(['foo'])
expect(body.request.multiValueQuery.qs2).to.deep.equal(['foo','bar'])
expect(body.request.multiValueQuery.qs3).to.deep.equal(['foo','bar','bat'])
expect(body.request.headers['test-header']).to.equal('val1,val2')
expect(body.request.multiValueHeaders['test-header']).to.deep.equal(['val1','val2'])
})

})

}) // end Request tests
29 changes: 29 additions & 0 deletions test/sample-event-alb2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"requestContext": {
"elb": {
"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:XXXXXXXXXX:targetgroup/Test-ALB-Lambda/XXXXXXX"
}
},
"httpMethod": "GET",
"path": "/test/hello",
"multiValueQueryStringParameters": {
"qs1": [ "foo" ],
"qs2": [ "foo", "bar" ],
"qs3": [ "foo", "bar", "bat" ]
},
"multiValueHeaders": {
"accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],
"accept-encoding": ["br, gzip, deflate"],
"accept-language": ["en-us"],
"cookie": [""],
"host": ["wt6mne2s9k.execute-api.us-west-2.amazonaws.com"],
"user-agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48"],
"x-amzn-trace-id": ["Root=1-5c1db69f-XXXXXXXXXXX"],
"x-forwarded-for": ["192.168.100.1"],
"x-forwarded-port": ["443"],
"x-forwarded-proto": ["https"],
"test-header": ["val1","val2"]
},
"body": "",
"isBase64Encoded": true
}
22 changes: 21 additions & 1 deletion test/sample-event-apigateway1.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"multiValueHeaders": {
"accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"],
"accept-encoding": ["gzip, deflate, lzma, sdch, br"],
"accept-language": ["en-US,en;q=0.8"],
"cloudfront-forwarded-proto": ["https"],
"cloudfront-is-desktop-viewer": ["true"],
"cloudfront-is-mobile-viewer": ["false"],
"cloudfront-is-smarttv-viewer": ["false"],
"cloudfront-is-tablet-viewer": ["false"],
"cloudfront-viewer-country": ["US"],
"host": ["wt6mne2s9k.execute-api.us-west-2.amazonaws.com"],
"upgrade-insecure-requests": ["1"],
"user-agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48"],
"via": ["1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)"],
"x-amz-cf-id": ["nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g=="],
"x-forwarded-for": ["192.168.100.1, 192.168.1.1"],
"x-forwarded-port": ["443"],
"x-forwarded-proto": ["https"],
"test-header": ["val1","val2"]
},
"pathParameters": {
"proxy": "hello"
},
Expand Down Expand Up @@ -52,7 +72,7 @@
},
"multiValueQueryStringParameters": {
"qs2": [ "foo", "bar" ],
"qs3": [ "bat", "baz" ]
"qs3": [ "bat", "baz" ]
},
"stageVariables": {
"stageVarName": "stageVarValue"
Expand Down

0 comments on commit c2e0f2f

Please sign in to comment.