Source: utils.js

/**
 * @module utils
 */

var crypto = require('crypto');
var logger = require('./logger');
var TraceID = require('./segments/attributes/trace_id');

var utils = {

  /**
   * Checks a HTTP response code, where 4xx are 'error' and 5xx are 'fault'.
   * @param {string} status - the HTTP response status code.
   * @returns [string] - 'error', 'fault' or nothing on no match
   * @alias module:utils.getCauseTypeFromHttpStatus
   */

  getCauseTypeFromHttpStatus: function getCauseTypeFromHttpStatus(status) {
    var stat = status.toString();
    if (stat.match(/^[4][0-9]{2}$/) !== null) {
      return 'error';
    } else if (stat.match(/^[5][0-9]{2}$/) !== null) {
      return 'fault';
    }
  },

  /**
   * Removes the query string parameters from a given http request path
   * as it may contain sensitive information
   *
   * Related issue: https://github.com/aws/aws-xray-sdk-node/issues/246
   *
   * Node documentation: https://nodejs.org/api/http.html#http_http_request_url_options_callback
   *
   * @param {string} path - options.path in a http.request callback
   * @returns [string] - removes query string element from path
   * @alias module:utils.stripQueryStringFromPath
   */

  stripQueryStringFromPath: function stripQueryStringFromPath(path) {
    return path ? path.split('?')[0] : '';
  },

  /**
   * Performs a case-insensitive wildcard match against two strings. This method works with pseduo-regex chars; specifically ? and * are supported.
   *   An asterisk (*) represents any combination of characters
   *   A question mark (?) represents any single character
   *
   * @param {string} pattern - the regex-like pattern to be compared against.
   * @param {string} text - the string to compare against the pattern.
   * @returns boolean
   * @alias module:utils.wildcardMatch
   */

  wildcardMatch: function wildcardMatch(pattern, text) {
    if (pattern === undefined || text === undefined) {
      return false;
    }

    if (pattern.length === 1 && pattern.charAt(0) === '*') {
      return true;
    }

    var patternLength = pattern.length;
    var textLength = text.length;
    var indexOfGlob = pattern.indexOf('*');

    pattern = pattern.toLowerCase();
    text = text.toLowerCase();

    // Infix globs are relatively rare, and the below search is expensive especially when
    // Balsa is used a lot. Check for infix globs and, in their absence, do the simple thing
    if (indexOfGlob === -1 || indexOfGlob === (patternLength - 1)) {
      var match = function simpleWildcardMatch() {
        var j = 0;

        for (var i = 0; i < patternLength; i++) {
          var patternChar = pattern.charAt(i);
          if (patternChar === '*') {
            // Presumption for this method is that globs only occur at end
            return true;
          } else if (patternChar === '?') {
            if (j === textLength) {
              return false;
            } // No character to match

            j++;
          } else {
            if (j >= textLength || patternChar != text.charAt(j)) {
              return false;
            }

            j++;
          }
        }
        // Ate up all the pattern and didn't end at a glob, so a match will have consumed all
        // the text
        return j === textLength;
      };

      return match();
    }

    /*
     * The matchArray[i] is used to record if there is a match between the first i chars in =
     * text and the first j chars in pattern.
     * So will return matchArray[textLength+1] in the end
     * Loop from the beginning of the pattern
     * case not '*': if text[i]==pattern[j] or pattern[j] is '?', and matchArray[i] is true,
     *   set matchArray[i+1] to true, otherwise false
     * case '*': since '*' can match any globing, as long as there is a true in matchArray before i
     *   all the matchArray[i+1], matchArray[i+2],...,matchArray[textLength] could be true
    */

    var matchArray = [];
    matchArray[0] = true;

    for (var j = 0; j < patternLength; j++) {
      var i;
      var patternChar = pattern.charAt(j);

      if (patternChar != '*') {
        for (i = textLength - 1; i >= 0; i--) {
          matchArray[i+1] = !!matchArray[i] && (patternChar === '?' || (patternChar === text.charAt(i)));
        }
      } else {
        i = 0;

        while (i <= textLength && !matchArray[i]) {
          i++;
        }

        for (i; i <= textLength; i++) {
          matchArray[i] = true;
        }
      }
      matchArray[0] = (matchArray[0] && patternChar === '*');
    }

    return matchArray[textLength];
  },

  LambdaUtils: {
    validTraceData: function(xAmznTraceId) {
      var valid = false;

      if (xAmznTraceId) {
        var data = utils.processTraceData(xAmznTraceId);
        valid = !!(data && data.root && data.parent && data.sampled);
      }

      return valid;
    },

    /**
     * Populates trace ID, parent ID, and sampled decision of given segment. Will always populate valid values,
     * even if xAmznTraceId contains missing or invalid values. This ensures downstream services receive valid
     * headers.
     * @param {Segment} segment - Facade segment to be populated
     * @param {String} xAmznTraceId - Raw Trace Header to supply trace data
     * @returns {Boolean} - true if required fields are present and Trace ID is valid, false otherwise
     */
    populateTraceData: function(segment, xAmznTraceId) {
      logger.getLogger().debug('Lambda trace data found: ' + xAmznTraceId);
      let traceData = utils.processTraceData(xAmznTraceId);
      var valid = false;

      if (!traceData) {
        traceData = {};
        logger.getLogger().error('_X_AMZN_TRACE_ID is empty or has an invalid format');
      } else if (traceData.root && !traceData.parent && !traceData.sampled) {
        // Lambda PassThrough only has root, treat as valid in this case and mark the segment
        segment.noOp = true;
        valid = true;
      } else if (!traceData.root || !traceData.parent || !traceData.sampled) {
        logger.getLogger().error('_X_AMZN_TRACE_ID is missing required information');
      } else {
        valid = true;
      }

      segment.trace_id = TraceID.FromString(traceData.root).toString();  // Will always assign valid trace_id
      segment.id = traceData.parent || crypto.randomBytes(8).toString('hex');

      if (traceData.root && segment.trace_id !== traceData.root)  {
        logger.getLogger().error('_X_AMZN_TRACE_ID contains invalid trace ID');
        valid = false;
      }

      if (!parseInt(traceData.sampled)) {
        segment.notTraced = true;
      } else {
        delete segment.notTraced;
      }

      if (traceData.data) {
        segment.additionalTraceData = traceData.data;
      }

      logger.getLogger().debug('Segment started: ' + JSON.stringify(traceData));
      return valid;
    }
  },

  /**
   * Splits out the data from the trace id format.  Used by the middleware.
   * @param {String} traceData - The additional trace data (typically in req.headers.x-amzn-trace-id).
   * @returns {object}
   * @alias module:mw_utils.processTraceData
   */

  processTraceData: function processTraceData(traceData) {
    var amznTraceData = {};
    var data = {};
    var reservedKeywords = ['root', 'parent', 'sampled', 'self'];
    var remainingBytes = 256;

    if (!(typeof traceData === 'string' && traceData)) {
      return amznTraceData;
    }

    traceData.split(';').forEach(function(header) {
      if (!header) {
        return;
      }

      var pair = header.split('=');

      if (pair[0] && pair[1]) {
        let key = pair[0].trim();
        let value = pair[1].trim();
        let lowerCaseKey = key.toLowerCase();
        let reserved = reservedKeywords.indexOf(lowerCaseKey) !== -1;

        if (reserved) {
          amznTraceData[lowerCaseKey] = value;
        } else if (!reserved && remainingBytes - (lowerCaseKey.length + value.length) >= 0) {
          data[key] = value;
          remainingBytes -= (key.length + value.length);
        }
      }
    });

    amznTraceData['data'] = data;

    return amznTraceData;
  },

  /**
   * Makes a shallow copy of an object without given keys - keeps prototype
   * @param {Object} obj - The object to copy
   * @param {string[]} [keys=[]] - The keys that won't be copied
   * @param {boolean} [preservePrototype=false] - If true also copy prototype properties
   * @returns {}
   */

  objectWithoutProperties: function objectWithoutProperties(obj, keys, preservePrototype) {
    keys = Array.isArray(keys) ? keys : [];
    preservePrototype = typeof preservePrototype === 'boolean' ? preservePrototype : false;
    var target = preservePrototype ? Object.create(Object.getPrototypeOf(obj))  : {};
    for (var property in obj) {
      if (keys.indexOf(property) >= 0) {
        continue;
      }
      if (!Object.prototype.hasOwnProperty.call(obj, property)) {
        continue;
      }
      target[property] = obj[property];
    }
    return target;
  },

  /**
   * Safely gets an integer from a string or number
   * @param {String | Number} - input to cast to integer
   * @returns {Number} - Integer representation of input, or 0 if input is not castable to int
   */
  safeParseInt: (val) => {
    if (!val || isNaN(val)) {
      return 0;
    }
    return parseInt(val);
  }
};

module.exports = utils;