UNPKG

aws-serverless-express-binary

Version:

Supported image upload forward to Node Server. This library enables you to utilize AWS Lambda and Amazon API Gateway to respond to web and API requests using your existing Node.js application framework.

265 lines (229 loc) 9.12 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') function getPathWithQueryStringParams (event) { return url.format({ pathname: event.path, query: event.queryStringParameters }) } function getEventBody (event) { return Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'binary') } 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) { const 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) { 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) } }) return server } function proxy (server, event, context, resolutionMode, callback) { // DEPRECATED: Legacy support if (!resolutionMode) { const resolver = makeResolver({ context, resolutionMode: 'CONTEXT_SUCCEED' }) if (server._isListening) { forwardRequestToNodeServer(server, event, context, resolver) return server } else { return startServer(server) .on('listening', () => proxy(server, event, context)) } } return { promise: new Promise((resolve, reject) => { const promise = { resolve, reject } const resolver = makeResolver({ context, callback, promise, resolutionMode }) if (server._isListening) { forwardRequestToNodeServer(server, event, context, resolver) } else { startServer(server) .on('listening', () => 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 }