Skip to content

Commit

Permalink
Add new headerValueCallback option that will called on every visisble…
Browse files Browse the repository at this point in the history
… header

PROD-36670
  • Loading branch information
stolosapo committed Apr 19, 2024
1 parent 9dbc941 commit e2449fe
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 16 deletions.
34 changes: 28 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ Use `riviere` if you want an easy way to log all the HTTP traffic for your serve
10. [context](#options_context)
11. [errors.callback](#options_errors_callback)
12. [headersRegex](#options_headers_regex)
13. [health](#options_health)
14. [outbound.enabled](#options_outbound_enabled)
15. [outbound.request.enabled](#options_outbound_request_enabled)
16. [outbound.maxBodyValueChars](#options_outbound_max_body_value_chars)
17. [outbound.blacklistedPathRegex](#options_outbound_blacklisted_path_regex)
18. [traceHeaderName](#options_trace_header_name)
13. [headerValueCallback](#options_header_value_callback)
14. [health](#options_health)
15. [outbound.enabled](#options_outbound_enabled)
16. [outbound.request.enabled](#options_outbound_request_enabled)
17. [outbound.maxBodyValueChars](#options_outbound_max_body_value_chars)
18. [outbound.blacklistedPathRegex](#options_outbound_blacklisted_path_regex)
19. [traceHeaderName](#options_trace_header_name)
8. [License](#License)

---
Expand Down Expand Up @@ -427,6 +428,27 @@ All the inbound request's headers starting with "X-" will be logged by default.
}
```

<a name="options_header_value_callback"></a>
**headerValueCallback**

This option can be used to let you modify or change part or the whole value of a header.
This function will be applied to all headers that are passed based on the `headersRegex`.
This function takes two argument, the `key` (i.e. the header name) and the `value` (i.e. the header value) and returns the new value of the header.
This function is very useful in cases where is mandatory to hide or remove sensitive data that is part of the header value.
Defaults to undefined. In case of undefined then the header value will remain as is.

*Example*:
In this example we hide some sensitive `id_token` value.

```js
{
headerValueCallback: (key, value) {
const regex = /id_token=[\w-]+\.[\w-]+\.[\w-]+/i;
return value.replace(regex, 'id_token=***');
};
}
```

<a name="options_health"></a>
**health**

Expand Down
25 changes: 25 additions & 0 deletions examples/withHeaderValueCallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const Koa = require('koa');
const riviere = require('../index');
const request = require('request-promise');

const app = new Koa();

app.use(
riviere({
outbound: {
enabled: true
},
styles: ['extended'],
headerValueCallback: (key, value) => {
const regex = /id_token=[\w-]+\.[\w-]+\.[\w-]+/i;
return value.replace(regex, 'id_token=***');
}
})
);

app.use(async function(ctx) {
ctx.body = 'Hello World';
ctx.set('x-something', 'id_token=some.token.to-hide');
});

app.listen(3000);
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const httpsProxy = require('./lib/proxies/https').proxy;
const { EVENT } = Loggable;

function buildRiviere(options = {}) {
const { errors = {}, logger, inbound, outbound, traceHeaderName, headersRegex } = defaultsDeep(
const { errors = {}, logger, inbound, outbound, traceHeaderName, headersRegex, headerValueCallback } = defaultsDeep(
options,
defaultOptions(options)
);
Expand All @@ -26,6 +26,7 @@ function buildRiviere(options = {}) {
bodyKeys: options.bodyKeys,
bodyKeysRegex: options.bodyKeysRegex,
bodyKeysCallback: options.bodyKeysCallback,
headerValueCallback,
...outbound
}
});
Expand Down
4 changes: 4 additions & 0 deletions lib/adapters/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function onInboundRequest({ ctx }) {
bodyKeysRegex,
bodyKeysCallback,
headersRegex,
headerValueCallback,
inbound: { maxBodyValueChars }
} = this;
const transformedReq = mapInReq({
Expand All @@ -102,6 +103,7 @@ function onInboundRequest({ ctx }) {
bodyKeysRegex,
bodyKeysCallback,
headersRegex,
headerValueCallback,
maxBodyValueChars
});

Expand All @@ -119,6 +121,7 @@ function onOutboundResponse({ ctx }) {
bodyKeysRegex,
bodyKeysCallback,
headersRegex,
headerValueCallback,
outbound: { maxBodyValueChars }
} = this;
const transformedRes = mapOutRes({
Expand All @@ -128,6 +131,7 @@ function onOutboundResponse({ ctx }) {
bodyKeysRegex,
bodyKeysCallback,
headersRegex,
headerValueCallback,
maxBodyValueChars
});
this.logger[this.inbound.level](transformedRes, this);
Expand Down
2 changes: 2 additions & 0 deletions lib/loggable.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Loggable extends EventEmitter {
bodyKeysCallback,
context,
headersRegex,
headerValueCallback,
health,
inbound,
outbound,
Expand All @@ -32,6 +33,7 @@ class Loggable extends EventEmitter {
this.bodyKeysRegex = bodyKeysRegex;
this.bodyKeysCallback = bodyKeysCallback;
this.headersRegex = headersRegex;
this.headerValueCallback = headerValueCallback;
this.health = health;
this.inbound = inbound;
this.outbound = outbound;
Expand Down
1 change: 1 addition & 0 deletions lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ module.exports = (options = {}) => {
bodyKeysRegex: undefined,
bodyKeysCallback: undefined,
headersRegex: new RegExp('^X-.*', 'i'),
headerValueCallback: undefined,
traceHeaderName: 'X-Riviere-Id',
forceIds: true
};
Expand Down
43 changes: 36 additions & 7 deletions lib/transformers/transformers.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,30 @@ const mapError = ({ ctx, err }) => {
/**
* Server HTTP in and out responses
*/
const mapInReq = ({ ctx, health, bodyKeys, bodyKeysRegex, bodyKeysCallback, headersRegex, maxBodyValueChars }) => {
const mapInReq = ({
ctx,
health,
bodyKeys,
bodyKeysRegex,
bodyKeysCallback,
headersRegex,
headerValueCallback,
maxBodyValueChars
}) => {
if (isHealth(ctx, health)) {
return Object.assign({}, ctx.logCtx, {
log_tag: logTags.CATEGORY.INBOUND_REQUEST_HEALTH.TAG
});
}
const meta = extractRequestMeta(ctx, bodyKeys, bodyKeysRegex, bodyKeysCallback, maxBodyValueChars, headersRegex);
const meta = extractRequestMeta(
ctx,
bodyKeys,
bodyKeysRegex,
bodyKeysCallback,
maxBodyValueChars,
headersRegex,
headerValueCallback
);

let userAgent = getUserAgent(ctx.headers);

Expand All @@ -53,11 +70,20 @@ const mapInReq = ({ ctx, health, bodyKeys, bodyKeysRegex, bodyKeysCallback, head
});
};

const mapOutRes = ({ ctx, health, bodyKeys, bodyKeysRegex, bodyKeysCallback, headersRegex, maxBodyValueChars }) => {
const mapOutRes = ({
ctx,
health,
bodyKeys,
bodyKeysRegex,
bodyKeysCallback,
headersRegex,
headerValueCallback,
maxBodyValueChars
}) => {
const status = ctx.status;
const duration = new Date().getTime() - ctx.state.riviereStartedAt;

const headers = extractHeaders(headersRegex, ctx.response.headers);
const headers = extractHeaders(headersRegex, headerValueCallback, ctx.response.headers);

if (isHealth(ctx, health)) {
return Object.assign({}, ctx.logCtx, {
Expand All @@ -74,6 +100,7 @@ const mapOutRes = ({ ctx, health, bodyKeys, bodyKeysRegex, bodyKeysCallback, hea
bodyKeysCallback,
maxBodyValueChars,
headersRegex,
headerValueCallback,
'request'
);

Expand All @@ -97,7 +124,7 @@ const mapInRes = (res, req, startedAt, reqId, opts) => {
let contentLength = getContentLength(res.headers);
let userAgent = getUserAgent(res.headers);

const headers = extractHeaders(opts.headersRegex, res.headers);
const headers = extractHeaders(opts.headersRegex, opts.headerValueCallback, res.headers);
return {
...pick(req, ['method', 'protocol', 'host', 'path', 'query', 'href']),
status,
Expand All @@ -115,6 +142,7 @@ const mapOutReq = (requestOptions, reqId, opts = {}) => {
const port = requestOptions.port;
const requestId = reqId;
const headersRegex = opts.headersRegex;
const headerValueCallback = opts.headerValueCallback;

const { protocol, host, path, query, href } = getUrlParameters(requestOptions);

Expand All @@ -124,7 +152,7 @@ const mapOutReq = (requestOptions, reqId, opts = {}) => {
const slicedPath = truncateText(path, maxPathChars);
const slicedHref = truncateText(href, maxHrefChars);

const metaHeaders = extractHeaders(headersRegex, requestOptions.headers);
const metaHeaders = extractHeaders(headersRegex, headerValueCallback, requestOptions.headers);

let metaBody = {};

Expand Down Expand Up @@ -184,12 +212,13 @@ function extractRequestMeta(
bodyKeysCallback,
maxBodyValueChars,
headersRegex,
headerValueCallback,
prefix = ''
) {
const method = ctx.request.method;

// pick headers
const metaHeaders = extractHeaders(headersRegex, ctx.request.headers, prefix);
const metaHeaders = extractHeaders(headersRegex, headerValueCallback, ctx.request.headers, prefix);

// pick body
let metaBody;
Expand Down
7 changes: 5 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ function safeExec(fn, logger) {
}
}

function extractHeaders(headersRegex, headers, prefix = '') {
function extractHeaders(headersRegex, headerValueCallback, headers, prefix = '') {
let metaHeaders = {};

if (!headersRegex) {
return metaHeaders;
}

const headerValue =
headerValueCallback && typeof headerValueCallback === 'function' ? headerValueCallback : (key, value) => value;

Object.keys(headers).forEach(header => {
if (new RegExp(headersRegex).test(header)) {
metaHeaders[header] = headers[header];
metaHeaders[header] = headerValue(header, headers[header]);
}
});

Expand Down
75 changes: 75 additions & 0 deletions test/lib/adapters/default/onInboundRequestTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,81 @@ describe('#defaultAdapter', () => {
});
});

it('should log headers mutated by the headerValueCallback', () => {
const opts = {
context: () => {
return {
test: true,
bodyKeys: ['testKayA']
};
},
logger: {
info: sandbox.spy()
},
constructor: {
EVENT: {
INBOUND_REQUEST_EVENT: 'INBOUND_REQUEST_EVENT'
}
},
bodyKeys: ['testKeyA'],
headerValueCallback: function(key, value) {
const regex = /id_token=[\w-]+\.[\w-]+\.[\w-]+/i;
return value.replace(regex, 'id_token=***');
},
headersRegex: new RegExp('XX-.*'),
serialize: msg => msg,
inbound: {
level: 'info'
},
traceHeaderName: 'x-ap-id',
sync: true
};
const ctx = {
request: {
method: 'post',
body: {
testKeyA: true
},
headers: {
'XX-something':
'https://site.com/path#id_token=xxx.xxx.xxx-xx&state=somestate&session_state=somesessionstate',
other: false,
'XX-something-else': 'random value',
'XX-something-different': 'id_token=some.token.tohide id_token=sometokentoshow'
},
req: {
url: '/test'
}
},
req: {
headers: {
'x-ap-id': uuid
}
},
originalUrl: '/test'
};
defaultAdapter.onInboundRequest.call(opts, { ctx });
opts.logger.info.calledOnce.should.equal(true);
opts.logger.info.args[0][0].should.eql({
test: true,
bodyKeys: ['testKayA'],
protocol: undefined,
method: 'POST',
path: '/test',
query: null,
requestId: uuid,
metaHeaders: {
headers: {
'XX-something': 'https://site.com/path#id_token=***&state=somestate&session_state=somesessionstate',
'XX-something-else': 'random value',
'XX-something-different': 'id_token=*** id_token=sometokentoshow'
}
},
userAgent: '',
log_tag: 'inbound_request'
});
});

it('should log full path when configured', () => {
const ctx = {
request: {
Expand Down
Loading

0 comments on commit e2449fe

Please sign in to comment.