/**
* @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;