UNPKG

@instana/core

Version:
328 lines (308 loc) 15.5 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2019 */ 'use strict'; /** * Used in http server instrumentation, the headers are already lower cased in the data we receive. This method is less * expensive than getExtraHeadersCaseInsensitive/getExtraHeadersFromOptions. * * @param {import('http').IncomingMessage} message the request object to inspect for headers * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} an object literal with the captured headers or undefined if no headers matched the * extraHttpHeadersToCapture list */ exports.getExtraHeadersFromMessage = function getExtraHeadersFromMessage(message, extraHttpHeadersToCapture) { return exports.getExtraHeadersFromNormalizedObjectLiteral(message.headers, extraHttpHeadersToCapture); }; /** * Used in http2 server instrumentation (and indirectly in http1 server instrumentation), the headers are already lower * cased in the data we receive. This method is less expensive than * getExtraHeadersCaseInsensitive/getExtraHeadersFromOptions. * * @param {Object.<string, string | string[]>} headers an object literal that represents the headers of an HTTP message; * this method expects that the keys have already been normalized to lower case * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} an object literal with the captured headers or undefined if no headers matched the * extraHttpHeadersToCapture list */ exports.getExtraHeadersFromNormalizedObjectLiteral = function getExtraHeadersFromNormalizedObjectLiteral( headers, extraHttpHeadersToCapture ) { if (!extraHttpHeadersToCapture || extraHttpHeadersToCapture.length === 0) { return undefined; } let extraHeadersFound = false; const /** @type Object.<string, string> */ extraHeaders = {}; for (let i = 0; i < extraHttpHeadersToCapture.length; i++) { const key = extraHttpHeadersToCapture[i]; const value = headers[key]; if (Array.isArray(value)) { // If the client has set the same header multiple times, Node.js has already normalized // this to a single string ("value 1, value 2, ..."). However, the type definitions in Node.js core declare // interface IncomingHttpHeaders extends NodeJS.Dict<string | string[]>, so technically string array values // are allowed and we should be able to handle them. extraHeaders[key] = value.join(', '); extraHeadersFound = true; } else if (value) { extraHeaders[key] = value; extraHeadersFound = true; } } if (!extraHeadersFound) { return undefined; } return extraHeaders; }; /** * Used in native fetch instrumentation; the headers are already normalized to a * Headers object (https://developer.mozilla.org/en-US/docs/Web/API/Headers) and the header names are normalized * to lower case. * * @param {import('node-fetch').Headers} headers a Fetch API Headers object * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} an object literal with the captured headers or undefined if no headers matched the * extraHttpHeadersToCapture list */ exports.getExtraHeadersFromFetchHeaders = function getExtraHeadersFromFetchHeaders(headers, extraHttpHeadersToCapture) { if (!extraHttpHeadersToCapture || extraHttpHeadersToCapture.length === 0) { return undefined; } let extraHeadersFound = false; const /** @type Object.<string, string> */ extraHeaders = {}; for (let i = 0; i < extraHttpHeadersToCapture.length; i++) { const key = extraHttpHeadersToCapture[i]; if (headers.has(key)) { // If the client has set the same header multiple times, Node.js has already normalized // this to "value 1, value 2, ..." extraHeaders[key] = headers.get(key); extraHeadersFound = true; } } if (!extraHeadersFound) { return undefined; } return extraHeaders; }; /** * Used in http client instrumentation, the headers can appear in any lower case/upper case combination there. This is * slightly more expensive than getExtraHeadersFromMessage. * * @param {import ('http').RequestOptions} options the options object passed to http.request/http.get * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} an object literal with the captured headers or undefined if no headers matched the * extraHttpHeadersToCapture list */ exports.getExtraHeadersFromOptions = function getExtraHeadersFromOptions(options, extraHttpHeadersToCapture) { if (!options) { return undefined; } return exports.getExtraHeadersCaseInsensitive(options.headers, extraHttpHeadersToCapture); }; /** * Used in http2 (and indirectly http 1.x client) instrumentation, the headers can appear in any lower case/upper case * combination there. This is slightly more expensive than getExtraHeadersFromNormalizedObjectLiteral. * * @param {Object.<string | number, number | string | string[]>} headers an object literal that represents the headers * of an HTTP request; in contrast to getExtraHeadersFromNormalizedObjectLiteral this method expects does not expect the * keys to have been normalized to lower case * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} an object literal with the captured headers or undefined if no headers matched the * extraHttpHeadersToCapture list */ exports.getExtraHeadersCaseInsensitive = function getExtraHeadersCaseInsensitive(headers, extraHttpHeadersToCapture) { if (!extraHttpHeadersToCapture || extraHttpHeadersToCapture.length === 0 || !headers || typeof headers !== 'object') { return undefined; } const keys = Object.keys(headers).map(key => ({ orig: key, low: key.toLowerCase() })); let extraHeadersFound = false; const /** @type Object.<string, string> */ extraHeaders = {}; for (let i = 0; i < extraHttpHeadersToCapture.length; i++) { const keyToCapture = extraHttpHeadersToCapture[i]; for (let j = 0; j < keys.length; j++) { if (keys[j].low === keyToCapture) { extraHeaders[keys[j].low] = Array.isArray(headers[keys[j].orig]) ? headers[keys[j].orig].join(', ') : headers[keys[j].orig]; extraHeadersFound = true; } } } if (!extraHeadersFound) { return undefined; } return extraHeaders; }; /** * This is used to merge HTTP response headers with HTTP request headers that have been captured earlier in the request * lifecycle (in the http server instrumentation), or to merge additional headers from multiple sources (in the http * client instrumentation). * * @param {Object.<string, string> | undefined} headersAlreadyCapturedIfAny an object literal representing the headers * that have already been captured earlier * @param {import('http').ServerResponse|import('http').ClientRequest} message the object to inspect for additional * headers; this object is assumed to have a `getHeader` method that retrieves headers in a case-insensitive fashion. * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} a new object literal with the merged captured headers, or the parameter * `headersAlreadyCapturedIfAny` unmodified if no new headers were found */ exports.mergeExtraHeadersFromServerResponseOrClientRequest = function mergeExtraHeadersFromServerResponseOrClientRequest( headersAlreadyCapturedIfAny, message, extraHttpHeadersToCapture ) { return mergeExtraHeaders(headersAlreadyCapturedIfAny, extraHttpHeadersToCapture, key => message.getHeader(key)); }; /** * This is used to merge HTTP response headers with HTTP request headers that have been captured earlier in the request * lifecycle (in the http client instrumentation). * * @param {Object.<string, string> | undefined} headersAlreadyCapturedIfAny an object literal representing the headers * that have already been captured earlier * @param {import('http').IncomingMessage} incomingMessage the response object to inspect for headers; this method * assumes that this object has a `headers` property which is an object literal where header names have been normalized * to lower case * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} a new object literal with the merged captured headers, or the parameter * `headersAlreadyCapturedIfAny` unmodified if no new headers were found */ exports.mergeExtraHeadersFromIncomingMessage = function mergeExtraHeadersFromIncomingMessage( headersAlreadyCapturedIfAny, incomingMessage, extraHttpHeadersToCapture ) { return exports.mergeExtraHeadersFromNormalizedObjectLiteral( headersAlreadyCapturedIfAny, incomingMessage.headers, extraHttpHeadersToCapture ); }; /** * This is used to merge HTTP response headers with HTTP request headers that have been captured earlier in the request * lifecycle (in the http 2 client instrumentation and implicitly in the http client instrumentation). * * @param {Object.<string, string> | undefined} headersAlreadyCapturedIfAny an object literal representing the headers * that have already been captured earlier * @param {Object.<string, string | string[]>} headers an object literal that represents the headers of an HTTP message; * this method expects that the keys have already been normalized to lower case * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} a new object literal with the merged captured headers, or the parameter * `headersAlreadyCapturedIfAny` unmodified if no new headers were found */ exports.mergeExtraHeadersFromNormalizedObjectLiteral = function mergeExtraHeadersFromNormalizedObjectLiteral( headersAlreadyCapturedIfAny, headers, extraHttpHeadersToCapture ) { if (!headers) { return headersAlreadyCapturedIfAny; } return mergeExtraHeaders(headersAlreadyCapturedIfAny, extraHttpHeadersToCapture, key => headers[key]); }; /** * This is used to merge HTTP response headers provided in the form of a Fetch API Headers object * (https://developer.mozilla.org/en-US/docs/Web/API/Headers) with HTTP request headers that have been captured earlier * in the request lifecycle (in the native fetch instrumentation). * * @param {Object.<string, string> | undefined} headersAlreadyCapturedIfAny an object literal representing the headers * that have already been captured earlier * @param {import('node-fetch').Headers} headers a Fetch API Headers object * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} a new object literal with the merged captured headers, or the parameter * `headersAlreadyCapturedIfAny` unmodified if no new headers were found */ exports.mergeExtraHeadersFromFetchHeaders = function mergeExtraHeadersFromFetchHeaders( headersAlreadyCapturedIfAny, headers, extraHttpHeadersToCapture ) { if (!extraHttpHeadersToCapture || extraHttpHeadersToCapture.length === 0 || !headers) { return headersAlreadyCapturedIfAny; } return mergeExtraHeaders(headersAlreadyCapturedIfAny, extraHttpHeadersToCapture, keyToCapture => headers.get(keyToCapture) ); }; /** * This is used to merge HTTP response headers with HTTP request headers that have been captured earlier in the request * lifecycle (in the http 2 server instrumentation). This method can handle object literals in which the keys have not * been normalized to lower case yet. It is slightly more expensive than mergeExtraHeadersFromNormalizedObjectLiteral. * * @param {Object.<string, string> | undefined} headersAlreadyCapturedIfAny an object literal representing the headers * that have already been captured earlier * @param {Object.<string, string | string[]>} headers an object literal that represents the headers of an HTTP message; * in contrast to mergeExtraHeadersFromNormalizedObjectLiteral this method does _not_ expect that the keys have already * been normalized to lower case * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @returns {Object | undefined} a new object literal with the merged captured headers, or the parameter * `headersAlreadyCapturedIfAny` unmodified if no new headers were found */ exports.mergeExtraHeadersCaseInsensitive = function mergeExtraHeadersCaseInsensitive( headersAlreadyCapturedIfAny, headers, extraHttpHeadersToCapture ) { if (!extraHttpHeadersToCapture || extraHttpHeadersToCapture.length === 0 || !headers) { return headersAlreadyCapturedIfAny; } const keys = Object.keys(headers).map(key => ({ orig: key, low: key.toLowerCase() })); return mergeExtraHeaders(headersAlreadyCapturedIfAny, extraHttpHeadersToCapture, keyToCapture => { for (let j = 0; j < keys.length; j++) { if (keys[j].low === keyToCapture) { return headers[keys[j].orig]; } } return null; }); }; /** * This is used to merge HTTP response headers with HTTP request headers that have been captured earlier in the request * lifecycle, or headers from multiple sources. It is used under the hood by all mergeExtraHeaders... methods. * * @param {Object.<string, string> | undefined} headersAlreadyCapturedIfAny an object literal representing the headers * that have already been captured earlier * @param {Array<string>} extraHttpHeadersToCapture the configured list of headers that need to be captured * @param {(key: string) => string | string[] | number | undefined} getHeader a function that takes a header name (from * the extraHttpHeadersToCapture list) and returns the header value from the HTTP request/response (or undefined, if the * header does not exist) * @returns {Object | undefined} a new object literal with the merged captured headers, or the parameter * `headersAlreadyCapturedIfAny` unmodified if no new headers were found */ function mergeExtraHeaders(headersAlreadyCapturedIfAny, extraHttpHeadersToCapture, getHeader) { if (!extraHttpHeadersToCapture || extraHttpHeadersToCapture.length === 0) { return headersAlreadyCapturedIfAny; } let /** @type Object.<string, string> */ additionalHeaders = {}; let additionalHeadersFound = false; for (let i = 0; i < extraHttpHeadersToCapture.length; i++) { const key = extraHttpHeadersToCapture[i]; const value = getHeader(key); if (Array.isArray(value)) { additionalHeaders[key] = value.join(', '); additionalHeadersFound = true; } else if (typeof value === 'number') { additionalHeaders[key] = String(value); additionalHeadersFound = true; } else if (value) { additionalHeaders[key] = value; additionalHeadersFound = true; } } if (!additionalHeadersFound) { additionalHeaders = undefined; } if (headersAlreadyCapturedIfAny && additionalHeaders) { return Object.assign(headersAlreadyCapturedIfAny, additionalHeaders); } else if (additionalHeaders) { return additionalHeaders; } return headersAlreadyCapturedIfAny; }