UNPKG

cloud-red

Version:

Serverless Node-RED for your cloud integration needs

329 lines (291 loc) 10.4 kB
/* * 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. */ 'use strict'; 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; }