File Explorer

/var/runtime/node_modules/@aws-sdk/node_modules/undici/lib/interceptor

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

decompress.js8.0 KB · 260 lines
'use strict' const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib')const { pipeline } = require('node:stream')const DecoratorHandler = require('../handler/decorator-handler')const { runtimeFeatures } = require('../util/runtime-features') /** @typedef {import('node:stream').Transform} Transform *//** @typedef {import('node:stream').Transform} Controller *//** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */ /** @type {Record<string, () => DecompressorStream>} */const supportedEncodings = {  gzip: createGunzip,  'x-gzip': createGunzip,  br: createBrotliDecompress,  deflate: createInflate,  compress: createInflate,  'x-compress': createInflate,  ...(runtimeFeatures.has('zstd') ? { zstd: createZstdDecompress } : {})} const defaultSkipStatusCodes = /** @type {const} */ ([204, 304]) let warningEmitted = /** @type {boolean} */ (false) /** * @typedef {Object} DecompressHandlerOptions * @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400) */ class DecompressHandler extends DecoratorHandler {  /** @type {Transform[]} */  #decompressors = []  /** @type {Readonly<number[]>} */  #skipStatusCodes  /** @type {boolean} */  #skipErrorResponses   constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true } = {}) {    super(handler)    this.#skipStatusCodes = skipStatusCodes    this.#skipErrorResponses = skipErrorResponses  }   /**   * Determines if decompression should be skipped based on encoding and status code   * @param {string} contentEncoding - Content-Encoding header value   * @param {number} statusCode - HTTP status code of the response   * @returns {boolean} - True if decompression should be skipped   */  #shouldSkipDecompression (contentEncoding, statusCode) {    if (!contentEncoding || statusCode < 200) return true    if (this.#skipStatusCodes.includes(statusCode)) return true    if (this.#skipErrorResponses && statusCode >= 400) return true    return false  }   /**   * Creates a chain of decompressors for multiple content encodings   *   * @param {string} encodings - Comma-separated list of content encodings   * @returns {Array<DecompressorStream>} - Array of decompressor streams   * @throws {Error} - If the number of content-encodings exceeds the maximum allowed   */  #createDecompressionChain (encodings) {    const parts = encodings.split(',')     // Limit the number of content-encodings to prevent resource exhaustion.    // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).    const maxContentEncodings = 5    if (parts.length > maxContentEncodings) {      throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`)    }     /** @type {DecompressorStream[]} */    const decompressors = []     for (let i = parts.length - 1; i >= 0; i--) {      const encoding = parts[i].trim()      if (!encoding) continue       if (!supportedEncodings[encoding]) {        decompressors.length = 0 // Clear if unsupported encoding        return decompressors // Unsupported encoding      }       decompressors.push(supportedEncodings[encoding]())    }     return decompressors  }   /**   * Sets up event handlers for a decompressor stream using readable events   * @param {DecompressorStream} decompressor - The decompressor stream   * @param {Controller} controller - The controller to coordinate with   * @returns {void}   */  #setupDecompressorEvents (decompressor, controller) {    decompressor.on('readable', () => {      let chunk      while ((chunk = decompressor.read()) !== null) {        const result = super.onResponseData(controller, chunk)        if (result === false) {          break        }      }    })     decompressor.on('error', (error) => {      super.onResponseError(controller, error)    })  }   /**   * Sets up event handling for a single decompressor   * @param {Controller} controller - The controller to handle events   * @returns {void}   */  #setupSingleDecompressor (controller) {    const decompressor = this.#decompressors[0]    this.#setupDecompressorEvents(decompressor, controller)     decompressor.on('end', () => {      super.onResponseEnd(controller, {})    })  }   /**   * Sets up event handling for multiple chained decompressors using pipeline   * @param {Controller} controller - The controller to handle events   * @returns {void}   */  #setupMultipleDecompressors (controller) {    const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]    this.#setupDecompressorEvents(lastDecompressor, controller)     pipeline(this.#decompressors, (err) => {      if (err) {        super.onResponseError(controller, err)        return      }      super.onResponseEnd(controller, {})    })  }   /**   * Cleans up decompressor references to prevent memory leaks   * @returns {void}   */  #cleanupDecompressors () {    this.#decompressors.length = 0  }   /**   * @param {Controller} controller   * @param {number} statusCode   * @param {Record<string, string | string[] | undefined>} headers   * @param {string} statusMessage   * @returns {void}   */  onResponseStart (controller, statusCode, headers, statusMessage) {    const contentEncoding = headers['content-encoding']     // If content encoding is not supported or status code is in skip list    if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {      return super.onResponseStart(controller, statusCode, headers, statusMessage)    }     const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())     if (decompressors.length === 0) {      this.#cleanupDecompressors()      return super.onResponseStart(controller, statusCode, headers, statusMessage)    }     this.#decompressors = decompressors     // Remove compression headers since we're decompressing    const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers     if (this.#decompressors.length === 1) {      this.#setupSingleDecompressor(controller)    } else {      this.#setupMultipleDecompressors(controller)    }     return super.onResponseStart(controller, statusCode, newHeaders, statusMessage)  }   /**   * @param {Controller} controller   * @param {Buffer} chunk   * @returns {void}   */  onResponseData (controller, chunk) {    if (this.#decompressors.length > 0) {      this.#decompressors[0].write(chunk)      return    }    super.onResponseData(controller, chunk)  }   /**   * @param {Controller} controller   * @param {Record<string, string | string[]> | undefined} trailers   * @returns {void}   */  onResponseEnd (controller, trailers) {    if (this.#decompressors.length > 0) {      this.#decompressors[0].end()      this.#cleanupDecompressors()      return    }    super.onResponseEnd(controller, trailers)  }   /**   * @param {Controller} controller   * @param {Error} err   * @returns {void}   */  onResponseError (controller, err) {    if (this.#decompressors.length > 0) {      for (const decompressor of this.#decompressors) {        decompressor.destroy(err)      }      this.#cleanupDecompressors()    }    super.onResponseError(controller, err)  }} /** * Creates a decompression interceptor for HTTP responses * @param {DecompressHandlerOptions} [options] - Options for the interceptor * @returns {Function} - Interceptor function */function createDecompressInterceptor (options = {}) {  // Emit experimental warning only once  if (!warningEmitted) {    process.emitWarning(      'DecompressInterceptor is experimental and subject to change',      'ExperimentalWarning'    )    warningEmitted = true  }   return (dispatch) => {    return (opts, handler) => {      const decompressHandler = new DecompressHandler(handler, options)      return dispatch(opts, decompressHandler)    }  }} module.exports = createDecompressInterceptor