Skip to content

Commit 9c01ae0

Browse files
authored
try to decompress binary when header content-encoding missing (#109)
* try to decompress if needed. * added unit test. * add unit tests. * bump version. * opimtization only decompress if content type is of text. * update test cases.
1 parent 64f6a82 commit 9c01ae0

File tree

5 files changed

+262
-60
lines changed

5 files changed

+262
-60
lines changed

lib/dataUtils.js

Lines changed: 126 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,88 @@ var url = require('url');
44
var hash = require('crypto-js/md5');
55
var isCreditCard = require('card-validator');
66
var assign = require('lodash/assign');
7+
var zlib = require('zlib');
8+
// Helper to check if buffer is binary (not utf8 string)
9+
function isBinaryBuffer(buf) {
10+
if (!Buffer.isBuffer(buf)) return false;
11+
// Heuristic: if buffer contains non-printable characters, treat as binary
12+
var text = buf.toString('utf8');
13+
// If conversion to string produces replacement chars, it's likely binary
14+
return text.includes('\uFFFD') || /[\x00-\x08\x0E-\x1F]/.test(text);
15+
}
16+
17+
// Decompress if needed based on headers and heuristics
18+
function decompressIfNeeded(body, headers) {
19+
if (!body) return body;
20+
var buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
21+
var encoding = (headers && (headers['content-encoding'] || headers['Content-Encoding'])) || '';
22+
var server = (headers && (headers['server'] || headers['Server'])) || '';
23+
var contentType = (headers && (headers['content-type'] || headers['Content-Type'])) || '';
24+
var decompressed = null;
25+
var isBinary = isBinaryBuffer(buf);
26+
27+
// Only decompress if content-type is json, xml, or text
28+
var isTextType = false;
29+
if (contentType) {
30+
var ct = contentType.toLowerCase();
31+
if (ct.indexOf('json') !== -1 || ct.indexOf('xml') !== -1 || ct.indexOf('text') !== -1) {
32+
isTextType = true;
33+
}
34+
}
35+
if (!isTextType) {
36+
// Not a text type, skip decompression
37+
return body;
38+
}
39+
40+
try {
41+
if (encoding && isBinary) {
42+
if (encoding.indexOf('gzip') !== -1) {
43+
decompressed = zlib.gunzipSync(buf);
44+
} else if (encoding.indexOf('deflate') !== -1) {
45+
decompressed = zlib.inflateSync(buf);
46+
} else if (encoding.indexOf('br') !== -1) {
47+
decompressed = zlib.brotliDecompressSync(buf);
48+
}
49+
} else if (isBinary) {
50+
// Heuristic: try brotli if server is cloudflare
51+
if (server.toLowerCase().indexOf('cloudflare') !== -1) {
52+
try {
53+
decompressed = zlib.brotliDecompressSync(buf);
54+
} catch (e) {
55+
// fallback below
56+
}
57+
}
58+
// Try gzip as fallback
59+
if (!decompressed) {
60+
try {
61+
decompressed = zlib.gunzipSync(buf);
62+
} catch (e) {}
63+
}
64+
// Try deflate as fallback
65+
if (!decompressed) {
66+
try {
67+
decompressed = zlib.inflateSync(buf);
68+
} catch (e) {}
69+
}
70+
// As last resort, try brotli if not already tried
71+
if (!decompressed && server.toLowerCase().indexOf('cloudflare') === -1) {
72+
try {
73+
decompressed = zlib.brotliDecompressSync(buf);
74+
} catch (e) {
75+
// fallback below
76+
}
77+
}
78+
}
79+
if (decompressed) {
80+
// Try to return as string if possible
81+
return decompressed.toString('utf8');
82+
}
83+
} catch (err) {
84+
// fallback below
85+
}
86+
// If not decompressed, return original
87+
return body;
88+
}
789

890
var logMessage = function (debug, functionName, message, details) {
991
if (debug) {
@@ -18,8 +100,7 @@ var logMessage = function (debug, functionName, message, details) {
18100
finalMessage = message + '\n' + JSON.stringify(details);
19101
}
20102
}
21-
} catch (err) {
22-
}
103+
} catch (err) {}
23104
console.log('MOESIF: [' + functionName + '] ' + finalMessage);
24105
}
25106
};
@@ -38,11 +119,7 @@ function _hashSensitive(jsonBody, debug) {
38119
if (itemType === 'number' || itemType === 'string') {
39120
var creditCardCheck = isCreditCard.number('' + item);
40121
if (creditCardCheck.isValid) {
41-
logMessage(
42-
debug,
43-
'hashSensitive',
44-
'looks like a credit card, performing hash.'
45-
);
122+
logMessage(debug, 'hashSensitive', 'looks like a credit card, performing hash.');
46123
return hash(item).toString();
47124
}
48125
}
@@ -58,24 +135,13 @@ function _hashSensitive(jsonBody, debug) {
58135
var innerVal = jsonBody[key];
59136
var innerValType = typeof innerVal;
60137

61-
if (
62-
key.toLowerCase().indexOf('password') !== -1 &&
63-
typeof innerVal === 'string'
64-
) {
65-
logMessage(
66-
debug,
67-
'hashSensitive',
68-
'key is password, so hashing the value.'
69-
);
138+
if (key.toLowerCase().indexOf('password') !== -1 && typeof innerVal === 'string') {
139+
logMessage(debug, 'hashSensitive', 'key is password, so hashing the value.');
70140
returnObject[key] = hash(jsonBody[key]).toString();
71141
} else if (innerValType === 'number' || innerValType === 'string') {
72142
var creditCardCheck = isCreditCard.number('' + innerVal);
73143
if (creditCardCheck.isValid) {
74-
logMessage(
75-
debug,
76-
'hashSensitive',
77-
'a field looks like credit card, performing hash.'
78-
);
144+
logMessage(debug, 'hashSensitive', 'a field looks like credit card, performing hash.');
79145
returnObject[key] = hash(jsonBody[key]).toString();
80146
} else {
81147
returnObject[key] = _hashSensitive(innerVal, debug);
@@ -97,10 +163,10 @@ function _getUrlFromRequestOptions(options, request) {
97163
options = url.parse(options);
98164
} else {
99165
// Avoid modifying the original options object.
100-
let originalOptions = options;
166+
var originalOptions = options;
101167
options = {};
102168
if (originalOptions) {
103-
Object.keys(originalOptions).forEach((key) => {
169+
Object.keys(originalOptions).forEach(function (key) {
104170
options[key] = originalOptions[key];
105171
});
106172
}
@@ -130,8 +196,7 @@ function _getUrlFromRequestOptions(options, request) {
130196
}
131197

132198
// Mix in default values used by http.request and others
133-
options.protocol =
134-
options.protocol || (request.agent && request.agent.protocol) || undefined;
199+
options.protocol = options.protocol || (request.agent && request.agent.protocol) || undefined;
135200
options.hostname = options.hostname || 'localhost';
136201

137202
return url.format(options);
@@ -156,15 +221,15 @@ function isPlainObject(value) {
156221
if (Object.prototype.toString.call(value) !== '[object Object]') {
157222
return false;
158223
}
159-
const prototype = Object.getPrototypeOf(value);
224+
var prototype = Object.getPrototypeOf(value);
160225
return prototype === null || prototype === Object.prototype;
161226
}
162227

163228
function isPlainObjectOrPrimitive(value) {
164229
if (isPlainObject(value)) {
165230
return true;
166231
}
167-
const type = typeof value;
232+
var type = typeof value;
168233
return (
169234
type === 'number' ||
170235
type === 'boolean' ||
@@ -184,11 +249,7 @@ function _safeJsonParse(body) {
184249
transferEncoding: undefined,
185250
};
186251
}
187-
if (
188-
Array.isArray(body) &&
189-
body.every &&
190-
body.every(isPlainObjectOrPrimitive)
191-
) {
252+
if (Array.isArray(body) && body.every && body.every(isPlainObjectOrPrimitive)) {
192253
return {
193254
body: body,
194255
transferEncoding: undefined,
@@ -241,6 +302,7 @@ function getRequestHeaders(requestOptions, request) {
241302
return {};
242303
}
243304

305+
// this is mostly for outgoing data capture.
244306
function _getEventModelFromRequestAndResponse(
245307
requestOptions,
246308
request,
@@ -260,17 +322,23 @@ function _getEventModelFromRequestAndResponse(
260322
logData.request.headers = getRequestHeaders(requestOptions, request);
261323
logData.request.time = requestTime;
262324

263-
if (requestBody) {
264-
var isReqBodyMaybeJson = _startWithJson(requestBody);
325+
// Decompress requestBody if needed
326+
var reqHeaders = logData.request.headers || {};
327+
var decompressedRequestBody = requestBody
328+
? decompressIfNeeded(requestBody, reqHeaders)
329+
: requestBody;
330+
331+
if (decompressedRequestBody) {
332+
var isReqBodyMaybeJson = _startWithJson(decompressedRequestBody);
265333

266334
if (isReqBodyMaybeJson) {
267-
var parsedReqBody = _safeJsonParse(requestBody);
335+
var parsedReqBody = _safeJsonParse(decompressedRequestBody);
268336

269337
logData.request.transferEncoding = parsedReqBody.transferEncoding;
270338
logData.request.body = parsedReqBody.body;
271339
} else {
272340
logData.request.transferEncoding = 'base64';
273-
logData.request.body = _bodyToBase64(requestBody);
341+
logData.request.body = _bodyToBase64(decompressedRequestBody);
274342
}
275343
}
276344

@@ -279,17 +347,23 @@ function _getEventModelFromRequestAndResponse(
279347
logData.response.status = (response && (response.statusCode || response.status)) || 599;
280348
logData.response.headers = assign({}, (response && response.headers) || {});
281349

282-
if (responseBody) {
283-
var isResBodyMaybeJson = _startWithJson(responseBody);
350+
// Decompress responseBody if needed
351+
var resHeaders = logData.response.headers || {};
352+
var decompressedResponseBody = responseBody
353+
? decompressIfNeeded(responseBody, resHeaders)
354+
: responseBody;
355+
356+
if (decompressedResponseBody) {
357+
var isResBodyMaybeJson = _startWithJson(decompressedResponseBody);
284358

285359
if (isResBodyMaybeJson) {
286-
var parsedResBody = _safeJsonParse(responseBody);
360+
var parsedResBody = _safeJsonParse(decompressedResponseBody);
287361

288362
logData.response.transferEncoding = parsedResBody.transferEncoding;
289363
logData.response.body = parsedResBody.body;
290364
} else {
291365
logData.response.transferEncoding = 'base64';
292-
logData.response.body = _bodyToBase64(responseBody);
366+
logData.response.body = _bodyToBase64(decompressedResponseBody);
293367
}
294368
}
295369

@@ -302,10 +376,7 @@ function isJsonHeader(msg) {
302376
if (headers['content-encoding']) {
303377
return false;
304378
}
305-
if (
306-
headers['content-type'] &&
307-
headers['content-type'].indexOf('json') >= 0
308-
) {
379+
if (headers['content-type'] && headers['content-type'].indexOf('json') >= 0) {
309380
return true;
310381
}
311382
}
@@ -314,7 +385,7 @@ function isJsonHeader(msg) {
314385

315386
function approximateObjectSize(obj) {
316387
try {
317-
const str = JSON.stringify(obj);
388+
var str = JSON.stringify(obj);
318389
return str.length;
319390
} catch (err) {
320391
return 0;
@@ -347,9 +418,7 @@ function appendChunk(buf, chunk) {
347418
}
348419
} else if (typeof chunk === 'string') {
349420
try {
350-
return buf
351-
? Buffer.concat([buf, Buffer.from(chunk)])
352-
: Buffer.from(chunk);
421+
return buf ? Buffer.concat([buf, Buffer.from(chunk)]) : Buffer.from(chunk);
353422
} catch (err) {
354423
return buf;
355424
}
@@ -401,14 +470,14 @@ function getReqHeaders(req) {
401470
}
402471

403472
function generateUUIDv4() {
404-
let timeNow = new Date().getTime(); // Current time in milliseconds
405-
let timeRandom = timeNow + Math.random(); // Combine time and random number
406-
407-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
408-
let r = (timeRandom + Math.random() * 16) % 16 | 0; // Mix in timeRandom for extra entropy
409-
timeRandom = Math.floor(timeRandom / 16);
410-
let v = c === 'x' ? r : (r & 0x3 | 0x8); // Handle the fixed bits for UUIDv4
411-
return v.toString(16);
473+
let timeNow = new Date().getTime(); // Current time in milliseconds
474+
let timeRandom = timeNow + Math.random(); // Combine time and random number
475+
476+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
477+
let r = (timeRandom + Math.random() * 16) % 16 | 0; // Mix in timeRandom for extra entropy
478+
timeRandom = Math.floor(timeRandom / 16);
479+
let v = c === 'x' ? r : (r & 0x3) | 0x8; // Handle the fixed bits for UUIDv4
480+
return v.toString(16);
412481
});
413482
}
414483

@@ -428,4 +497,5 @@ module.exports = {
428497
ensureToString: ensureToString,
429498
getReqHeaders: getReqHeaders,
430499
generateUUIDv4: generateUUIDv4,
500+
decompressIfNeeded: decompressIfNeeded,
431501
};

lib/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function makeMoesifMiddleware(options) {
111111
* @type {string}
112112
*/
113113
config.ApplicationId = options.applicationId || options.ApplicationId;
114-
config.UserAgent = 'moesif-nodejs/' + '3.9.1';
114+
config.UserAgent = 'moesif-nodejs/' + '3.10.0';
115115
config.BaseUri = options.baseUri || options.BaseUri || config.BaseUri;
116116
// default retry to 1.
117117
config.retry = isNil(options.retry) ? 1 : options.retry;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "moesif-nodejs",
3-
"version": "3.9.1",
3+
"version": "3.10.0",
44
"description": "Monitoring agent to log API calls to Moesif for deep API analytics",
55
"main": "lib/index.js",
66
"typings": "dist/index.d.ts",

0 commit comments

Comments
 (0)