Skip to content

Commit

Permalink
feat(runtime-handler): handle incoming headers and cookies (#293)
Browse files Browse the repository at this point in the history
  • Loading branch information
philnash authored Jul 1, 2021
1 parent 24d4319 commit 62ff180
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 16 deletions.
193 changes: 180 additions & 13 deletions packages/runtime-handler/__tests__/dev-runtime/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Response } from '../../src/dev-runtime/internal/response';
import {
constructContext,
constructEvent,
constructHeaders,
constructGlobalScope,
handleError,
handleSuccess,
Expand All @@ -37,16 +38,21 @@ import { cleanUpStackTrace } from '../../src/dev-runtime/utils/stack-trace/clean

const { VoiceResponse, MessagingResponse, FaxResponse } = twiml;

const mockResponse = (new MockResponse() as unknown) as ExpressResponse;
const mockResponse = new MockResponse() as unknown as ExpressResponse;
mockResponse.type = jest.fn(() => mockResponse);

function asExpressRequest(req: { query?: {}; body?: {} }): ExpressRequest {
return (req as unknown) as ExpressRequest;
function asExpressRequest(req: {
query?: {};
body?: {};
rawHeaders?: string[];
cookies?: {};
}): ExpressRequest {
return req as unknown as ExpressRequest;
}

describe('handleError function', () => {
test('returns string error', () => {
const mockRequest = (new MockRequest() as unknown) as ExpressRequest;
const mockRequest = new MockRequest() as unknown as ExpressRequest;
mockRequest['useragent'] = {
isDesktop: true,
isMobile: false,
Expand All @@ -59,7 +65,7 @@ describe('handleError function', () => {
});

test('handles objects as error argument', () => {
const mockRequest = (new MockRequest() as unknown) as ExpressRequest;
const mockRequest = new MockRequest() as unknown as ExpressRequest;
mockRequest['useragent'] = {
isDesktop: true,
isMobile: false,
Expand All @@ -72,7 +78,7 @@ describe('handleError function', () => {
});

test('wraps error object for desktop requests', () => {
const mockRequest = (new MockRequest() as unknown) as ExpressRequest;
const mockRequest = new MockRequest() as unknown as ExpressRequest;
mockRequest['useragent'] = {
isDesktop: true,
isMobile: false,
Expand All @@ -85,7 +91,7 @@ describe('handleError function', () => {
});

test('wraps error object for mobile requests', () => {
const mockRequest = (new MockRequest() as unknown) as ExpressRequest;
const mockRequest = new MockRequest() as unknown as ExpressRequest;
mockRequest['useragent'] = {
isDesktop: false,
isMobile: true,
Expand All @@ -98,7 +104,7 @@ describe('handleError function', () => {
});

test('returns string version of error for other requests', () => {
const mockRequest = (new MockRequest() as unknown) as ExpressRequest;
const mockRequest = new MockRequest() as unknown as ExpressRequest;
mockRequest['useragent'] = {
isDesktop: false,
isMobile: false,
Expand Down Expand Up @@ -128,7 +134,11 @@ describe('constructEvent function', () => {
},
})
);
expect(event).toEqual({ Body: 'Hello', index: 5 });
expect(event).toEqual({
Body: 'Hello',
index: 5,
request: { headers: {}, cookies: {} },
});
});

test('overrides query with body', () => {
Expand All @@ -143,7 +153,28 @@ describe('constructEvent function', () => {
},
})
);
expect(event).toEqual({ Body: 'Bye', From: '+123456789' });
expect(event).toEqual({
Body: 'Bye',
From: '+123456789',
request: { headers: {}, cookies: {} },
});
});

test('does not override request', () => {
const event = constructEvent(
asExpressRequest({
body: {
Body: 'Bye',
},
query: {
request: 'Hello',
},
})
);
expect(event).toEqual({
Body: 'Bye',
request: 'Hello',
});
});

test('handles empty body', () => {
Expand All @@ -156,7 +187,11 @@ describe('constructEvent function', () => {
},
})
);
expect(event).toEqual({ Body: 'Hello', From: '+123456789' });
expect(event).toEqual({
Body: 'Hello',
From: '+123456789',
request: { headers: {}, cookies: {} },
});
});

test('handles empty query', () => {
Expand All @@ -169,7 +204,11 @@ describe('constructEvent function', () => {
query: {},
})
);
expect(event).toEqual({ Body: 'Hello', From: '+123456789' });
expect(event).toEqual({
Body: 'Hello',
From: '+123456789',
request: { headers: {}, cookies: {} },
});
});

test('handles both empty', () => {
Expand All @@ -179,7 +218,135 @@ describe('constructEvent function', () => {
query: {},
})
);
expect(event).toEqual({});
expect(event).toEqual({ request: { headers: {}, cookies: {} } });
});

test('adds headers to request property', () => {
const event = constructEvent(
asExpressRequest({
body: {},
query: {},
rawHeaders: ['x-test', 'example'],
})
);
expect(event).toEqual({
request: { headers: { 'x-test': 'example' }, cookies: {} },
});
});

test('adds cookies to request property', () => {
const event = constructEvent(
asExpressRequest({
body: {},
query: {},
rawHeaders: [],
cookies: { flavour: 'choc chip' },
})
);
expect(event).toEqual({
request: { headers: {}, cookies: { flavour: 'choc chip' } },
});
});
});

describe('constructHeaders function', () => {
test('handles undefined', () => {
const headers = constructHeaders();
expect(headers).toEqual({});
});
test('handles an empty array', () => {
const headers = constructHeaders([]);
expect(headers).toEqual({});
});
test('it handles a single header value', () => {
const headers = constructHeaders(['x-test', 'hello, world']);
expect(headers).toEqual({ 'x-test': 'hello, world' });
});
test('it handles a duplicated header value', () => {
const headers = constructHeaders([
'x-test',
'hello, world',
'x-test',
'ahoy',
]);
expect(headers).toEqual({ 'x-test': ['hello, world', 'ahoy'] });
});
test('it handles a duplicated header value multiple times', () => {
const headers = constructHeaders([
'x-test',
'hello, world',
'x-test',
'ahoy',
'x-test',
'third',
]);
expect(headers).toEqual({ 'x-test': ['hello, world', 'ahoy', 'third'] });
});
test('it strips restricted headers', () => {
const headers = constructHeaders([
'x-test',
'hello, world',
'I-Twilio-Test',
'nope',
]);
expect(headers).toEqual({ 'x-test': 'hello, world' });
});
test('it lowercases and combines header names', () => {
const headers = constructHeaders([
'X-Test',
'hello, world',
'X-test',
'ahoy',
'x-test',
'third',
]);
expect(headers).toEqual({
'x-test': ['hello, world', 'ahoy', 'third'],
});
});

test("it doesn't pass on restricted headers", () => {
const headers = constructHeaders([
'I-Twilio-Example',
'example',
'I-T-Example',
'example',
'OT-Example',
'example',
'x-amz-example',
'example',
'via',
'example',
'Referer',
'example.com',
'transfer-encoding',
'example',
'proxy-authorization',
'example',
'proxy-authenticate',
'example',
'x-forwarded-example',
'example',
'x-real-ip',
'example',
'connection',
'example',
'proxy-connection',
'example',
'expect',
'example',
'trailer',
'example',
'upgrade',
'example',
'x-accel-example',
'example',
'x-actual-header',
'this works',
]);
expect(headers).toEqual({
'x-actual-header': 'this works',
});
});
});

Expand Down
6 changes: 4 additions & 2 deletions packages/runtime-handler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,18 @@
},
"devDependencies": {
"@types/common-tags": "^1.8.0",
"@types/cookie-parser": "^1.4.2",
"@types/debug": "^4.1.4",
"@types/express-useragent": "^0.2.21",
"@types/jest": "^24.0.16",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^14.0.19",
"@types/supertest": "^2.0.8",
"jest": "^24.8.0",
"jest": "^26.2.2",
"npm-run-all": "^4.1.5",
"rimraf": "^2.6.3",
"supertest": "^3.1.0",
"ts-jest": "^24.0.2",
"ts-jest": "^26.0.0",
"typescript": "^3.8.3"
},
"bugs": {
Expand All @@ -64,6 +65,7 @@
"@types/express": "4.17.7",
"chalk": "^4.1.1",
"common-tags": "^1.8.0",
"cookie-parser": "^1.4.5",
"debug": "^3.1.0",
"express": "^4.16.3",
"express-useragent": "^1.0.13",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const restrictedHeaderPrefixes = [
'i-twilio-',
'i-t-',
'ot-',
'x-amz',
'x-forwarded-',
'x-accel-',
];

export const restrictedHeaderExactMatches = [
'via',
'referer',
'transfer-encoding',
'proxy-authorization',
'proxy-authenticate',
'x-real-ip',
'connection',
'proxy-connection',
'expect',
'trailer',
'upgrade',
];
49 changes: 48 additions & 1 deletion packages/runtime-handler/src/dev-runtime/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import debug from './utils/debug';
import { wrapErrorInHtml } from './utils/error-html';
import { requireFromProject } from './utils/requireFromProject';
import { cleanUpStackTrace } from './utils/stack-trace/clean-up';
import {
restrictedHeaderPrefixes,
restrictedHeaderExactMatches,
} from './checks/restricted-headers';

const log = debug('twilio-runtime-handler:dev:route');

Expand All @@ -35,8 +39,51 @@ const RUNNER_PATH =

let twilio: TwilioPackage;

type Headers = {
[key: string]: string | string[];
};
type Cookies = {
[key: string]: string;
};

export function constructHeaders(rawHeaders?: string[]): Headers {
if (rawHeaders && rawHeaders.length > 0) {
const headers: Headers = {};
for (let i = 0, len = rawHeaders.length; i < len; i += 2) {
const headerName = rawHeaders[i].toLowerCase();
if (
restrictedHeaderExactMatches.some(
(headerType) => headerName === headerType
) ||
restrictedHeaderPrefixes.some((headerType) =>
headerName.startsWith(headerType)
)
) {
continue;
}
const currentHeader = headers[headerName];
if (!currentHeader) {
headers[headerName] = rawHeaders[i + 1];
} else if (typeof currentHeader === 'string') {
headers[headerName] = [currentHeader, rawHeaders[i + 1]];
} else {
headers[headerName] = [...currentHeader, rawHeaders[i + 1]];
}
}
return headers;
}
return {};
}

export function constructEvent<T extends {} = {}>(req: ExpressRequest): T {
return { ...req.query, ...req.body };
return {
request: {
headers: constructHeaders(req.rawHeaders),
cookies: (req.cookies || {}) as Cookies,
},
...req.query,
...req.body,
};
}

export function constructContext<T extends {} = {}>(
Expand Down
Loading

0 comments on commit 62ff180

Please sign in to comment.