elastic-apm-node
Version:
The official Elastic APM agent for Node.js
226 lines (199 loc) • 7.54 kB
JavaScript
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
;
const url = require('url');
const basicAuth = require('basic-auth');
const getUrlFromRequest = require('original-url');
const parseHttpHeadersFromReqOrRes = require('http-headers');
const cookie = require('cookie');
const stringify = require('fast-safe-stringify');
const REDACTED = require('./constants').REDACTED;
const {
redactKeysFromObject,
redactKeysFromPostedFormVariables,
} = require('./filters/sanitize-field-names');
// When redacting individual cookie field values, this string is used instead
// of `[REDACTED]`. The APM spec says:
// > The replacement string SHOULD be `[REDACTED]`.
// We diverge from spec here because, for better or worse, the `cookie` module
// does `encodeURIComponent/decodeURIComponent` encoding on cookie fields. If we
// used the brackets, then the reconstructed cookie would look like
// `foo=bar; session-id=%5BREDACTED%5D`, which isn't helpful.
const COOKIE_VAL_REDACTED = 'REDACTED';
/**
* Extract appropriate `{transaction,error}.context.request` from an HTTP
* request object. This handles header and body capture and redaction
* according to the agent config.
*
* @param {Object} req - Typically `req` is a Node.js `http.IncomingMessage`
* (https://nodejs.org/api/all.html#all_http_class-httpincomingmessage).
* However, some cases (e.g. Lambda and Azure Functions instrumentation)
* create a pseudo-req object that matches well enough for this function.
* Some relevant fields: (TODO: document all used fields)
* - `headers` - Required. An object.
* - `body` - The incoming request body, if available. The `json` and
* `payload` fields are also checked to accomodate some web frameworks.
* - `bodyIsBase64Encoded` - An optional boolean. If `true`, then the `body`
* needs to be base64-decoded before inclusion and redaction. Used by
* Lambda instrumentation in some cases (e.g. for ELB triggers).
* @param {Object} conf - The full agent configuration.
* @param {String} type - 'errors' or 'transactions'. Indicates if this req
* is being captured for an APM error or transaction event.
*/
function getContextFromRequest(req, conf, type) {
var captureBody = conf.captureBody === type || conf.captureBody === 'all';
var context = {
http_version: req.httpVersion,
method: req.method,
url: getUrlFromRequest(req),
headers: undefined,
};
if (req.socket && req.socket.remoteAddress) {
context.socket = {
remote_address: req.socket.remoteAddress,
};
}
if (conf.captureHeaders) {
context.headers = redactKeysFromObject(
req.headers,
conf.sanitizeFieldNamesRegExp,
);
if (context.headers.cookie && context.headers.cookie !== REDACTED) {
let cookies = cookie.parse(req.headers.cookie);
cookies = redactKeysFromObject(
cookies,
conf.sanitizeFieldNamesRegExp,
COOKIE_VAL_REDACTED,
);
try {
context.headers.cookie = Object.keys(cookies)
.map((k) => cookie.serialize(k, cookies[k]))
.join('; ');
} catch (_err) {
// Fallback to full redaction if there is an issue re-serializing.
context.headers.cookie = REDACTED;
}
}
}
var contentLength = parseInt(req.headers['content-length'], 10);
var transferEncoding = req.headers['transfer-encoding'];
var chunked =
typeof transferEncoding === 'string' &&
transferEncoding.toLowerCase() === 'chunked';
var body = req.json || req.body || req.payload;
var haveBody = body && (chunked || contentLength > 0);
if (haveBody) {
if (!captureBody) {
context.body = '[REDACTED]';
} else if (Buffer.isBuffer(body)) {
context.body = '<Buffer>';
} else {
if (typeof body === 'string' && req.bodyIsBase64Encoded === true) {
body = Buffer.from(body, 'base64').toString('utf8');
}
body = redactKeysFromPostedFormVariables(
body,
req.headers,
conf.sanitizeFieldNamesRegExp,
);
if (typeof body !== 'string') {
body = tryJsonStringify(body) || stringify(body);
}
context.body = body;
}
}
// TODO: Tempoary fix for https://github.com/elastic/apm-agent-nodejs/issues/813
if (context.url && context.url.port) {
context.url.port = String(context.url.port);
}
return context;
}
/**
* Extract appropriate `{transaction,error}.context.response` from an HTTP
* response object. This handles header redaction according to the agent config.
*
* @param {Object} res - Typically `res` is a Node.js `http.ServerResponse`
* (https://nodejs.org/api/http.html#class-httpserverresponse).
* However, some cases (e.g. Lambda and Azure Functions instrumentation)
* create a pseudo-res object that matches well enough for this function.
* Some relevant fields: (TODO: document all used fields)
* - `statusCode` - Required. A number.
* - `headers` - An object.
* - `headersSent` - Boolean indicating if the headers have been sent
* (https://nodejs.org/api/http.html#outgoingmessageheaderssent)
* - `finished` - Boolean indicating if `response.end()` has been called
* (https://nodejs.org/api/http.html#responsefinished)
* @param {Object} conf - The full agent configuration.
* @param {Boolean} isError - Indicates if this response contains an error and
* some extra fields should be added to the context
*/
function getContextFromResponse(res, conf, isError) {
var context = {
status_code: res.statusCode,
headers: undefined,
};
if (conf.captureHeaders) {
context.headers = res.headers || parseHttpHeadersFromReqOrRes(res, true);
context.headers = redactKeysFromObject(
context.headers,
conf.sanitizeFieldNamesRegExp,
);
}
if (isError) {
context.headers_sent = res.headersSent;
if (typeof res.finished === 'boolean') {
context.finished = res.finished;
} else {
context.finished = res.writableEnded;
}
}
return context;
}
/**
* Extract appropriate `{transaction,error}.context.user` from an HTTP
* request object.
*
* @param {Object} req - Typically `req` is a Node.js `http.IncomingMessage`.
* However, some cases (e.g. Lambda and Azure Functions instrumentation)
* create a pseudo-req object that matches well enough for this function.
* Some relevant fields: (TODO: document all used fields)
* - `headers` - Required. An object.
*/
function getUserContextFromRequest(req) {
var user = req.user || basicAuth(req) || req.session;
if (!user) {
return;
}
var context = {};
if (typeof user.id === 'string' || typeof user.id === 'number') {
context.id = user.id;
} else if (typeof user._id === 'string' || typeof user._id === 'number') {
context.id = user._id;
}
if (typeof user.username === 'string') {
context.username = user.username;
} else if (typeof user.name === 'string') {
context.username = user.name;
}
if (typeof user.email === 'string') {
context.email = user.email;
}
return context;
}
function parseUrl(urlStr) {
return new url.URL(urlStr, 'relative:///');
}
function tryJsonStringify(obj) {
try {
return JSON.stringify(obj);
} catch (e) {}
}
module.exports = {
getContextFromRequest,
getContextFromResponse,
getUserContextFromRequest,
parseUrl,
};