@vercel/node-bridge
Advanced tools
Comparing version 3.0.0 to 3.1.0
246
bridge.js
@@ -0,3 +1,9 @@ | ||
const { URL } = require('url'); | ||
const { request } = require('http'); | ||
const { Socket } = require('net'); | ||
const { createCipheriv } = require('crypto'); | ||
const { pipeline, Transform } = require('stream'); | ||
const CRLF = `\r\n`; | ||
/** | ||
@@ -20,5 +26,19 @@ * If the `http.Server` handler function throws an error asynchronously, | ||
let bodyBuffer; | ||
const { method, path, headers, encoding, body, payloads } = JSON.parse( | ||
event.body | ||
); | ||
/** | ||
* @type {import('./types').VercelProxyRequest} | ||
*/ | ||
const payload = JSON.parse(event.body); | ||
const { | ||
method, | ||
path, | ||
headers, | ||
encoding, | ||
body, | ||
payloads, | ||
responseCallbackCipher, | ||
responseCallbackCipherIV, | ||
responseCallbackCipherKey, | ||
responseCallbackStream, | ||
responseCallbackUrl, | ||
} = payload; | ||
@@ -32,3 +52,3 @@ /** | ||
if (b) { | ||
if (encoding === 'base64') { | ||
if (typeof b === 'string' && encoding === 'base64') { | ||
bodyBuffer = Buffer.from(b, encoding); | ||
@@ -47,9 +67,5 @@ } else if (encoding === undefined) { | ||
if (payloads) { | ||
/** | ||
* @param {{ body: string | Buffer }} payload | ||
*/ | ||
const normalizePayload = payload => { | ||
payload.body = normalizeBody(payload.body); | ||
}; | ||
payloads.forEach(normalizePayload); | ||
for (const p of payloads) { | ||
p.body = normalizeBody(payload.body); | ||
} | ||
} | ||
@@ -65,2 +81,7 @@ bodyBuffer = normalizeBody(body); | ||
payloads, | ||
responseCallbackCipher, | ||
responseCallbackCipherIV, | ||
responseCallbackCipherKey, | ||
responseCallbackStream, | ||
responseCallbackUrl, | ||
}; | ||
@@ -86,3 +107,14 @@ } | ||
return { isApiGateway: true, method, path, headers, body: bodyBuffer }; | ||
return { | ||
body: bodyBuffer, | ||
headers, | ||
isApiGateway: true, | ||
method, | ||
path, | ||
responseCallbackCipher: undefined, | ||
responseCallbackCipherIV: undefined, | ||
responseCallbackCipherKey: undefined, | ||
responseCallbackStream: undefined, | ||
responseCallbackUrl: undefined, | ||
}; | ||
} | ||
@@ -92,2 +124,3 @@ | ||
* @param {import('./types').VercelProxyEvent | import('aws-lambda').APIGatewayProxyEvent} event | ||
* @return {import('./types').VercelProxyRequest} | ||
*/ | ||
@@ -185,3 +218,3 @@ function normalizeEvent(event) { | ||
* @param {import('aws-lambda').Context} context | ||
* @return {Promise<{statusCode: number, headers: import('http').IncomingHttpHeaders, body: string, encoding: 'base64'}>} | ||
* @return {Promise<import('./types').VercelProxyResponse>} | ||
*/ | ||
@@ -278,2 +311,6 @@ async launcher(event, context) { | ||
} else { | ||
// TODO We expect this to error as it is possible to resolve to empty. | ||
// For now it is not very important as we will only pass | ||
// `responseCallbackUrl` in production. | ||
// @ts-ignore | ||
return this.handleEvent(normalizedEvent); | ||
@@ -286,7 +323,17 @@ } | ||
* @param {ReturnType<typeof normalizeEvent>} normalizedEvent | ||
* @return {Promise<{statusCode: number, headers: import('http').IncomingHttpHeaders, body: string, encoding: 'base64'}>} | ||
* @return {Promise<import('./types').VercelProxyResponse | import('./types').VercelStreamProxyResponse>} | ||
*/ | ||
async handleEvent(normalizedEvent) { | ||
const { port } = await this.listening; | ||
const { isApiGateway, method, headers, body } = normalizedEvent; | ||
const { | ||
body, | ||
headers, | ||
isApiGateway, | ||
method, | ||
responseCallbackCipher, | ||
responseCallbackCipherIV, | ||
responseCallbackCipherKey, | ||
responseCallbackStream, | ||
responseCallbackUrl, | ||
} = normalizedEvent; | ||
let { path } = normalizedEvent; | ||
@@ -300,4 +347,26 @@ | ||
// eslint-disable-next-line consistent-return | ||
return new Promise((resolve, reject) => { | ||
let socket; | ||
let cipher; | ||
let url; | ||
if (responseCallbackUrl) { | ||
socket = new Socket(); | ||
url = new URL(responseCallbackUrl); | ||
socket.connect(parseInt(url.port, 10), url.hostname); | ||
socket.write(`${responseCallbackStream}${CRLF}`); | ||
} | ||
if ( | ||
responseCallbackCipher && | ||
responseCallbackCipherKey && | ||
responseCallbackCipherIV | ||
) { | ||
cipher = createCipheriv( | ||
responseCallbackCipher, | ||
Buffer.from(responseCallbackCipherKey, 'base64'), | ||
Buffer.from(responseCallbackCipherIV, 'base64') | ||
); | ||
} | ||
// if the path is improperly encoded we need to encode it or | ||
@@ -309,30 +378,9 @@ // http.request will throw an error (related check: https://github.com/nodejs/node/blob/4ece669c6205ec78abfdadfe78869bbb8411463e/lib/_http_client.js#L84) | ||
const opts = { hostname: '127.0.0.1', port, path, method }; | ||
const req = request(opts, res => { | ||
const response = res; | ||
/** | ||
* @type {Buffer[]} | ||
*/ | ||
const respBodyChunks = []; | ||
response.on('data', chunk => respBodyChunks.push(Buffer.from(chunk))); | ||
response.on('error', reject); | ||
response.on('end', () => { | ||
const bodyBuffer = Buffer.concat(respBodyChunks); | ||
delete response.headers.connection; | ||
const req = request( | ||
{ hostname: '127.0.0.1', port, path, method }, | ||
socket && url && cipher | ||
? getStreamResponseCallback({ url, socket, cipher, resolve, reject }) | ||
: getResponseCallback({ isApiGateway, resolve, reject }) | ||
); | ||
if (isApiGateway) { | ||
delete response.headers['content-length']; | ||
} else if (response.headers['content-length']) { | ||
response.headers['content-length'] = String(bodyBuffer.length); | ||
} | ||
resolve({ | ||
statusCode: response.statusCode || 200, | ||
headers: response.headers, | ||
body: bodyBuffer.toString('base64'), | ||
encoding: 'base64', | ||
}); | ||
}); | ||
}); | ||
req.on('error', error => { | ||
@@ -346,12 +394,6 @@ setTimeout(() => { | ||
for (const [name, value] of Object.entries(headers)) { | ||
if (value === undefined) { | ||
console.error( | ||
`Skipping HTTP request header "${name}" because value is undefined` | ||
); | ||
continue; | ||
} | ||
for (const [name, value] of getHeadersIterator(headers)) { | ||
try { | ||
req.setHeader(name, value); | ||
} catch (err) { | ||
} catch (/** @type any */ err) { | ||
console.error(`Skipping HTTP request header: "${name}: ${value}"`); | ||
@@ -378,2 +420,104 @@ console.error(err.message); | ||
/** | ||
* Generates the streaming response callback which writes in the given socket client a raw | ||
* HTTP Request message to later pipe the response body into the socket. It will pass request | ||
* headers namespace and an additional header with the status code. Once everything is | ||
* written it will destroy the socket and resolve to an empty object. If a cipher is given | ||
* it will be used to pipe bytes. | ||
* | ||
* @type {(params: { | ||
* url: import('url').URL, | ||
* socket: import('net').Socket, | ||
* cipher: import('crypto').Cipher | ||
* resolve: (result: (Record<string, never>)) => void, | ||
* reject: (err: Error) => void | ||
* }) => (response: import("http").IncomingMessage) => void} | ||
*/ | ||
function getStreamResponseCallback({ url, socket, cipher, resolve, reject }) { | ||
return response => { | ||
const chunked = new Transform(); | ||
chunked._transform = function (chunk, _, callback) { | ||
this.push(Buffer.byteLength(chunk).toString(16) + CRLF); | ||
this.push(chunk); | ||
this.push(CRLF); | ||
callback(); | ||
}; | ||
let headers = `Host: ${url.host}${CRLF}`; | ||
headers += `transfer-encoding: chunked${CRLF}`; | ||
headers += `x-vercel-status-code: ${response.statusCode || 200}${CRLF}`; | ||
for (const [name, value] of getHeadersIterator(response.headers)) { | ||
if (!['connection', 'transfer-encoding'].includes(name)) { | ||
headers += `x-vercel-header-${name}: ${value}${CRLF}`; | ||
} | ||
} | ||
cipher.write(`POST ${url.pathname} HTTP/1.1${CRLF}${headers}${CRLF}`); | ||
pipeline(response, chunked, cipher, socket, err => { | ||
if (err) return reject(err); | ||
resolve({}); | ||
}); | ||
}; | ||
} | ||
/** | ||
* Generates the normal response callback which waits until the body is fully | ||
* received before resolving the promise. It caches the entire body and resolve | ||
* with an object that describes the response. | ||
* | ||
* @type {(params: { | ||
* isApiGateway: boolean, | ||
* resolve: (result: (import('./types').VercelProxyResponse)) => void, | ||
* reject: (err: Error) => void | ||
* }) => (response: import("http").IncomingMessage) => void} | ||
*/ | ||
function getResponseCallback({ isApiGateway, resolve, reject }) { | ||
return response => { | ||
/** | ||
* @type {Buffer[]} | ||
*/ | ||
const respBodyChunks = []; | ||
response.on('data', chunk => respBodyChunks.push(Buffer.from(chunk))); | ||
response.on('error', reject); | ||
response.on('end', () => { | ||
const bodyBuffer = Buffer.concat(respBodyChunks); | ||
delete response.headers.connection; | ||
if (isApiGateway) { | ||
delete response.headers['content-length']; | ||
} else if (response.headers['content-length']) { | ||
response.headers['content-length'] = String(bodyBuffer.length); | ||
} | ||
resolve({ | ||
statusCode: response.statusCode || 200, | ||
headers: response.headers, | ||
body: bodyBuffer.toString('base64'), | ||
encoding: 'base64', | ||
}); | ||
}); | ||
}; | ||
} | ||
/** | ||
* Get an iterator for the headers object and yield the name and value when | ||
* the value is not undefined only. | ||
* | ||
* @type {(headers: import('http').IncomingHttpHeaders) => | ||
* Generator<[string, string | string[]], void, unknown>} | ||
*/ | ||
function* getHeadersIterator(headers) { | ||
for (const [name, value] of Object.entries(headers)) { | ||
if (value === undefined) { | ||
console.error( | ||
`Skipping HTTP request header "${name}" because value is undefined` | ||
); | ||
continue; | ||
} | ||
yield [name, value]; | ||
} | ||
} | ||
module.exports = { Bridge }; |
{ | ||
"name": "@vercel/node-bridge", | ||
"version": "3.0.0", | ||
"version": "3.1.0", | ||
"license": "MIT", | ||
@@ -26,5 +26,7 @@ "main": "./index.js", | ||
"@types/node": "*", | ||
"jsonlines": "0.1.1", | ||
"test-listen": "1.1.0", | ||
"typescript": "4.3.4" | ||
}, | ||
"gitHead": "de0d2fba0b32588726a2799015eaff4e6bb65ffb" | ||
"gitHead": "a630e1989613052f64411fd7e5468de58c5999ba" | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
182576
4925
5
4