diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..46828fd2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# Ignore code that was originally part of the Lambda runtime +runtime \ No newline at end of file diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 47333f4a..2db9800a 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -2,6 +2,7 @@ Component,Origin,License,Copyright async-listener,github.com/othiym23/async-listener,BSD-2-Clause,"Copyright (c) 2013-2017, Forrest L Norvell All rights reserved." async,github.com/caolan/async,MIT,Copyright (c) 2010-2014 Caolan McMahon atomic-batcher,github.com/mafintosh/atomic-batcher,MIT,Copyright (c) 2016 Mathias Buus +aws-lambda-nodejs-runtime-interface-client,github.com/aws/aws-lambda-nodejs-runtime-interface-client,Apache 2.0,Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. aws-sdk,github.com/aws/aws-sdk-js,Apache-2.0,"Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved." aws-xray-sdk-core,github.com/aws/aws-xray-sdk-node/tree/master/packages/core,Apache-2.0,"Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved." base64-js,github.com/beatgammit/base64-js,MIT,Copyright (c) 2014 Jameson Little diff --git a/NOTICE b/NOTICE index aaa50835..4907a0df 100644 --- a/NOTICE +++ b/NOTICE @@ -2,3 +2,6 @@ Datadog datadog-lambda-js Copyright 2019 Datadog, Inc. This product includes software developed at Datadog (https://www.datadoghq.com/). + +The Initial Developer of the files in the runtime directory is Amazon.com, Inc. +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/src/handler.ts b/src/handler.ts index 114180fd..62101567 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,9 +1,7 @@ import { datadog, datadogHandlerEnvVar, lambdaTaskRootEnvVar, traceExtractorEnvVar, getEnvValue } from "./index"; import { TraceExtractor } from "./trace"; import { logDebug, logError } from "./utils"; -// We reuse the function loading logic already inside the lambda runtime. -// tslint:disable-next-line:no-var-requires -const { load } = require("/var/runtime/UserFunction") as any; +import { load } from "./runtime"; if (process.env.DD_TRACE_DISABLED_PLUGINS === undefined) { process.env.DD_TRACE_DISABLED_PLUGINS = "fs"; diff --git a/src/runtime/errors.ts b/src/runtime/errors.ts new file mode 100644 index 00000000..f9c58b62 --- /dev/null +++ b/src/runtime/errors.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Defines custom error types throwable by the runtime. + */ + +"use strict"; + +import util from "util"; + +export function isError(obj: any): obj is Error { + return ( + obj && + obj.name && + obj.message && + obj.stack && + typeof obj.name === "string" && + typeof obj.message === "string" && + typeof obj.stack === "string" + ); +} + +interface RuntimeErrorResponse { + errorType: string; + errorMessage: string; + trace: string[]; +} + +/** + * Attempt to convert an object into a response object. + * This method accounts for failures when serializing the error object. + */ +export function toRuntimeResponse(error: unknown): RuntimeErrorResponse { + try { + if (util.types.isNativeError(error) || isError(error)) { + if (!error.stack) { + throw new Error("Error stack is missing."); + } + return { + errorType: error.name, + errorMessage: error.message, + trace: error.stack.split("\n") || [], + }; + } else { + return { + errorType: typeof error, + errorMessage: (error as any).toString(), + trace: [], + }; + } + } catch (_err) { + return { + errorType: "handled", + errorMessage: + "callback called with Error argument, but there was a problem while retrieving one or more of its message, name, and stack", + trace: [], + }; + } +} + +/** + * Format an error with the expected properties. + * For compatability, the error string always starts with a tab. + */ +export const toFormatted = (error: unknown): string => { + try { + return ( + "\t" + JSON.stringify(error, (_k, v) => _withEnumerableProperties(v)) + ); + } catch (err) { + return "\t" + JSON.stringify(toRuntimeResponse(error)); + } +}; + +/** + * Error name, message, code, and stack are all members of the superclass, which + * means they aren't enumerable and don't normally show up in JSON.stringify. + * This method ensures those interesting properties are available along with any + * user-provided enumerable properties. + */ +function _withEnumerableProperties(error: any) { + if (error instanceof Error) { + const extendedError: ExtendedError = (error); + const ret: any = Object.assign( + { + errorType: extendedError.name, + errorMessage: extendedError.message, + code: extendedError.code, + }, + extendedError + ); + if (typeof extendedError.stack == "string") { + ret.stack = extendedError.stack.split("\n"); + } + return ret; + } else { + return error; + } +} + +export class ExtendedError extends Error { + code?: number; + custom?: string; + reason?: string; + promise?: Promise; + + constructor(reason?: string) { + super(reason); // 'Error' breaks prototype chain here + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + } +} + +export class ImportModuleError extends ExtendedError {} +export class HandlerNotFound extends ExtendedError {} +export class MalformedHandlerName extends ExtendedError {} +export class UserCodeSyntaxError extends ExtendedError {} +export class UnhandledPromiseRejection extends ExtendedError { + constructor(reason?: string, promise?: Promise) { + super(reason); + this.reason = reason; + this.promise = promise; + } +} + +const errorClasses = [ + ImportModuleError, + HandlerNotFound, + MalformedHandlerName, + UserCodeSyntaxError, + UnhandledPromiseRejection, +]; + +errorClasses.forEach((e) => { + e.prototype.name = `Runtime.${e.name}`; +}); \ No newline at end of file diff --git a/src/runtime/index.ts b/src/runtime/index.ts new file mode 100644 index 00000000..10e9f9ab --- /dev/null +++ b/src/runtime/index.ts @@ -0,0 +1 @@ +export { load } from "./user-function"; diff --git a/src/runtime/user-function.ts b/src/runtime/user-function.ts new file mode 100644 index 00000000..1fd53403 --- /dev/null +++ b/src/runtime/user-function.ts @@ -0,0 +1,170 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Modifications copyright 2021 Datadog, Inc. + * + * The original file was part of aws-lambda-nodejs-runtime-interface-client + * https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/main/src/utils/UserFunction.ts + * + * This module defines the functions for loading the user's code as specified + * in a handler string. + */ + + "use strict"; + + import path from "path"; + import fs from "fs"; + import { + HandlerNotFound, + MalformedHandlerName, + ImportModuleError, + UserCodeSyntaxError, + ExtendedError, + } from "./errors"; + + const FUNCTION_EXPR = /^([^.]*)\.(.*)$/; + const RELATIVE_PATH_SUBSTRING = ".."; + + /** + * Break the full handler string into two pieces, the module root and the actual + * handler string. + * Given './somepath/something/module.nestedobj.handler' this returns + * ['./somepath/something', 'module.nestedobj.handler'] + */ + function _moduleRootAndHandler(fullHandlerString: string): [string, string] { + const handlerString = path.basename(fullHandlerString); + const moduleRoot = fullHandlerString.substring( + 0, + fullHandlerString.indexOf(handlerString) + ); + return [moduleRoot, handlerString]; + } + + /** + * Split the handler string into two pieces: the module name and the path to + * the handler function. + */ + function _splitHandlerString(handler: string): [string, string] { + const match = handler.match(FUNCTION_EXPR); + if (!match || match.length != 3) { + throw new MalformedHandlerName("Bad handler"); + } + return [match[1], match[2]]; // [module, function-path] + } + + /** + * Resolve the user's handler function from the module. + */ + function _resolveHandler(object: any, nestedProperty: string): any { + return nestedProperty.split(".").reduce((nested, key) => { + return nested && nested[key]; + }, object); + } + + /** + * Verify that the provided path can be loaded as a file per: + * https://nodejs.org/dist/latest-v10.x/docs/api/modules.html#modules_all_together + * @param string - the fully resolved file path to the module + * @return bool + */ + function _canLoadAsFile(modulePath: string): boolean { + return fs.existsSync(modulePath) || fs.existsSync(modulePath + ".js"); + } + + /** + * Attempt to load the user's module. + * Attempts to directly resolve the module relative to the application root, + * then falls back to the more general require(). + */ + function _tryRequire(appRoot: string, moduleRoot: string, module: string): any { + const lambdaStylePath = path.resolve(appRoot, moduleRoot, module); + if (_canLoadAsFile(lambdaStylePath)) { + return require(lambdaStylePath); + } else { + // Why not just require(module)? + // Because require() is relative to __dirname, not process.cwd() + const nodeStylePath = require.resolve(module, { + paths: [appRoot, moduleRoot], + }); + return require(nodeStylePath); + } + } + + /** + * Load the user's application or throw a descriptive error. + * @throws Runtime errors in two cases + * 1 - UserCodeSyntaxError if there's a syntax error while loading the module + * 2 - ImportModuleError if the module cannot be found + */ + function _loadUserApp( + appRoot: string, + moduleRoot: string, + module: string + ): any { + try { + return _tryRequire(appRoot, moduleRoot, module); + } catch (e) { + if (e instanceof SyntaxError) { + throw new UserCodeSyntaxError(e); + // @ts-ignore + } else if (e.code !== undefined && e.code === "MODULE_NOT_FOUND") { + // @ts-ignore + throw new ImportModuleError(e); + } else { + throw e; + } + } + } + + function _throwIfInvalidHandler(fullHandlerString: string): void { + if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) { + throw new MalformedHandlerName( + `'${fullHandlerString}' is not a valid handler name. Use absolute paths when specifying root directories in handler names.` + ); + } + } + + /** + * Load the user's function with the approot and the handler string. + * @param appRoot {string} + * The path to the application root. + * @param handlerString {string} + * The user-provided handler function in the form 'module.function'. + * @return userFuction {function} + * The user's handler function. This function will be passed the event body, + * the context object, and the callback function. + * @throws In five cases:- + * 1 - if the handler string is incorrectly formatted an error is thrown + * 2 - if the module referenced by the handler cannot be loaded + * 3 - if the function in the handler does not exist in the module + * 4 - if a property with the same name, but isn't a function, exists on the + * module + * 5 - the handler includes illegal character sequences (like relative paths + * for traversing up the filesystem '..') + * Errors for scenarios known by the runtime, will be wrapped by Runtime.* errors. + */ + export const load = function ( + appRoot: string, + fullHandlerString: string + ) { + _throwIfInvalidHandler(fullHandlerString); + + const [moduleRoot, moduleAndHandler] = _moduleRootAndHandler( + fullHandlerString + ); + const [module, handlerPath] = _splitHandlerString(moduleAndHandler); + + const userApp = _loadUserApp(appRoot, moduleRoot, module); + const handlerFunc = _resolveHandler(userApp, handlerPath); + + if (!handlerFunc) { + throw new HandlerNotFound( + `${fullHandlerString} is undefined or not exported` + ); + } + + if (typeof handlerFunc !== "function") { + throw new HandlerNotFound(`${fullHandlerString} is not a function`); + } + + return handlerFunc; + }; \ No newline at end of file diff --git a/tslint.json b/tslint.json index 657150ac..69877b01 100644 --- a/tslint.json +++ b/tslint.json @@ -1,5 +1,10 @@ { "extends": "tslint:recommended", + "linterOptions": { + "exclude": [ + "**/runtime/**" + ] + }, "rules": { "interface-name": false, "variable-name": {