mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
479 lines
13 KiB
JavaScript
Vendored
479 lines
13 KiB
JavaScript
Vendored
const assert = require('assert');
|
|
const crypto = require('crypto');
|
|
const jsonlines = require('jsonlines');
|
|
const { Server } = require('http');
|
|
const { Bridge } = require('../bridge');
|
|
const { runServer } = require('./run-test-server');
|
|
const { runTcpServer } = require('./run-test-server');
|
|
|
|
test('port binding', async () => {
|
|
const server = new Server();
|
|
const bridge = new Bridge(server);
|
|
bridge.listen();
|
|
|
|
// Test port binding
|
|
const info = await bridge.listening;
|
|
assert.equal(info.address, '127.0.0.1');
|
|
assert.equal(typeof info.port, 'number');
|
|
|
|
server.close();
|
|
});
|
|
|
|
test('`APIGatewayProxyEvent` normalizing', async () => {
|
|
const server = new Server((req, res) =>
|
|
res.end(
|
|
JSON.stringify({
|
|
method: req.method,
|
|
path: req.url,
|
|
headers: req.headers,
|
|
})
|
|
)
|
|
);
|
|
const bridge = new Bridge(server);
|
|
bridge.listen();
|
|
const context = {};
|
|
const result = await bridge.launcher(
|
|
{
|
|
httpMethod: 'GET',
|
|
headers: { foo: 'bar' },
|
|
path: '/apigateway',
|
|
body: null,
|
|
},
|
|
context
|
|
);
|
|
assert.equal(result.encoding, 'base64');
|
|
assert.equal(result.statusCode, 200);
|
|
const body = JSON.parse(Buffer.from(result.body, 'base64').toString());
|
|
assert.equal(body.method, 'GET');
|
|
assert.equal(body.path, '/apigateway');
|
|
assert.equal(body.headers.foo, 'bar');
|
|
assert.equal(context.callbackWaitsForEmptyEventLoop, false);
|
|
|
|
server.close();
|
|
});
|
|
|
|
test('`NowProxyEvent` normalizing', async () => {
|
|
const server = new Server((req, res) =>
|
|
res.end(
|
|
JSON.stringify({
|
|
method: req.method,
|
|
path: req.url,
|
|
headers: req.headers,
|
|
})
|
|
)
|
|
);
|
|
const bridge = new Bridge(server);
|
|
bridge.listen();
|
|
const context = { callbackWaitsForEmptyEventLoop: true };
|
|
const result = await bridge.launcher(
|
|
{
|
|
Action: 'Invoke',
|
|
body: JSON.stringify({
|
|
method: 'POST',
|
|
headers: { foo: 'baz' },
|
|
path: '/nowproxy',
|
|
body: 'body=1',
|
|
}),
|
|
},
|
|
context
|
|
);
|
|
assert.equal(result.encoding, 'base64');
|
|
assert.equal(result.statusCode, 200);
|
|
const body = JSON.parse(Buffer.from(result.body, 'base64').toString());
|
|
assert.equal(body.method, 'POST');
|
|
assert.equal(body.path, '/nowproxy');
|
|
assert.equal(body.headers.foo, 'baz');
|
|
assert.equal(context.callbackWaitsForEmptyEventLoop, false);
|
|
|
|
server.close();
|
|
});
|
|
|
|
test('multi-payload handling', async () => {
|
|
const server = new Server((req, res) => {
|
|
if (req.url === '/redirect') {
|
|
res.setHeader('Location', '/somewhere');
|
|
res.statusCode = 307;
|
|
res.end('/somewhere');
|
|
return;
|
|
}
|
|
res.setHeader(
|
|
'content-type',
|
|
req.url.includes('_next/data') ? 'application/json' : 'text/html'
|
|
);
|
|
|
|
res.end(
|
|
JSON.stringify({
|
|
method: req.method,
|
|
path: req.url,
|
|
headers: req.headers,
|
|
})
|
|
);
|
|
});
|
|
const bridge = new Bridge(server);
|
|
bridge.listen();
|
|
const context = { callbackWaitsForEmptyEventLoop: true };
|
|
const result = await bridge.launcher(
|
|
{
|
|
Action: 'Invoke',
|
|
body: JSON.stringify({
|
|
payloads: [
|
|
{
|
|
method: 'GET',
|
|
headers: { foo: 'baz' },
|
|
path: '/nowproxy',
|
|
},
|
|
{
|
|
method: 'GET',
|
|
headers: { foo: 'baz' },
|
|
path: '/_next/data/build-id/nowproxy.json',
|
|
},
|
|
{
|
|
method: 'GET',
|
|
headers: { foo: 'baz' },
|
|
path: '/redirect',
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
context
|
|
);
|
|
assert.equal(result.encoding, 'base64');
|
|
assert.equal(result.statusCode, 200);
|
|
assert.equal(
|
|
result.headers['content-type'],
|
|
'multipart/mixed; boundary="payload-separator"'
|
|
);
|
|
const bodies = [];
|
|
const payloadParts = result.body.split('\r\n');
|
|
|
|
payloadParts.forEach(item => {
|
|
if (
|
|
item.trim() &&
|
|
!item.startsWith('content-type:') &&
|
|
!item.startsWith('--payload')
|
|
) {
|
|
const content = Buffer.from(
|
|
item.split('--payload-separator')[0],
|
|
'base64'
|
|
).toString();
|
|
bodies.push(content.startsWith('{') ? JSON.parse(content) : content);
|
|
}
|
|
});
|
|
|
|
// ensure content-type is always specified as is required for
|
|
// proper parsing of the multipart body
|
|
assert(payloadParts.some(part => part.includes('content-type: text/plain')));
|
|
|
|
assert.equal(bodies[0].method, 'GET');
|
|
assert.equal(bodies[0].path, '/nowproxy');
|
|
assert.equal(bodies[0].headers.foo, 'baz');
|
|
assert.equal(bodies[1].method, 'GET');
|
|
assert.equal(bodies[1].path, '/_next/data/build-id/nowproxy.json');
|
|
assert.equal(bodies[1].headers.foo, 'baz');
|
|
assert.equal(bodies[2], '/somewhere');
|
|
assert.equal(result.headers['x-vercel-payload-3-status'], '307');
|
|
assert.equal(result.headers['x-vercel-payload-2-status'], undefined);
|
|
assert.equal(result.headers['x-vercel-payload-1-status'], undefined);
|
|
assert.equal(result.headers['x-vercel-payload-1-content-type'], 'text/html');
|
|
assert.equal(
|
|
result.headers['x-vercel-payload-2-content-type'],
|
|
'application/json'
|
|
);
|
|
assert.equal(result.headers['x-vercel-payload-3-content-type'], undefined);
|
|
assert.equal(result.headers['x-vercel-payload-3-location'], '/somewhere');
|
|
assert.equal(result.headers['x-vercel-payload-2-location'], undefined);
|
|
assert.equal(context.callbackWaitsForEmptyEventLoop, false);
|
|
|
|
server.close();
|
|
});
|
|
|
|
test('consumeEvent', async () => {
|
|
const mockListener = jest.fn((_, res) => {
|
|
res.end('hello');
|
|
});
|
|
|
|
const server = new Server(mockListener);
|
|
const bridge = new Bridge(server, true);
|
|
bridge.listen();
|
|
|
|
const context = { callbackWaitsForEmptyEventLoop: true };
|
|
await bridge.launcher(
|
|
{
|
|
Action: 'Invoke',
|
|
body: JSON.stringify({
|
|
method: 'POST',
|
|
headers: { foo: 'baz' },
|
|
path: '/nowproxy',
|
|
body: 'body=1',
|
|
}),
|
|
},
|
|
context
|
|
);
|
|
|
|
const headers = mockListener.mock.calls[0][0].headers;
|
|
const reqId = headers['x-now-bridge-request-id'];
|
|
|
|
expect(reqId).toBeTruthy();
|
|
|
|
const event = bridge.consumeEvent(reqId);
|
|
expect(event.body.toString()).toBe('body=1');
|
|
|
|
// an event can't be consumed multiple times
|
|
// to avoid memory leaks
|
|
expect(bridge.consumeEvent(reqId)).toBeUndefined();
|
|
|
|
server.close();
|
|
});
|
|
|
|
test('consumeEvent and handle decoded path', async () => {
|
|
const mockListener = jest.fn((_, res) => {
|
|
res.end('hello');
|
|
});
|
|
|
|
const server = new Server(mockListener);
|
|
const bridge = new Bridge(server, true);
|
|
bridge.listen();
|
|
|
|
const context = { callbackWaitsForEmptyEventLoop: true };
|
|
await bridge.launcher(
|
|
{
|
|
Action: 'Invoke',
|
|
body: JSON.stringify({
|
|
method: 'POST',
|
|
headers: { foo: 'baz' },
|
|
path: '/now proxy',
|
|
body: 'body=1',
|
|
}),
|
|
},
|
|
context
|
|
);
|
|
|
|
const headers = mockListener.mock.calls[0][0].headers;
|
|
const reqId = headers['x-now-bridge-request-id'];
|
|
|
|
expect(reqId).toBeTruthy();
|
|
|
|
const event = bridge.consumeEvent(reqId);
|
|
expect(event.body.toString()).toBe('body=1');
|
|
|
|
// an event can't be consumed multiple times
|
|
// to avoid memory leaks
|
|
expect(bridge.consumeEvent(reqId)).toBeUndefined();
|
|
|
|
server.close();
|
|
});
|
|
|
|
test('invalid request headers', async () => {
|
|
const server = new Server((req, res) =>
|
|
res.end(
|
|
JSON.stringify({
|
|
method: req.method,
|
|
path: req.url,
|
|
headers: req.headers,
|
|
})
|
|
)
|
|
);
|
|
const bridge = new Bridge(server);
|
|
bridge.listen();
|
|
const context = { callbackWaitsForEmptyEventLoop: true };
|
|
const result = await bridge.launcher(
|
|
{
|
|
Action: 'Invoke',
|
|
body: JSON.stringify({
|
|
method: 'GET',
|
|
headers: { foo: 'baz\n', ok: 'true' },
|
|
path: '/nowproxy',
|
|
body: 'body=1',
|
|
}),
|
|
},
|
|
context
|
|
);
|
|
assert.equal(result.encoding, 'base64');
|
|
assert.equal(result.statusCode, 200);
|
|
const body = JSON.parse(Buffer.from(result.body, 'base64').toString());
|
|
assert.equal(body.method, 'GET');
|
|
assert.equal(body.path, '/nowproxy');
|
|
assert.equal(body.headers.ok, 'true');
|
|
assert(!body.headers.foo);
|
|
assert.equal(context.callbackWaitsForEmptyEventLoop, false);
|
|
|
|
server.close();
|
|
});
|
|
|
|
test('`NowProxyEvent` proxy streaming with a sync handler', async () => {
|
|
const cipherParams = {
|
|
cipher: 'aes-256-ctr',
|
|
cipherIV: crypto.randomBytes(16),
|
|
cipherKey: crypto.randomBytes(32),
|
|
};
|
|
|
|
const effects = {
|
|
callbackPayload: undefined,
|
|
callbackStream: undefined,
|
|
};
|
|
|
|
const { deferred, resolve } = createDeferred();
|
|
|
|
const httpServer = await runServer({
|
|
handler: (req, res) => {
|
|
const chunks = [];
|
|
req.on('data', chunk => {
|
|
chunks.push(chunk.toString());
|
|
});
|
|
req.on('close', () => {
|
|
effects.callbackPayload = chunks;
|
|
res.writeHead(200, 'OK', { 'content-type': 'application/json' });
|
|
res.end();
|
|
resolve();
|
|
});
|
|
},
|
|
});
|
|
|
|
const tcpServerCallback = await runTcpServer({
|
|
cipherParams,
|
|
effects,
|
|
httpServer,
|
|
});
|
|
|
|
const server = new Server((req, res) => {
|
|
res.setHeader('content-type', 'text/html');
|
|
res.end('hello');
|
|
});
|
|
|
|
const bridge = new Bridge(server);
|
|
bridge.listen();
|
|
const context = { callbackWaitsForEmptyEventLoop: true };
|
|
const result = await bridge.launcher(
|
|
{
|
|
Action: 'Invoke',
|
|
body: JSON.stringify({
|
|
method: 'POST',
|
|
responseCallbackCipher: cipherParams.cipher,
|
|
responseCallbackCipherIV: cipherParams.cipherIV.toString('base64'),
|
|
responseCallbackCipherKey: cipherParams.cipherKey.toString('base64'),
|
|
responseCallbackStream: 'abc',
|
|
responseCallbackUrl: String(tcpServerCallback.url),
|
|
headers: { foo: 'bar' },
|
|
path: '/nowproxy',
|
|
body: 'body=1',
|
|
}),
|
|
},
|
|
context
|
|
);
|
|
|
|
await deferred;
|
|
|
|
expect(result).toEqual({});
|
|
expect(context.callbackWaitsForEmptyEventLoop).toEqual(false);
|
|
expect(effects.callbackStream).toEqual('abc');
|
|
expect(effects.callbackPayload).toEqual(['hello']);
|
|
|
|
server.close();
|
|
await httpServer.close();
|
|
await tcpServerCallback.close();
|
|
});
|
|
|
|
test('`NowProxyEvent` proxy streaming with an async handler', async () => {
|
|
const effects = {
|
|
callbackHeaders: undefined,
|
|
callbackMethod: undefined,
|
|
callbackPayload: undefined,
|
|
callbackStream: undefined,
|
|
};
|
|
|
|
const cipherParams = {
|
|
cipher: 'aes-256-ctr',
|
|
cipherIV: crypto.randomBytes(16),
|
|
cipherKey: crypto.randomBytes(32),
|
|
};
|
|
|
|
const { deferred, resolve } = createDeferred();
|
|
const jsonParser = jsonlines.parse();
|
|
const httpServer = await runServer({
|
|
handler: (req, res) => {
|
|
const chunks = [];
|
|
req.pipe(jsonParser);
|
|
jsonParser.on('data', chunk => {
|
|
chunks.push(chunk);
|
|
});
|
|
req.on('close', () => {
|
|
effects.callbackMethod = req.method;
|
|
effects.callbackHeaders = req.headers;
|
|
effects.callbackPayload = chunks;
|
|
res.writeHead(200, 'OK', { 'content-type': 'application/json' });
|
|
res.end();
|
|
resolve();
|
|
});
|
|
},
|
|
});
|
|
|
|
const tcpServerCallback = await runTcpServer({
|
|
cipherParams,
|
|
httpServer,
|
|
effects,
|
|
});
|
|
|
|
const jsonStringifier = jsonlines.stringify();
|
|
const server = new Server((req, res) => {
|
|
res.setHeader('x-test', 'hello');
|
|
res.setHeader('content-type', 'text/html');
|
|
jsonStringifier.pipe(res);
|
|
jsonStringifier.write({ method: req.method });
|
|
jsonStringifier.write({ path: req.url });
|
|
setTimeout(() => {
|
|
jsonStringifier.write({ headers: req.headers });
|
|
res.end();
|
|
}, 100);
|
|
});
|
|
|
|
const bridge = new Bridge(server);
|
|
bridge.listen();
|
|
const context = { callbackWaitsForEmptyEventLoop: true };
|
|
const result = await bridge.launcher(
|
|
{
|
|
Action: 'Invoke',
|
|
body: JSON.stringify({
|
|
method: 'POST',
|
|
responseCallbackCipher: cipherParams.cipher,
|
|
responseCallbackCipherIV: cipherParams.cipherIV.toString('base64'),
|
|
responseCallbackCipherKey: cipherParams.cipherKey.toString('base64'),
|
|
responseCallbackStream: 'abc',
|
|
responseCallbackUrl: String(tcpServerCallback.url),
|
|
headers: { foo: 'bar' },
|
|
path: '/nowproxy',
|
|
body: 'body=1',
|
|
}),
|
|
},
|
|
context
|
|
);
|
|
|
|
await deferred;
|
|
|
|
expect(result).toEqual({});
|
|
expect(context.callbackWaitsForEmptyEventLoop).toEqual(false);
|
|
expect(effects.callbackStream).toEqual('abc');
|
|
expect(effects.callbackMethod).toEqual('POST');
|
|
expect(effects.callbackHeaders).toMatchObject({
|
|
'x-vercel-status-code': '200',
|
|
'x-vercel-header-x-test': 'hello',
|
|
'x-vercel-header-content-type': 'text/html',
|
|
});
|
|
expect(effects.callbackPayload).toMatchObject([
|
|
{ method: 'POST' },
|
|
{ path: '/nowproxy' },
|
|
{ headers: { foo: 'bar' } },
|
|
]);
|
|
|
|
server.close();
|
|
httpServer.close();
|
|
tcpServerCallback.close();
|
|
});
|
|
|
|
function createDeferred() {
|
|
let resolve;
|
|
const deferred = new Promise(_resolve => {
|
|
resolve = _resolve;
|
|
});
|
|
return { deferred, resolve };
|
|
}
|