cloud-red
Version:
Serverless Node-RED for your cloud integration needs
329 lines (291 loc) • 10.4 kB
JavaScript
/*
* Copyright 2016-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file.
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
;
const http = require('http');
const url = require('url');
const binarycase = require('binary-case');
const isType = require('type-is');
const { performance } = require('perf_hooks');
function getPathWithQueryStringParams(event) {
return url.format({
pathname: event.path,
query: event.queryStringParameters
});
}
function getEventBody(event) {
return Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8');
}
function clone(json) {
return JSON.parse(JSON.stringify(json));
}
function getContentType(params) {
// only compare mime type; ignore encoding part
return params.contentTypeHeader ? params.contentTypeHeader.split(';')[0] : '';
}
function isContentTypeBinaryMimeType(params) {
return (
params.binaryMimeTypes.length > 0 &&
!!isType.is(params.contentType, params.binaryMimeTypes)
);
}
function mapApiGatewayEventToHttpRequest(event, context, socketPath) {
const headers = Object.assign({}, event.headers);
// NOTE: API Gateway is not setting Content-Length header on requests even when they have a body
if (event.body && !headers['Content-Length']) {
const body = getEventBody(event);
headers['Content-Length'] = Buffer.byteLength(body);
}
const clonedEventWithoutBody = clone(event);
delete clonedEventWithoutBody.body;
headers['x-apigateway-event'] = encodeURIComponent(
JSON.stringify(clonedEventWithoutBody)
);
headers['x-apigateway-context'] = encodeURIComponent(JSON.stringify(context));
return {
method: event.httpMethod,
path: getPathWithQueryStringParams(event),
headers,
socketPath
// protocol: `${headers['X-Forwarded-Proto']}:`,
// host: headers.Host,
// hostname: headers.Host, // Alias for host
// port: headers['X-Forwarded-Port']
};
}
function forwardResponseToApiGateway(server, response, resolver) {
let buf = [];
response
.on('data', chunk => buf.push(chunk))
.on('end', () => {
const bodyBuffer = Buffer.concat(buf);
const statusCode = response.statusCode;
const headers = response.headers;
// chunked transfer not currently supported by API Gateway
/* istanbul ignore else */
if (headers['transfer-encoding'] === 'chunked') {
delete headers['transfer-encoding'];
}
// HACK: modifies header casing to get around API Gateway's limitation of not allowing multiple
// headers with the same name, as discussed on the AWS Forum https://forums.aws.amazon.com/message.jspa?messageID=725953#725953
Object.keys(headers).forEach(h => {
if (Array.isArray(headers[h])) {
if (h.toLowerCase() === 'set-cookie') {
headers[h].forEach((value, i) => {
headers[binarycase(h, i + 1)] = value;
});
delete headers[h];
} else {
headers[h] = headers[h].join(',');
}
}
});
const contentType = getContentType({
contentTypeHeader: headers['content-type']
});
const isBase64Encoded = isContentTypeBinaryMimeType({
contentType,
binaryMimeTypes: server._binaryTypes
});
const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');
const successResponse = { statusCode, body, headers, isBase64Encoded };
resolver.succeed({ response: successResponse });
});
}
function forwardConnectionErrorResponseToApiGateway(error, resolver) {
console.log('ERROR: aws-serverless-express connection error');
console.error(error);
const errorResponse = {
statusCode: 502, // "DNS resolution, TCP level errors, or actual HTTP parse errors" - https://nodejs.org/api/http.html#http_http_request_options_callback
body: '',
headers: {}
};
resolver.succeed({ response: errorResponse });
}
function forwardLibraryErrorResponseToApiGateway(error, resolver) {
console.log('ERROR: aws-serverless-express error');
console.error(error);
const errorResponse = {
statusCode: 500,
body: '',
headers: {}
};
resolver.succeed({ response: errorResponse });
}
function forwardRequestToNodeServer(server, event, context, resolver) {
try {
const requestOptions = mapApiGatewayEventToHttpRequest(
event,
context,
getSocketPath(server._socketPathSuffix)
);
const req = http.request(requestOptions, response =>
forwardResponseToApiGateway(server, response, resolver)
);
if (event.body) {
const body = getEventBody(event);
req.write(body);
}
req
.on('error', error =>
forwardConnectionErrorResponseToApiGateway(error, resolver)
)
.end();
} catch (error) {
forwardLibraryErrorResponseToApiGateway(error, resolver);
return server;
}
}
function startServer(server) {
return server.listen(getSocketPath(server._socketPathSuffix));
}
function getSocketPath(socketPathSuffix) {
/* istanbul ignore if */ /* only running tests on Linux; Window support is for local dev only */
if (/^win/.test(process.platform)) {
const path = require('path');
return path.join(
'\\\\?\\pipe',
process.cwd(),
`server-${socketPathSuffix}`
);
} else {
return `/tmp/server-${socketPathSuffix}.sock`;
}
}
function getRandomString() {
return Math.random()
.toString(36)
.substring(2, 15);
}
function createServer(requestListener, serverListenCallback, binaryTypes) {
let tCreateServerStart = performance.now();
const server = http.createServer(requestListener);
server._socketPathSuffix = getRandomString();
server._binaryTypes = binaryTypes ? binaryTypes.slice() : [];
server.on('listening', () => {
server._isListening = true;
if (serverListenCallback) serverListenCallback();
});
server
.on('close', () => {
server._isListening = false;
})
.on('error', error => {
/* istanbul ignore else */
if (error.code === 'EADDRINUSE') {
console.warn(
`WARNING: Attempting to listen on socket ${getSocketPath(
server._socketPathSuffix
)}, but it is already in use. This is likely as a result of a previous invocation error or timeout. Check the logs for the invocation(s) immediately prior to this for root cause, and consider increasing the timeout and/or cpu/memory allocation if this is purely as a result of a timeout. aws-serverless-express will restart the Node.js server listening on a new port and continue with this request.`
);
server._socketPathSuffix = getRandomString();
return server.close(() => startServer(server));
} else {
console.log('ERROR: server error');
console.error(error);
}
});
let tCreateServerEnd = performance.now();
console.log(
`[lambdify] Proxy Server created in (${tCreateServerEnd -
tCreateServerStart}) milliseconds.`
);
return server;
}
function proxy(server, event, context, resolutionMode, callback) {
// console.log('[lambdify] proxy invoked');
// DEPRECATED: Legacy support
if (!resolutionMode) {
// console.log('[lambdify] resolution mode is FALSE');
const resolver = makeResolver({
context,
resolutionMode: 'CONTEXT_SUCCEED'
});
if (server._isListening) {
// console.log('[lambdify] forwarding request to node server');
forwardRequestToNodeServer(server, event, context, resolver);
// console.log('[lambdify] returning server...');
// console.log(server);
return server;
} else {
// console.log('[lambdify] starting the server...');
return startServer(server).on('listening', () => {
// @ts-ignore
// console.log('[lambdify] proxy the event...');
proxy(server, event, context);
});
}
}
return {
promise: new Promise((resolve, reject) => {
const promise = {
reject,
resolve
};
const resolver = makeResolver({
callback,
context,
promise,
resolutionMode
});
if (server._isListening) {
// console.log(
// '[lambdify] forwarding request to node server inside promise'
// );
forwardRequestToNodeServer(server, event, context, resolver);
} else {
// console.log('[lambdify] starting the server inside promise...');
startServer(server).on('listening', () => {
// console.log('[lambdify] proxy the event inside promise...');
forwardRequestToNodeServer(server, event, context, resolver);
});
}
})
};
}
function makeResolver(
params /* {
context,
callback,
promise,
resolutionMode
} */
) {
return {
succeed: (params2 /* {
response
} */) => {
if (params.resolutionMode === 'CONTEXT_SUCCEED')
return params.context.succeed(params2.response);
if (params.resolutionMode === 'CALLBACK')
return params.callback(null, params2.response);
if (params.resolutionMode === 'PROMISE')
return params.promise.resolve(params2.response);
}
};
}
exports.createServer = createServer;
exports.proxy = proxy;
/* istanbul ignore else */
if (process.env.NODE_ENV === 'test') {
exports.getPathWithQueryStringParams = getPathWithQueryStringParams;
exports.mapApiGatewayEventToHttpRequest = mapApiGatewayEventToHttpRequest;
exports.forwardResponseToApiGateway = forwardResponseToApiGateway;
exports.forwardConnectionErrorResponseToApiGateway = forwardConnectionErrorResponseToApiGateway;
exports.forwardLibraryErrorResponseToApiGateway = forwardLibraryErrorResponseToApiGateway;
exports.forwardRequestToNodeServer = forwardRequestToNodeServer;
exports.startServer = startServer;
exports.getSocketPath = getSocketPath;
exports.makeResolver = makeResolver;
}