1 /* eslint-disable security/detect-object-injection */
5 * Utility and miscellaneous functions.
8 const path
= require('path');
9 const crypto
= require('crypto');
10 const uuid
= require('uuid');
11 const Enum
= require('./enum');
14 * Return a function which combines a part of the filename with a scope, for use in logging.
15 * @param {string} filename
17 const fileScope
= (filename
) => {
18 let fScope
= path
.basename(filename
, '.js');
19 if (fScope
=== 'index') {
20 fScope
= path
.basename(path
.dirname(filename
));
22 return (scope
) => `${fScope}:${scope}`;
26 * Simple ETag from data.
27 * @param {string} filePath
28 * @param {object} fileStat
31 const generateETag
= (_filePath
, fileStat
, fileData
) => {
32 const hash
= crypto
.createHash('sha256');
33 if (fileStat
&& fileStat
.mtimeMs
) {
34 hash
.update(fileStat
.mtimeMs
.toString());
36 hash
.update(fileData
);
37 const digest
= hash
.digest('base64').replace('=', '');
43 * @param {string} prop
46 const get = (obj
, prop
, def
) => obj
&& prop
&& (prop
in obj
) ? obj
[prop
] : def
;
49 * @param {http.ClientRequest} req
50 * @param {http.ServerResponse} res
53 const handlerLogData
= (req
, res
, ctx
) => ({
54 req: requestLogData(req
),
55 res: responseLogData(res
),
61 * @param {http.ClientRequest} req
62 * @param {Number} modifiedTimeMs
63 * @param {string} eTag
65 const isClientCached
= (req
, modifiedTimeMs
, eTag
) => {
66 let clientCached
= false;
68 const ifModifiedSince
= req
.getHeader(Enum
.Header
.IfModifiedSince
);
69 if (ifModifiedSince
) {
70 const ifModifiedSinceMs
= Date
.parse(ifModifiedSince
);
71 if (modifiedTimeMs
< ifModifiedSinceMs
) {
76 const ifNoneMatch
= req
.getHeader(Enum
.Header
.IfNoneMatch
);
78 const matches
= ifNoneMatch
.split(',').map((m
) => m
.trim());
79 if (matches
.includes(eTag
)
80 || (ifNoneMatch
=== '*' && eTag
)) {
83 // If if-none-matched header is present, it takes precedence over modified-since.
92 * Shallow merge for enums, to be called by derived constructor.
93 * Expects only one-level deep, is not recursive!
94 * @param {Object} origEnum
95 * @param {Object} additionalEnum
97 const mergeEnum
= (origEnum
, additionalEnum
) => {
98 for (const e
of Object
.keys(additionalEnum
)) {
99 if (typeof additionalEnum
[e
] === 'object') {
100 if (! (e
in origEnum
)) {
103 Object
.assign(origEnum
[e
], additionalEnum
[e
]);
105 origEnum
[e
] = additionalEnum
[e
];
112 * Isolate the general category of an http status code.
113 * @param {Number} statusCode
116 const httpStatusCodeClass
= (statusCode
) => Math
.floor(statusCode
/ 100);
118 const _isObject
= (obj
) => obj
&& typeof obj
=== 'object';
119 const _isArray
= (obj
) => Array
.isArray(obj
);
121 * Return a new object with all objects combined.
122 * @param {...any} objects
125 const mergeDeep
= (...objects
) => {
126 return objects
.reduce((acc
, obj
) => {
127 const objectProperties
= [...Object
.getOwnPropertyNames(obj
), ...Object
.getOwnPropertySymbols(obj
)];
128 objectProperties
.forEach((k
) => {
131 if (_isArray(oVal
)) {
132 acc
[k
] = (_isArray(aVal
) ? aVal : []).concat(oVal
);
133 } else if (_isObject(oVal
)) {
134 acc
[k
] = mergeDeep(_isObject(aVal
) ? aVal : {}, oVal
);
145 * Return a new object with selected props.
146 * @param {Object} obj
147 * @param {string[]} props
149 const pick
= (obj
, props
) => {
151 props
.forEach((prop
) => {
153 picked
[prop
] = obj
[prop
];
160 * Return a subset of a request object, suitable for logging.
161 * Obscures sensitive header values.
162 * @param {http.ClientRequest} req
164 const requestLogData
= (req
) => {
165 const data
= pick(req
, [
172 scrubHeaderObject(data
);
178 * Remove sensitive header data.
179 * @param {Object} data
180 * @param {Object} data.headers
182 const scrubHeaderObject
= (data
) => {
183 if (data
&& data
.headers
&& 'authorization' in data
.headers
) {
184 data
.headers
= Object
.assign({}, data
.headers
, {
185 authorization: obscureAuthorizationHeader(data
.headers
['authorization']),
192 * Hide sensitive part of an Authorization header.
193 * @param {String} authHeader
196 const obscureAuthorizationHeader
= (authHeader
) => {
200 const space
= authHeader
.indexOf(' ');
201 // This blurs entire string if no space found, because -1.
202 return authHeader
.slice(0, space
+ 1) + '*'.repeat(authHeader
.length
- (space
+ 1));
207 * Return a subset of a response object, suitable for logging.
208 * @param {http.ServerResponse} res
210 const responseLogData
= (res
) => {
211 const response
= pick(res
, [
215 response
.headers
= res
.getHeaders();
221 * Store updates to defaultOptions, but no new properties.
222 * @param {Object} target
223 * @param {Object} defaultOptions
224 * @param {Object} options
226 const setOptions
= (target
, defaultOptions
, options
) => {
227 Object
.assign(target
, defaultOptions
, pick(options
, Object
.keys(defaultOptions
)));
231 * Return a list of source split at first delimiter.
232 * @param {string} src
233 * @param {string} delimiter
234 * @param {string} fill trailing stand-in if no delimiter in src
236 const splitFirst
= (src
, delimiter
, fill
) => {
237 const idx
= src
.indexOf(delimiter
);
239 return [ src
.slice(0, idx
), src
.slice(idx
+ 1) ];
241 return [ src
, fill
];
246 * Generate a new request identifier.
249 const requestId
= () => {
253 const nop
= () => { /**/ };
263 * Populates any absent logger levels.
264 * @param {Object} logger
266 const ensureLoggerLevels
= (logger
= {}) => {
267 for (const level
in nullLogger
) {
268 if (! (level
in logger
)) {
269 logger
[level
] = nullLogger
[level
];
276 * Merges folded header lines
277 * @param {String[]} lines
279 const unfoldHeaderLines
= (lines
) => {
280 const foldedLineRE
= /^(\t| +)(.*)$/;
282 lines
.reduceRight((_
, line
, idx
) => {
283 const result
= foldedLineRE
.exec(line
);
285 const prevIdx
= idx
- 1;
286 const mergedLine
= `${lines[prevIdx]} ${result[2]}`;
287 lines
.splice(prevIdx
, 2, mergedLine
);
307 obscureAuthorizationHeader
,