UNPKG

newrelic

Version:
248 lines (221 loc) 7.69 kB
/* * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const url = require('url') const logger = require('../logger').child({ component: 'urltils' }) const LOCALHOST_NAMES = { localhost: true, '127.0.0.1': true, '0.0.0.0': true, '0:0:0:0:0:0:0:1': true, '::1': true, '0:0:0:0:0:0:0:0': true, '::': true } /** * Utility functions for enforcing New Relic naming conditions on URLs, * and extracting and setting parameters on traces / web trace segments. */ module.exports = { /** * Dictionary whose keys are all synonyms for localhost. * * @constant */ LOCALHOST_NAMES, /** * Checks if the given name is in the dictionary of localhost names. * * @param {string} host - The hostname to lookup. * @returns {boolean} - True if the given hostname is a synonym for localhost. */ isLocalhost: function isLocahost(host) { return LOCALHOST_NAMES[host] != null }, /** * This was handed down from the prototype as the canonical list of status * codes that short-circuit naming and normalization. The agent can be * configured to mark HTTP status codes as not being errors. * * @param {Config} config The configuration containing the error list. * @param {string} code The HTTP status code to check. * @returns {boolean} Whether the status code should be ignored. */ isError: function isError(config, code) { return code >= 400 && !isIgnoredStatusCodeForErrors(config, code) }, /** * Returns true if the status code is an HTTP error, and it is configured to be ignored. * * @param {Config} config The configuration containing the error list. * @param {string} code The HTTP status code to check. * @returns {boolean} Whether the status code should be ignored. */ isIgnoredError: function isIgnoredError(config, code) { return code >= 400 && isIgnoredStatusCodeForErrors(config, code) }, /** * Returns true if the status code is configured to be expected * * @param {Config} config The configuration containing the error list. * @param {string} code The HTTP status code to check. * @returns {boolean} Whether the status code is expected. */ isExpectedError: function isExpectedError(config, code) { return isExpectedStatusCodeForErrors(config, code) }, /** * Get back the pieces of the URL that New Relic cares about. Apply these * restrictions, in order: * * 1. Ensure that after parsing the URL, there's at least '/' * 2. Strip off session trackers after ';' (a New Relic convention) * 3. Remove trailing slash. * * @param {string|url.URL} requestURL The URL fragment to be scrubbed. * @returns {string} The cleaned URL. */ scrub: function scrub(requestURL) { if (typeof requestURL === 'string') { requestURL = url.parse(requestURL) } let path = requestURL.pathname if (path) { path = path.split(';')[0] if (path !== '/' && path.charAt(path.length - 1) === '/') { path = path.substring(0, path.length - 1) } } else { path = '/' } return path }, /** * Extract query parameters, dealing with bare parameters and parameters with * no value as appropriate: * * 'var1&var2=value' is not necessarily the same as 'var1=&var2=value' * * In my world, one is an assertion of presence, and the other is an empty * variable. Some web frameworks behave this way as well, so don't lose * information. * * @param {string|url.URL} requestURL The URL to be parsed. * @returns {object} The parameters parsed from the request */ parseParameters: function parseParameters(requestURL) { let parsed = requestURL const parameters = Object.create(null) function addParam(key, value, raw) { if (value === '' && raw.indexOf(key + '=') === -1) { parameters[key] = true } else { parameters[key] = value } } if (typeof requestURL === 'string') { parsed = url.parse(requestURL, true) } if (parsed.query) { for (const key of Object.keys(parsed.query)) { addParam(key, parsed.query[key], parsed.path || '') } } if (parsed.searchParams) { for (const [key, value] of parsed.searchParams) { addParam(key, value, parsed.pathname || '') } } return parameters }, /** * Performs the logic of `urltils.scrub` and `urltils.parseParameters` with * only a single parse of the given URL. * * @param {string|url.URL} requestURL - The URL to scrub and extra parameters from. * @returns {object} An object containing the scrubbed url at `.path` and the * parsed parameters at `.parameters`. */ scrubAndParseParameters: function scrubAndParseParameters(requestURL) { if (typeof requestURL === 'string') { requestURL = url.parse(requestURL, true) } return { protocol: requestURL.protocol, path: this.scrub(requestURL), parameters: this.parseParameters(requestURL) } }, /** * Obfuscates path parameters with regex from config * * @param {Config} config The configuration containing the regex * @param {string} path The path to be obfuscated * @returns {string} The obfuscated path or the original path */ obfuscatePath: function obfuscatePath(config, path) { const { enabled, regex } = config.url_obfuscation if (typeof path !== 'string' || !enabled || !regex) { return path } const { pattern, flags = '', replacement = '' } = regex try { const regexPattern = new RegExp(pattern, flags) return path.replace(regexPattern, replacement) } catch { logger.warn('Invalid regular expression for url_obfuscation.regex.pattern', pattern) return path } }, /** * Copy a set of request parameters from one object to another, * but do not overwrite any existing parameters in destination, * including parameters set to null or undefined. * * @param {object} source Parameters to be copied (not changed). * @param {object} destination Dictionary to which parameters are copied * (mutated in place). */ copyParameters: function copyParameters(source, destination) { if (source && destination) { const keys = Object.keys(source) for (let i = 0; i < keys.length; i++) { const key = keys[i] if (!(key in destination)) { destination[key] = source[key] } } } }, /** * Copy a set of request parameters from one object to another. * Existing attributes on the `destination` will be overwritten. * * @param {object} source Parameters to be copied (not changed). * @param {object} destination Dictionary to which parameters are copied * (mutated in place). */ overwriteParameters: function overwriteParameters(source, destination) { const keys = Object.keys(source) for (let i = 0; i < keys.length; i++) { const key = keys[i] destination[key] = source[key] } } } function isIgnoredStatusCodeForErrors(config, code) { let codes = [] if (config && config.error_collector && config.error_collector.ignore_status_codes) { codes = config.error_collector.ignore_status_codes } return codes.indexOf(parseInt(code, 10)) >= 0 } function isExpectedStatusCodeForErrors(config, code) { let codes = [] if (config && config.error_collector && config.error_collector.expected_status_codes) { codes = config.error_collector.expected_status_codes } return codes.indexOf(parseInt(code, 10)) >= 0 }