{"size": 9717, "ext": "js", "max_stars_repo_path": "src/StaticFileHandler.js", "max_stars_repo_name": "tomchiverton/serverless-aws-static-file-handler", "max_stars_repo_stars_event_min_datetime": null, "max_stars_repo_stars_event_max_datetime": null, "max_issues_repo_path": "src/StaticFileHandler.js", "max_issues_repo_name": "tomchiverton/serverless-aws-static-file-handler", "max_issues_repo_issues_event_min_datetime": null, "max_issues_repo_issues_event_max_datetime": null, "max_forks_repo_path": "src/StaticFileHandler.js", "max_forks_repo_name": "tomchiverton/serverless-aws-static-file-handler", "max_forks_repo_forks_event_min_datetime": null, "max_forks_repo_forks_event_max_datetime": null}
{"max_stars_count": null, "max_issues_count": null, "max_forks_count": null, "avg_line_length": 33.9755244755, "max_line_length": 198, "alphanum_fraction": 0.6497890295}
"use strict"
const assert = require("assert")
const fs = require("fs")
const mimetypes = require("mime-types")
const Mustache = require("mustache")
const path = require("path")
const util = require("util")
const _ = require("lodash")
const readFileAsync = util.promisify(fs.readFile)
const accessAsync = util.promisify(fs.access)

class StaticFileHandler {
  /**
   * Initializes a new instance of @see StaticFileHandler
   * @param {*string} clientFilesPath The fully qualified path to the client files that this module should serve.
   * @param {*string} customErrorPagePath Optional path to a custom error page. Must be relative to @see clientFilesPath .
   */
  constructor(clientFilesPath, customErrorPagePath = null) {
    if (clientFilesPath == null || clientFilesPath.length === 0) {
      throw new Error("clientFilesPath must be specified")
    }
    this.clientFilesPath = clientFilesPath
    this.customErrorPagePath = customErrorPagePath
  }

  static getMimeType(filePath) {
    return mimetypes.lookup(filePath) || "application/octet-stream"
  }

  static isBinaryType(mimeType) {
    const mimeCharset = mimetypes.charset(mimeType)
    /* Using https://w3techs.com/technologies/overview/character_encoding/all
     * to be more comprehensive go through those at https://www.iana.org/assignments/character-sets/character-sets.xhtml
     */
    const textualCharSets = [
      "UTF-8",
      "ISO-8859-1",
      "Windows-1251",
      "Windows-1252",
      "Shift_JIS",
      "GB2312",
      "EUC-KR",
      "ISO-8859-2",
      "GBK",
      "Windows-1250",
      "EUC-JP",
      "Big5",
      "ISO-8859-15",
      "Windows-1256",
      "ISO-8859-9",
    ]
    const found = textualCharSets.find(
      (cs) => 0 === cs.localeCompare(mimeCharset, "en", { sensitivity: "base" })
    )
    return found === undefined || found === null
  }

  async get(event, context) {
    if (!event) {
      throw new Error("event object not specified.")
    }

    if (event.rawPath) {
      // convert the V2 API to look like v1 like rest of code expects

      // this matches validateLambdaProxyIntegration required props
      event.resource = event.requestContext.http.path
      event.path = event.rawPath
      event.httpMethod = event.requestContext.http.method
      // OK as is event.headers
      event.multiValueHeaders = event.headers
      event.queryStringParameters = event.queryStringParamaters
      event.multiValueQueryStringParameters = event.queryStringParameters // Not sure what old code does ?
      // OK as is event.pathParameters
      event.stageVariables = event.requestContext.stage // Not sure we ever pass these ?
      // OK as is event.requestContext
      event.body = "" // It is a GET, there never is one ?
      // OK as is event.isBase64Encoded
    }

    if (!event.path) {
      throw new Error("Empty path.")
    }
    await StaticFileHandler.validateLambdaProxyIntegration(event)
    let requestPath
    if (event.pathParameters) {
      requestPath = ""
      /*
       * event.path is an object when `integration: lambda` and there is a greedy path parameter
       * If there are zero properties, it is just "lambda integration" and no path parameters
       * If there are properties, it indicates there are path parameters.
       * For example: The path parameter could be mapped like so in serverless.yml:
       * - http:
             path: fontsdir/{fonts+}
       * The {fonts+} in the path indicates the base path and tells APIG to pass along the whole path.
       */
      // now enumerate the properties of it:
      let propNames = Object.getOwnPropertyNames(event.pathParameters)
      if (propNames.length === 0) {
        const msg =
          "The event.path is an object but there are no properties. Check serverless.yml."
        throw new Error(msg)
      }
      if (propNames.length !== 1) {
        const msg = `Expected exactly one property name, but found: ${util.inspect(
          propNames
        )}. Check that you configured the pathParameter in serverless.yml with a plus sign like \`path/{pathparam+}\`.`
        throw new Error(msg)
      }
      requestPath = "/" + event.pathParameters[propNames[0]]
    } else {
      assert(typeof event.path === "string", "expected path to be string")
      requestPath = event.path
    }
    let filePath = path.join(this.clientFilesPath, requestPath)
    return this.readFileAsResponse(filePath, context).catch((err) => {
      throw new Error(
        `Unable to read client file '${requestPath}'. Error: ${err}`
      )
    })
  }

  /**
   * Loads the specified file's content and returns a response that can be called back to lambda for sending the file as the http response.
   */
  async readFileAsResponse(filePath, context, statusCode = 200) {
    let stream
    try {
      stream = await readFileAsync(filePath)
    } catch (err) {
      if (err.code === "ENOENT") {
        // NOTE: avoid leaking full local path
        const fileName = path.basename(filePath)
        return this.responseAsError(`File ${fileName} does not exist`, 404)
      }
    }
    let mimeType = StaticFileHandler.getMimeType(filePath)
    return StaticFileHandler.readStreamAsResponse(
      stream,
      context,
      statusCode,
      mimeType
    )
  }

  static readStreamAsResponse(stream, context, statusCode, mimeType) {
    let body
    let isBase64Encoded = false
    if (StaticFileHandler.isBinaryType(mimeType)) {
      isBase64Encoded = true
      body = Buffer.from(stream).toString("base64")
    } else {
      body = stream.toString("utf8")
    }
    return StaticFileHandler.readStringAsResponse(
      body,
      context,
      statusCode,
      mimeType,
      isBase64Encoded
    )
  }

  static readStringAsResponse(
    stringData,
    context,
    statusCode,
    mimeType,
    isBase64Encoded
  ) {
    assert(mimeType, "expected mimeType to always be provided")
    if (
      context &&
      "staticFileHandler" in context &&
      "viewData" in context.staticFileHandler
    ) {
      const viewData = context.staticFileHandler.viewData
      stringData = Mustache.render(stringData, viewData)
    }
    const response = {
      statusCode: statusCode,
      headers: {
        "Content-Type": mimeType,
      },
      isBase64Encoded,
      body: stringData,
    }
    return response
  }

  /**
   * Returns a Promise with a response that is an HTML page with the specified error text on it.
   * @param {*string} errorText The error to add to the page.
   */
  async responseAsError(errorText, statusCode) {
    const context = {
      staticFileHandler: {
        viewData: {
          errorText: errorText,
        },
      },
    }
    if (this.customErrorPagePath) {
      let filePath = path.join(this.clientFilesPath, this.customErrorPagePath)
      try {
        await accessAsync(filePath, fs.constants.R_OK)
        return this.readFileAsResponse(filePath, context, statusCode)
      } catch (err) {
        console.warn(
          "serverless-aws-static-file-handler: Error using customErrorPagePath",
          this.customErrorPagePath,
          ". Using fallback error HTML."
        )
      }
    }

    const DEFAULT_ERROR_HTML = `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Error</title>
  </head>

  <body>
    {{errorText}}
  </body>
</html>
`
    return StaticFileHandler.readStringAsResponse(
      DEFAULT_ERROR_HTML,
      context,
      statusCode,
      "text/html",
      false
    )
  }

  /**
   * Rejects if the specified event is not Lambda Proxy integration
   */
  static async validateLambdaProxyIntegration(event) {
    /* 
    There are two different event schemas in API Gateway + Lambda Proxy APIs. One is known as "REST API" or the old V1 API and the newer one is the V2 or "HTTP API". 
    Each are described at https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html#services-apigateway-apitypes
    You can see examples of each at https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
    TLDR: 
    - V2 has a { version: "2.0" } field and { requestContext.http.method: "..." }  field.
    - V1 has a { version: "1.0" } field and { requestContext.httpMethod: "..." }  field.
    To set each up in serverless.com:
    - V1: https://www.serverless.com/framework/docs/providers/aws/events/apigateway
    - V2: https://www.serverless.com/framework/docs/providers/aws/events/http-api
    */
    function isV2ProxyAPI(evt) {
      return (
        evt.version === "2.0" &&
        typeof _.get(evt, "requestContext.http.method") === "string"
      )
    }
    function isV1ProxyAPI(evt) {
      return (
        // docs say there is a .version but there isn't!
        // evt.version === "1.0" &&
        typeof _.get(evt, "requestContext.httpMethod") === "string"
      )
    }
    // serverless-offline doesn't provide the `isBase64Encoded` prop, but does add the isOffline. Fixes issue #10: https://github.com/activescott/serverless-aws-static-file-handler/issues/10
    const isServerlessOfflineEnvironment = "isOffline" in event
    if (!isV1ProxyAPI(event) && !isV2ProxyAPI(event)) {
      const logProps = [
        "version",
        "requestContext.httpMethod",
        "requestContext.http.method",
      ]
      const addendum = logProps
        .map((propName) => `event.${propName} was '${_.get(event, propName)}'`)
        .join(" ")
      throw new Error(
        "API Gateway method does not appear to be setup for Lambda Proxy Integration. Please confirm that `integration` property of the http event is not specified or set to `integration: proxy`." +
          addendum
      )
    }
  }
}

module.exports = StaticFileHandler
