repo_description: Actionhero is a realtime multi-transport nodejs API Server with integrated cluster capabilities and delayed tasks
repo_topics: actionhero api api-server async-await cluster delayed-jobs delayed-tasks framework frameworks http https javascript nodejs server socket-client tcp ts typescript websocket
code: import * as http from "http";
  import * as https from "https";
  import * as url from "url";
  import * as qs from "qs";
  import * as fs from "fs";
  import * as zlib from "zlib";
  import * as path from "path";
  import * as formidable from "formidable";
  import * as Mime from "mime";
  import * as uuid from "uuid";
  import * as etag from "etag";
  import { BrowserFingerprint } from "browser_fingerprint";
  import { api, config, utils, Server, Connection } from "../index";
  import { ActionsStatus, ActionProcessor } from "../classes/actionProcessor";
  
  export class WebServer extends Server {
    server: http.Server | https.Server;
    fingerPrinter: BrowserFingerprint;
    sockets: { [id: string]: any };
  
    constructor() {
      super();
      this.type = "web";
      this.sockets = {};
  
      this.attributes = {
        canChat: false,
        logConnections: false,
        logExits: false,
        sendWelcomeMessage: false,
        verbs: [], // no verbs for connections of this type, as they are to be very short-lived
      };
  
      this.connectionCustomMethods = {
        setHeader: (
          connection: Connection,
          key: string,
          value: string | number,
        ) => {
          connection.rawConnection.res.setHeader(key, value);
        },
  
        setStatusCode: (connection: Connection, value: number) => {
          connection.rawConnection.responseHttpCode = value;
        },
  
        pipe: (
          connection: Connection,
          buffer: string | Buffer,
          headers: Record<string, string>,
        ) => {
          for (const k in headers) {
            connection.setHeader(k, headers[k]);
          }
          if (typeof buffer === "string") {
            buffer = Buffer.from(buffer);
          }
          connection.rawConnection.res.end(buffer);
        },
      };
    }
  
    async initialize() {
      if (["api", "file"].indexOf(this.config.rootEndpointType) < 0) {
        throw new Error("rootEndpointType can only be 'api' or 'file'");
      }
  
      if (
        !this.config.urlPathForFiles &&
        this.config.rootEndpointType === "file"
      ) {
        throw new Error(
          'rootEndpointType cannot be "file" without a urlPathForFiles',
        );
      }
  
      this.fingerPrinter = new BrowserFingerprint(this.config.fingerprintOptions);
    }
  
    async start() {
      let bootAttempts = 0;
      if (this.config.secure === false) {
        this.server = http.createServer((req, res) => {
          this.handleRequest(req, res);
        });
      } else {
        this.server = https.createServer(
          this.config.serverOptions,
          (req, res) => {
            this.handleRequest(req, res);
          },
        );
      }
  
      this.server.on("error", (error) => {
        bootAttempts++;
        if (bootAttempts < this.config.bootAttempts) {
          this.log(`cannot boot web server; trying again [${error}]`, "error");
          if (bootAttempts === 1) {
            this.cleanSocket(this.config.bindIP, this.config.port);
          }
          setTimeout(() => {
            this.log("attempting to boot again..");
            this.server.listen(this.config.port, this.config.bindIP);
          }, 1000);
        } else {
          throw new Error(
            `cannot start web server @ ${this.config.bindIP}:${this.config.port} => ${error}`,
          );
        }
      });
  
      let socketCounter = 0;
      this.server.on("connection", (socket) => {
        const id = socketCounter;
        this.sockets[id] = socket;
        socket.on("close", () => delete this.sockets[id]);
        socketCounter++;
      });
  
      await new Promise((resolve) => {
        this.server.listen(this.config.port, this.config.bindIP, () => {
          this.chmodSocket(this.config.bindIP, this.config.port);
          resolve(null);
        });
      });
  
      this.on("connection", async (connection: Connection) => {
        const requestMode = await this.determineRequestParams(connection);
        if (requestMode === "api") {
          this.processAction(connection);
        } else if (requestMode === "file") {
          this.processFile(connection);
        } else if (requestMode === "options") {
          this.respondToOptions(connection);
        } else if (requestMode === "trace") {
          this.respondToTrace(connection);
        }
      });
  
      this.on("actionComplete", this.completeResponse);
    }
  
    async stop() {
      if (!this.server) return;
  
      await new Promise((resolve) => {
        this.server.close(resolve);
        for (const socket of Object.values(this.sockets)) {
          socket.destroy();
        }
      });
    }
  
    async sendMessage(connection: Connection, message: string) {
      let stringResponse = "";
      if (connection.rawConnection.method !== "HEAD") {
        stringResponse = String(message);
      }
  
      this.cleanHeaders(connection);
      const headers = connection.rawConnection.responseHeaders;
      const responseHttpCode = parseInt(
        connection.rawConnection.responseHttpCode,
      );
  
      this.sendWithCompression(
        connection,
        responseHttpCode,
        headers,
        stringResponse,
      );
    }
  
    async sendFile(
      connection: Connection,
      error: NodeJS.ErrnoException,
      fileStream: any,
      mime: string,
      length: number,
      lastModified: Date,
    ) {
      let foundCacheControl = false;
      let ifModifiedSince;
  
      connection.rawConnection.responseHeaders.forEach((pair: string[]) => {
        if (pair[0].toLowerCase() === "cache-control") {
          foundCacheControl = true;
        }
      });
  
      connection.rawConnection.responseHeaders.push(["Content-Type", mime]);
  
      if (fileStream) {
        if (!foundCacheControl) {
          connection.rawConnection.responseHeaders.push([
            "Cache-Control",
            "max-age=" +
              this.config.flatFileCacheDuration +
              ", must-revalidate, public",
          ]);
        }
      }
  
      if (fileStream && !this.config.enableEtag) {
        if (lastModified) {
          connection.rawConnection.responseHeaders.push([
            "Last-Modified",
            new Date(lastModified).toUTCString(),
          ]);
        }
      }
  
      this.cleanHeaders(connection);
      const headers = connection.rawConnection.responseHeaders;
      const reqHeaders = connection.rawConnection.req.headers;
  
      const sendRequestResult = () => {
        const responseHttpCode = parseInt(
          connection.rawConnection.responseHttpCode,
          10,
        );
        if (error) {
          this.sendWithCompression(
            connection,
            responseHttpCode,
            headers,
            String(error),
          );
        } else if (responseHttpCode !== 304) {
          this.sendWithCompression(
            connection,
            responseHttpCode,
            headers,
            null,
            fileStream,
            length,
          );
        } else {
          connection.rawConnection.res.writeHead(
            responseHttpCode,
            this.transformHeaders(headers),
          );
          connection.rawConnection.res.end();
          connection.destroy();
          fileStream.close();
        }
      };
  
      if (error) {
        connection.rawConnection.responseHttpCode = 404;
        return sendRequestResult();
      }
  
      if (reqHeaders["if-modified-since"]) {
        ifModifiedSince = new Date(reqHeaders["if-modified-since"]);
        lastModified.setMilliseconds(0);
        if (lastModified <= ifModifiedSince) {
          connection.rawConnection.responseHttpCode = 304;
        }
        return sendRequestResult();
      }
  
      if (this.config.enableEtag && fileStream && fileStream.path) {
        const fileStats: fs.Stats = await new Promise((resolve) => {
          fs.stat(fileStream.path, (error, fileStats) => {
            if (error || !fileStats) {
              this.log(
                "Error receving file statistics: " + String(error),
                "error",
              );
            }
            return resolve(fileStats);
          });
        });
  
        if (!fileStats) return sendRequestResult();
  
        const fileEtag = etag(fileStats, { weak: true });
        connection.rawConnection.responseHeaders.push(["ETag", fileEtag]);
        let noneMatchHeader = reqHeaders["if-none-match"];
        const cacheCtrlHeader = reqHeaders["cache-control"];
        let noCache = false;
        let etagMatches;
        // check for no-cache cache request directive
        if (cacheCtrlHeader && cacheCtrlHeader.indexOf("no-cache") !== -1) {
          noCache = true;
        }
        // parse if-none-match
        if (noneMatchHeader) {
          noneMatchHeader = noneMatchHeader.split(/ *, */);
        }
        // if-none-match
        if (noneMatchHeader) {
          etagMatches = noneMatchHeader.some((match: string) => {
            return (
              match === "*" || match === fileEtag || match === "W/" + fileEtag
            );
          });
        }
        if (etagMatches && !noCache) {
          connection.rawConnection.responseHttpCode = 304;
        }
        sendRequestResult();
      } else {
        sendRequestResult();
      }
    }
  
    sendWithCompression(
      connection: Connection,
      responseHttpCode: number,
      headers: Array<[string, string | number]>,
      stringResponse: string,
      fileStream?: any,
      fileLength?: number,
    ) {
      let acceptEncoding =
        connection.rawConnection.req.headers["accept-encoding"];
      let compressor;
      let stringEncoder;
      if (!acceptEncoding) {
        acceptEncoding = "";
      }
  
      // Note: this is not a conforming accept-encoding parser.
      // https://nodejs.org/api/zlib.html#zlib_zlib_createinflate_options
      // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
      if (this.config.compress === true) {
        const gzipMatch = acceptEncoding.match(/\bgzip\b/);
        const deflateMatch = acceptEncoding.match(/\bdeflate\b/);
        if (
          (gzipMatch && !deflateMatch) ||
          (gzipMatch && deflateMatch && gzipMatch.index < deflateMatch.index)
        ) {
          headers.push(["Content-Encoding", "gzip"]);
          compressor = zlib.createGzip();
          stringEncoder = zlib.gzip;
        } else if (
          (!gzipMatch && deflateMatch) ||
          (gzipMatch && deflateMatch && deflateMatch.index < gzipMatch.index)
        ) {
          headers.push(["Content-Encoding", "deflate"]);
          compressor = zlib.createDeflate();
          stringEncoder = zlib.deflate;
        }
      }
  
      // the 'finish' event denotes a successful transfer
      connection.rawConnection.res.on("finish", () => {
        connection.destroy();
      });
  
      // the 'close' event denotes a failed transfer, but it is probably the client's fault
      connection.rawConnection.res.on("close", () => {
        connection.destroy();
      });
  
      if (fileStream) {
        if (compressor) {
          connection.rawConnection.res.writeHead(
            responseHttpCode,
            this.transformHeaders(headers),
          );
          fileStream.pipe(compressor).pipe(connection.rawConnection.res);
        } else {
          if (fileLength) {
            headers.push(["Content-Length", fileLength]);
          }
          connection.rawConnection.res.writeHead(
            responseHttpCode,
            this.transformHeaders(headers),
          );
          fileStream.pipe(connection.rawConnection.res);
        }
      } else {
        if (stringEncoder) {
          stringEncoder(stringResponse, (error, zippedString) => {
            if (error) {
              console.error(error);
            }
            headers.push(["Content-Length", zippedString.length]);
            connection.rawConnection.res.writeHead(
              responseHttpCode,
              this.transformHeaders(headers),
            );
            connection.rawConnection.res.end(zippedString);
          });
        } else {
          headers.push(["Content-Length", Buffer.byteLength(stringResponse)]);
          connection.rawConnection.res.writeHead(
            responseHttpCode,
            this.transformHeaders(headers),
          );
          connection.rawConnection.res.end(stringResponse);
        }
      }
    }
  
    handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
      const {
        fingerprint,
        headersHash,
      }: { fingerprint: string; headersHash: Record<string, string> } =
        this.fingerPrinter.fingerprint(req);
      const responseHeaders = [];
      const cookies = utils.parseCookies(req);
      const responseHttpCode = 200;
      const method = req.method.toUpperCase();
  
      // waiting until URL() can handle relative paths
      // https://github.com/nodejs/node/issues/12682
      const parsedURL = url.parse(req.url, true);
      let i;
      for (i in headersHash) {
        responseHeaders.push([i, headersHash[i]]);
      }
  
      // https://github.com/actionhero/actionhero/issues/189
      responseHeaders.push(["Content-Type", "application/json; charset=utf-8"]);
  
      for (i in this.config.httpHeaders) {
        if (this.config.httpHeaders[i]) {
          responseHeaders.push([i, this.config.httpHeaders[i]]);
        }
      }
  
      // check if this request (http://other-host.com) is in allowedRequestHosts ([https://host.com])
      if (
        this.config.allowedRequestHosts &&
        this.config.allowedRequestHosts.length > 0
      ) {
        const requestHost = req.headers["x-forwarded-proto"]
          ? req.headers["x-forwarded-proto"] + "://" + req.headers.host
          : (this.config.secure ? "https://" : "http://") + req.headers.host;
  
        if (!this.config.allowedRequestHosts.includes(requestHost)) {
          const newHost = this.config.allowedRequestHosts[0];
          res.statusCode = 302;
          res.setHeader("Location", newHost + req.url);
          return res.end(`You are being redirected to ${newHost + req.url}\r\n`);
        }
      }
  
      const { ip, port } = utils.parseHeadersForClientAddress(req.headers);
      const messageId = uuid.v4();
  
      this.buildConnection({
        rawConnection: {
          req: req,
          res: res,
          params: {},
          method: method,
          cookies: cookies,
          responseHeaders: responseHeaders,
          responseHttpCode: responseHttpCode,
          parsedURL: parsedURL,
        },
        id: `${fingerprint}-${messageId}`,
        messageId: messageId,
        fingerprint: fingerprint,
        remoteAddress: ip || req.connection.remoteAddress || "0.0.0.0",
        remotePort: port || req.connection.remotePort || "0",
      });
    }
  
    async completeResponse(data: ActionProcessor<any>) {
      if (data.toRender !== true) {
        if (data.connection.rawConnection.res.finished) {
          data.connection.destroy();
        } else {
          data.connection.rawConnection.res.on("finish", () =>
            data.connection.destroy(),
          );
          data.connection.rawConnection.res.on("close", () =>
            data.connection.destroy(),
          );
        }
  
        return;
      }
  
      if (
        this.config.metadataOptions.serverInformation &&
        typeof data.response !== "string"
      ) {
        data.response.serverInformation = this.buildServerInformation(
          data.connection.connectedAt,
        );
      }
  
      if (
        this.config.metadataOptions.requesterInformation &&
        typeof data.response !== "string"
      ) {
        data.response.requesterInformation = this.buildRequesterInformation(
          data.connection,
        );
      }
  
      if (data.response.error) {
        if (
          this.config.returnErrorCodes === true &&
          data.connection.rawConnection.responseHttpCode === 200
        ) {
          const customErrorCode = parseInt(data.response.error.code, 10);
          const isValidCustomResponseCode =
            customErrorCode >= 100 && customErrorCode < 600;
          if (isValidCustomResponseCode) {
            data.connection.rawConnection.responseHttpCode = customErrorCode;
          } else if (data.actionStatus === ActionsStatus.UnknownAction) {
            data.connection.rawConnection.responseHttpCode = 404;
          } else if (data.actionStatus === ActionsStatus.MissingParams) {
            data.connection.rawConnection.responseHttpCode = 422;
          } else {
            data.connection.rawConnection.responseHttpCode =
              this.config.defaultErrorStatusCode ?? 500;
          }
        }
      }
  
      if (
        !data.response.error &&
        data.action &&
        data.params.apiVersion &&
        api.actions.actions[data.params.action][data.params.apiVersion]
          .matchExtensionMimeType === true &&
        data.connection.extension
      ) {
        const mime = Mime.getType(data.connection.extension);
        if (mime) {
          data.connection.rawConnection.responseHeaders.push([
            "Content-Type",
            mime,
          ]);
        }
      }
  
      if (data.response.error) {
        data.response.error = await config.errors.serializers.servers.web(
          data.response.error,
        );
      }
  
      let stringResponse = "";
  
      if (this.extractHeader(data.connection, "Content-Type").match(/json/)) {
        stringResponse = JSON.stringify(data.response, null, this.config.padding);
        if (data.params.callback) {
          data.connection.rawConnection.responseHeaders.push([
            "Content-Type",
            "application/javascript",
          ]);
          stringResponse =
            this.callbackHtmlEscape(data.connection.params.callback) +
            "(" +
            stringResponse +
            ");";
        }
      } else {
        stringResponse = data.response as unknown as string;
      }
  
      this.sendMessage(data.connection, stringResponse);
    }
  
    extractHeader(connection: Connection, match: string) {
      let i = connection.rawConnection.responseHeaders.length - 1;
      while (i >= 0) {
        if (
          connection.rawConnection.responseHeaders[i][0].toLowerCase() ===
          match.toLowerCase()
        ) {
          return connection.rawConnection.responseHeaders[i][1];
        }
        i--;
      }
      return null;
    }
  
    respondToOptions(connection: Connection) {
      if (
        !this.config.httpHeaders["Access-Control-Allow-Methods"] &&
        !this.extractHeader(connection, "Access-Control-Allow-Methods")
      ) {
        const methods = "HEAD, GET, POST, PATCH, PUT, DELETE, OPTIONS, TRACE";
        connection.rawConnection.responseHeaders.push([
          "Access-Control-Allow-Methods",
          methods,
        ]);
      }
  
      if (
        !this.config.httpHeaders["Access-Control-Allow-Origin"] &&
        !this.extractHeader(connection, "Access-Control-Allow-Origin")
      ) {
        const origin = "*";
        connection.rawConnection.responseHeaders.push([
          "Access-Control-Allow-Origin",
          origin,
        ]);
      }
  
      this.sendMessage(connection, "");
    }
  
    respondToTrace(connection: Connection) {
      const data = this.buildRequesterInformation(connection);
      const stringResponse = JSON.stringify(data, null, this.config.padding);
      this.sendMessage(connection, stringResponse);
    }
  
    async determineRequestParams(connection: Connection) {
      // determine file or api request
      let requestMode = this.config.rootEndpointType;
      const pathname = connection.rawConnection.parsedURL.pathname;
      const pathParts = pathname.split("/");
      let i;
  
      while (pathParts[0] === "") {
        pathParts.shift();
      }
      if (pathParts[pathParts.length - 1] === "") {
        pathParts.pop();
      }
  
      let urlPathForActionsParts = [];
      if (this.config.urlPathForActions) {
        urlPathForActionsParts = this.config.urlPathForActions.split("/");
        while (urlPathForActionsParts[0] === "") {
          urlPathForActionsParts.shift();
        }
      }
  
      let urlPathForFilesParts = [];
      if (this.config.urlPathForFiles) {
        urlPathForFilesParts = this.config.urlPathForFiles.split("/");
        while (urlPathForFilesParts[0] === "") {
          urlPathForFilesParts.shift();
        }
      }
  
      if (
        pathParts[0] &&
        utils.arrayStartingMatch(urlPathForActionsParts, pathParts)
      ) {
        requestMode = "api";
        for (i = 0; i < urlPathForActionsParts.length; i++) {
          pathParts.shift();
        }
      } else if (
        pathParts[0] &&
        utils.arrayStartingMatch(urlPathForFilesParts, pathParts)
      ) {
        requestMode = "file";
        for (i = 0; i < urlPathForFilesParts.length; i++) {
          pathParts.shift();
        }
      }
  
      const extensionParts =
        connection.rawConnection.parsedURL.pathname.split(".");
      if (extensionParts.length > 1) {
        connection.extension = extensionParts[extensionParts.length - 1];
      }
  
      // OPTIONS
      if (connection.rawConnection.method === "OPTIONS") {
        requestMode = "options";
        return requestMode;
      }
  
      // API
      if (requestMode === "api") {
        if (connection.rawConnection.method === "TRACE") {
          requestMode = "trace";
        }
  
        let search = "";
        if (connection.rawConnection.parsedURL.search) {
          search = connection.rawConnection.parsedURL.search.slice(1);
        }
  
        this.fillParamsFromWebRequest(
          connection,
          qs.parse(search, this.config.queryParseOptions),
        );
        connection.rawConnection.params.query =
          connection.rawConnection.parsedURL.query;
        if (
          connection.rawConnection.method !== "GET" &&
          connection.rawConnection.method !== "HEAD" &&
          (connection.rawConnection.req.headers["content-type"] ||
            connection.rawConnection.req.headers["Content-Type"])
        ) {
          connection.rawConnection.form = new formidable.IncomingForm();
          if (this.config?.formOptions) {
            for (i in this.config.formOptions) {
              connection.rawConnection.form.options[i] =
                this.config.formOptions[i];
            }
          }
  
          let rawBody = Promise.resolve(Buffer.alloc(0));
          if (this.config.saveRawBody) {
            rawBody = new Promise((resolve, reject) => {
              let fullBody = Buffer.alloc(0);
              connection.rawConnection.req
                .on("data", (chunk: Uint8Array) => {
                  fullBody = Buffer.concat([fullBody, chunk]);
                })
                .on("end", () => {
                  resolve(fullBody);
                });
            });
          }
  
          const { fields, files } = (await new Promise((resolve) => {
            connection.rawConnection.form.parse(
              connection.rawConnection.req,
              (
                error: NodeJS.ErrnoException,
                fields: string[],
                files: string[],
              ) => {
                if (error) {
                  this.log("error processing form: " + String(error), "error");
                  connection.error = new Error(
                    "There was an error processing this form.",
                  );
                }
  
                // this is for backward compatibility formidable v3 and v2,
                // because in v3 was deleted `multiples` option and mechanism
                const isMultiples = Boolean(this.config?.formOptions?.multiples);
                if (isMultiples) {
                  resolve({ fields, files });
                } else {
                  // reimplementing firstValues values helper
                  // @see https://github.com/node-formidable/formidable/blob/master/src/helpers/firstValues.js
                  // but instead of first we are taking last values, mimicking v2 behavior
                  const lastValues = (val: Record<string, any>) => {
                    return Object.fromEntries(
                      Object.entries(val).map(([key, value]) => {
                        return [key, Array.isArray(value) ? value.at(-1) : value];
                      }),
                    );
                  };
  
                  resolve({
                    // @ts-expect-error wrong result type
                    fields: lastValues(fields),
                    // @ts-expect-error wrong result type
                    files: lastValues(files),
                  });
                }
              },
            );
            // looks like wrong types here
          })) as { fields: string[]; files: string[] };
  
          connection.rawConnection.params.body = fields;
          connection.rawConnection.params.rawBody = await rawBody;
          connection.rawConnection.params.files = files;
          this.fillParamsFromWebRequest(connection, files);
          this.fillParamsFromWebRequest(connection, fields);
          connection.params.action = null;
          api.routes.processRoute(connection, pathParts);
          return requestMode;
        } else {
          connection.params.action = null;
          api.routes.processRoute(connection, pathParts);
          return requestMode;
        }
      }
  
      // FILE
      if (requestMode === "file") {
        api.routes.processRoute(connection, pathParts);
        if (!connection.params.file) {
          connection.params.file = pathParts.join(path.sep);
        }
        if (
          connection.params.file === "" ||
          connection.params.file[connection.params.file.length - 1] === "/"
        ) {
          connection.params.file =
            connection.params.file + config.general.directoryFileType;
        }
        try {
          connection.params.file = decodeURIComponent(connection.params.file);
        } catch (e) {
          connection.error = new Error("There was an error decoding URI: " + e);
        }
        return requestMode;
      }
    }
  
    fillParamsFromWebRequest(
      connection: Connection,
      varsHash: Record<string, any>,
    ) {
      // helper for JSON posts
      const collapsedVarsHash = utils.collapseObjectToArray(varsHash);
      if (collapsedVarsHash !== false) {
        varsHash = { payload: collapsedVarsHash }; // post was an array, lets call it "payload"
      }
  
      for (const v in varsHash) {
        connection.params[v] = varsHash[v];
      }
    }
  
    transformHeaders(headersArray: Array<[string, string | number]>) {
      return headersArray.reduce(
        (headers: Record<string, string[]>, currentHeader) => {
          const currentHeaderKey = currentHeader[0].toLowerCase();
          // we have a set-cookie, let's see what we have to do
          if (currentHeaderKey === "set-cookie") {
            if (headers[currentHeaderKey]) {
              headers[currentHeaderKey].push(currentHeader[1].toString());
            } else {
              headers[currentHeaderKey] = [currentHeader[1].toString()];
            }
          } else {
            headers[currentHeaderKey] = [currentHeader[1].toString()];
          }
  
          return headers;
        },
        {},
      );
    }
  
    buildServerInformation(connectedAt: number) {
      const stopTime = new Date().getTime();
      return {
        serverName: config.general.serverName,
        apiVersion: config.general.apiVersion,
        requestDuration: stopTime - connectedAt,
        currentTime: stopTime,
      };
    }
  
    buildRequesterInformation(connection: Connection) {
      const requesterInformation = {
        id: connection.id,
        fingerprint: connection.fingerprint,
        messageId: connection.messageId,
        remoteIP: connection.remoteIP,
        receivedParams: {} as { [key: string]: any },
      };
  
      for (const p in connection.params) {
        if (
          config.general.disableParamScrubbing === true ||
          api.params.postVariables.indexOf(p) >= 0
        ) {
          requesterInformation.receivedParams[p] = connection.params[p];
        }
      }
  
      return requesterInformation;
    }
  
    cleanHeaders(connection: Connection) {
      const originalHeaders = connection.rawConnection.responseHeaders.reverse();
      const foundHeaders = [];
      const cleanedHeaders = [];
      for (const i in originalHeaders) {
        const key = originalHeaders[i][0];
        const value = originalHeaders[i][1];
        if (
          foundHeaders.indexOf(key.toLowerCase()) >= 0 &&
          key.toLowerCase().indexOf("set-cookie") < 0
        ) {
          // ignore, it's a duplicate
        } else if (
          connection.rawConnection.method === "HEAD" &&
          key === "Transfer-Encoding"
        ) {
          // ignore, we can't send this header for HEAD requests
        } else {
          foundHeaders.push(key.toLowerCase());
          cleanedHeaders.push([key, value]);
        }
      }
      connection.rawConnection.responseHeaders = cleanedHeaders;
    }
  
    cleanSocket(bindIP: string | number, port: string | number) {
      if (!bindIP && typeof port === "string" && port.indexOf("/") >= 0) {
        fs.unlink(port, (error) => {
          if (error) {
            this.log(`cannot remove stale socket @ ${port}: ${error}`, "error");
          } else {
            this.log(`removed stale unix socket @ ${port}`);
          }
        });
      }
    }
  
    chmodSocket(bindIP: string | number, port: string | number) {
      if (!bindIP && typeof port === "string" && port.indexOf("/") >= 0) {
        fs.chmodSync(port, "0777");
      }
    }
  
    callbackHtmlEscape(s: string) {
      return s
        .replace(/&/g, "&amp;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#39;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/\)/g, "")
        .replace(/\(/g, "");
    }
  }
  