Skip to content

feat(runtime-handler): handle incoming headers and cookies #293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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