From a76337b05bfef68b8b1bc5268558bcd79036c9bc Mon Sep 17 00:00:00 2001 From: Nick Hinsch Date: Tue, 19 Oct 2021 16:56:51 -0400 Subject: [PATCH 1/4] Vendor the load function --- NOTICE | 3 + src/handler.ts | 4 +- src/runtime/errors.ts | 137 ++++++++++++++++++++++++++++ src/runtime/index.ts | 1 + src/runtime/user-function.ts | 167 +++++++++++++++++++++++++++++++++++ 5 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 src/runtime/errors.ts create mode 100644 src/runtime/index.ts create mode 100644 src/runtime/user-function.ts 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..b81b372a --- /dev/null +++ b/src/runtime/index.ts @@ -0,0 +1 @@ +export { load } from "./user-function"; \ No newline at end of file diff --git a/src/runtime/user-function.ts b/src/runtime/user-function.ts new file mode 100644 index 00000000..9d989aae --- /dev/null +++ b/src/runtime/user-function.ts @@ -0,0 +1,167 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Modifictions copyright 2021 Datadog, Inc. + * + * 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 From e8007c87a3c0462f21abe2efd2be4c0e841b11f0 Mon Sep 17 00:00:00 2001 From: Nick Hinsch Date: Tue, 19 Oct 2021 17:41:59 -0400 Subject: [PATCH 2/4] Add .prettierignore --- .prettierignore | 2 ++ src/runtime/index.ts | 2 +- src/runtime/user-function.ts | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .prettierignore 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/src/runtime/index.ts b/src/runtime/index.ts index b81b372a..10e9f9ab 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1 +1 @@ -export { load } from "./user-function"; \ No newline at end of file +export { load } from "./user-function"; diff --git a/src/runtime/user-function.ts b/src/runtime/user-function.ts index 9d989aae..9d6270cf 100644 --- a/src/runtime/user-function.ts +++ b/src/runtime/user-function.ts @@ -1,6 +1,9 @@ /** * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * Modifictions 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. From 73d6ddc568a3840b69c3e0cdccba5e95bdf81c87 Mon Sep 17 00:00:00 2001 From: Nick Hinsch Date: Tue, 19 Oct 2021 17:56:13 -0400 Subject: [PATCH 3/4] Exclude runtime from tslint --- tslint.json | 5 +++++ 1 file changed, 5 insertions(+) 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": { From d969973379bd59d5687d48be3ae81d7fc267cbb0 Mon Sep 17 00:00:00 2001 From: Nick Hinsch Date: Wed, 20 Oct 2021 14:23:44 -0400 Subject: [PATCH 4/4] Tweaks --- LICENSE-3rdparty.csv | 1 + src/runtime/user-function.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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/src/runtime/user-function.ts b/src/runtime/user-function.ts index 9d6270cf..1fd53403 100644 --- a/src/runtime/user-function.ts +++ b/src/runtime/user-function.ts @@ -1,6 +1,6 @@ /** * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * Modifictions copyright 2021 Datadog, Inc. + * 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