Skip to content

Commit 8b922ac

Browse files
committedAug 22, 2022
Normalize headers to lowercase in middleware
1 parent fece771 commit 8b922ac

10 files changed

+111
-50
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -206,5 +206,6 @@ $RECYCLE.BIN/
206206
*.lnk
207207

208208
samconfig.toml
209+
env.json
209210

210211
# End of https://www.toptal.com/developers/gitignore/api/osx,node,linux,windows,sam

‎src/handlers/get-collection-by-id.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
const middleware = require("./middleware");
12
const { getCollection } = require("../api/opensearch");
23
const opensearchResponse = require("../api/response/opensearch");
34

45
/**
56
* A simple function to get a Collection by id
67
*/
78
exports.handler = async (event) => {
9+
event = middleware(event);
810
const id = event.pathParameters.id;
911
let esResponse = await getCollection(id);
1012
return opensearchResponse.transform(esResponse);

‎src/handlers/get-file-set-by-id.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
const middleware = require("./middleware");
12
const { getFileSet } = require("../api/opensearch");
23
const opensearchResponse = require("../api/response/opensearch");
34

45
/**
56
* A simple function to get a FileSet by id
67
*/
78
exports.handler = async (event) => {
9+
event = middleware(event);
810
const id = event.pathParameters.id;
911
let esResponse = await getFileSet(id);
1012
return opensearchResponse.transform(esResponse);

‎src/handlers/get-work-by-id.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
const middleware = require("./middleware");
12
const { getWork } = require("../api/opensearch");
23
const opensearchResponse = require("../api/response/opensearch");
34

45
/**
56
* A simple function to get a Work by id
67
*/
78
exports.handler = async (event) => {
9+
event = middleware(event);
810
const id = event.pathParameters.id;
911
let esResponse = await getWork(id);
1012
return opensearchResponse.transform(esResponse);

‎src/handlers/middleware.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
const base64 = require("base64-js");
1+
const { decodeEventBody, normalizeHeaders } = require("../helpers");
22

3-
function decodeEventBody(event) {
4-
if (!event.isBase64Encoded) return event;
5-
event.body = new Buffer.from(base64.toByteArray(event.body)).toString();
6-
event.isBase64Encoded = false;
7-
return event;
3+
function prepareEvent(event) {
4+
let result = normalizeHeaders(event);
5+
result = decodeEventBody(result);
6+
return result;
87
}
98

10-
module.exports = { decodeEventBody };
9+
module.exports = prepareEvent;

‎src/handlers/search.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { decodeEventBody } = require("./middleware");
1+
const middleware = require("./middleware");
22
const { baseUrl } = require("../helpers");
33
const { modelsToTargets, validModels } = require("../api/request/models");
44
const { search } = require("../api/opensearch");
@@ -7,6 +7,7 @@ const { decodeSearchToken, Paginator } = require("../api/pagination");
77
const RequestPipeline = require("../api/request/pipeline");
88

99
const getSearch = async (event) => {
10+
event = middleware(event);
1011
let token = event.queryStringParameters.searchToken;
1112
if (token === undefined || token === "") {
1213
return invalidRequest("searchToken parameter is required");
@@ -26,7 +27,7 @@ const getSearch = async (event) => {
2627
};
2728

2829
const postSearch = async (event) => {
29-
event = decodeEventBody(event);
30+
event = middleware(event);
3031
const eventBody = JSON.parse(event.body);
3132

3233
const requestedModels =

‎src/helpers.js

+30-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
const base64 = require("base64-js");
12
const gatewayRe = /execute-api.[a-z]+-[a-z]+-\d+.amazonaws.com/;
23

4+
function decodeEventBody(event) {
5+
if (!event.isBase64Encoded) return event;
6+
event.body = new Buffer.from(base64.toByteArray(event.body)).toString();
7+
event.isBase64Encoded = false;
8+
return event;
9+
}
10+
311
function isApiGateway(event) {
412
return gatewayRe.test(event.requestContext.domainName);
513
}
@@ -12,19 +20,35 @@ function isLocal(event) {
1220
return event.requestContext.domainName === "localhost";
1321
}
1422

15-
function getHeader(event, name) {
16-
return event.headers[name] || event.headers[name.toLowerCase()];
23+
function normalizeHeaders(event) {
24+
if (event.normalizedHeaders) return event;
25+
26+
const headers = { ...event.headers };
27+
28+
for (header in headers) {
29+
const lowerHeader = header.toLowerCase();
30+
if (header != lowerHeader) {
31+
const value = headers[header];
32+
delete headers[header];
33+
headers[lowerHeader] = value;
34+
}
35+
}
36+
37+
event.headers = headers;
38+
event.normalizedHeaders = true;
39+
return event;
1740
}
1841

1942
function baseUrl(event) {
20-
const scheme = getHeader(event, "X-Forwarded-Proto");
43+
event = normalizeHeaders(event);
44+
const scheme = event.headers["x-forwarded-proto"];
2145

2246
// The localhost check only matters in dev mode, but it's
2347
// really inconvenient not to have it
2448
const host = isLocal(event)
25-
? getHeader(event, "Host").split(/:/)[0]
49+
? event.headers["host"].split(/:/)[0]
2650
: event.requestContext.domainName;
27-
const port = getHeader(event, "X-Forwarded-Port");
51+
const port = event.headers["x-forwarded-port"];
2852

2953
let result = new URL(`${scheme}://${host}:${port}`);
3054

@@ -45,4 +69,4 @@ function baseUrl(event) {
4569
return result.toString();
4670
}
4771

48-
module.exports = { baseUrl, getHeader };
72+
module.exports = { baseUrl, decodeEventBody, normalizeHeaders };

‎test/integration/search.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe("Search routes", () => {
5757
.pathParams({ models: "works,collections,blargh" })
5858
.body(originalQuery)
5959
.render();
60+
6061
const result = await handler(event);
6162
expect(result.statusCode).to.eq(400);
6263

‎test/test-helpers/event-builder.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = class {
44
constructor(method, route) {
55
const now = new Date();
66
this._method = method;
7+
this._pathPrefix = "";
78
this._route = route;
89
this._event = {
910
version: "2.0",
@@ -50,10 +51,15 @@ module.exports = class {
5051

5152
body(body) {
5253
switch (typeof body) {
54+
case "string":
55+
this._event.body = body;
56+
break;
5357
case "undefined":
54-
this._event.body = {};
58+
this._event.body = "";
59+
break;
5560
case "object":
5661
this._event.body = JSON.stringify(body);
62+
break;
5763
}
5864
return this;
5965
}

‎test/unit/api/helpers.test.js

+57-34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"use strict";
22

3-
const { baseUrl, getHeader } = require("../../../src/helpers");
3+
const {
4+
baseUrl,
5+
decodeEventBody,
6+
normalizeHeaders,
7+
} = require("../../../src/helpers");
48
const chai = require("chai");
59
const expect = chai.expect;
610

@@ -11,9 +15,9 @@ describe("helpers", () => {
1115
routeKey: "GET /route/{param}",
1216
rawPath: "/route/value",
1317
headers: {
14-
Host: "localhost",
15-
"X-Forwarded-Proto": "http",
16-
"X-Forwarded-Port": "3000",
18+
host: "localhost",
19+
"x-forwarded-proto": "http",
20+
"x-forwarded-port": "3000",
1721
},
1822
requestContext: {
1923
domainName: "localhost",
@@ -30,9 +34,9 @@ describe("helpers", () => {
3034
routeKey: "GET /route/{param}",
3135
rawPath: "/v2/route/value",
3236
headers: {
33-
Host: "abcdefghijz.execute-api.us-east-1.amazonaws.com",
34-
"X-Forwarded-Proto": "https",
35-
"X-Forwarded-Port": "443",
37+
host: "abcdefghijz.execute-api.us-east-1.amazonaws.com",
38+
"x-forwarded-proto": "https",
39+
"x-forwarded-port": "443",
3640
},
3741
requestContext: {
3842
domainName: "abcdefghijz.execute-api.us-east-1.amazonaws.com",
@@ -51,9 +55,9 @@ describe("helpers", () => {
5155
routeKey: "GET /route/{param}",
5256
rawPath: "/route/value",
5357
headers: {
54-
Host: "abcdefghijz.cloudfront.net",
55-
"X-Forwarded-Proto": "https",
56-
"X-Forwarded-Port": "443",
58+
host: "abcdefghijz.cloudfront.net",
59+
"x-forwarded-proto": "https",
60+
"x-forwarded-port": "443",
5761
},
5862
requestContext: {
5963
domainName: "abcdefghijz.cloudfront.net",
@@ -70,9 +74,9 @@ describe("helpers", () => {
7074
routeKey: "GET /route/{param}",
7175
rawPath: "/route/value",
7276
headers: {
73-
Host: "api.test.library.northwestern.edu",
74-
"X-Forwarded-Proto": "https",
75-
"X-Forwarded-Port": "443",
77+
host: "api.test.library.northwestern.edu",
78+
"x-forwarded-proto": "https",
79+
"x-forwarded-port": "443",
7680
},
7781
requestContext: {
7882
domainName: "api.test.library.northwestern.edu",
@@ -91,9 +95,9 @@ describe("helpers", () => {
9195
routeKey: "GET /route/{param}",
9296
rawPath: "/route/value",
9397
headers: {
94-
Host: "localhost",
95-
"X-Forwarded-Proto": "http",
96-
"X-Forwarded-Port": "3000",
98+
host: "localhost",
99+
"x-forwarded-proto": "http",
100+
"x-forwarded-port": "3000",
97101
},
98102
requestContext: {
99103
domainName: "api.test.library.northwestern.edu",
@@ -121,25 +125,44 @@ describe("helpers", () => {
121125
});
122126
});
123127

124-
describe("getHeader()", () => {
125-
it("extracts event headers regardless of case", () => {
126-
const event = {
127-
routeKey: "GET /route/{param}",
128-
rawPath: "/route/value",
129-
headers: {
130-
Host: "abcdefghijz.execute-api.us-east-1.amazonaws.com",
131-
"X-Forwarded-Proto": "https",
132-
"x-forwarded-port": "443",
133-
},
134-
requestContext: {
135-
domainName: "abcdefghijz.execute-api.us-east-1.amazonaws.com",
136-
domainPrefix: "abcdefghijz",
137-
stage: "v2",
138-
},
139-
};
128+
describe("decodeEventBody()", () => {
129+
it("passes plain text body through unaltered", () => {
130+
const event = helpers
131+
.mockEvent("POST", "/search")
132+
.body("plain body")
133+
.render();
134+
const result = decodeEventBody(event);
135+
expect(result.isBase64Encoded).to.be.false;
136+
expect(result.body).to.eq("plain body");
137+
});
138+
139+
it("decodes base64 encoded body", () => {
140+
const event = helpers
141+
.mockEvent("POST", "/search")
142+
.body("encoded body")
143+
.base64Encode()
144+
.render();
145+
expect(event.isBase64Encoded).to.be.true;
146+
expect(event.body).not.to.eq("encoded body");
147+
148+
const result = decodeEventBody(event);
149+
expect(result.isBase64Encoded).to.be.false;
150+
expect(result.body).to.eq("encoded body");
151+
});
152+
});
153+
154+
describe("normalizeHeaders()", () => {
155+
it("converts all headers to lowercase", () => {
156+
const upperHeaders = ["Host", "X-Forwarded-For", "X-Forwarded-Proto"];
157+
const lowerHeaders = ["host", "x-forwarded-for", "x-forwarded-proto"];
158+
159+
const event = helpers.mockEvent("GET", "/search").render();
160+
expect(event.headers).not.to.include.keys(lowerHeaders);
161+
expect(event.headers).to.include.keys(upperHeaders);
140162

141-
expect(getHeader(event, "X-Forwarded-Proto")).to.eq("https");
142-
expect(getHeader(event, "X-Forwarded-Port")).to.eq("443");
163+
const result = normalizeHeaders(event);
164+
expect(result.headers).to.include.keys(lowerHeaders);
165+
expect(result.headers).not.to.include.keys(upperHeaders);
143166
});
144167
});
145168
});

0 commit comments

Comments
 (0)