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');
15 * @param {String} scope
19 * Return a function which prefixes a provided scope with the most-
20 * relevant part of the filename, for use in logging.
21 * @param {String} filename
24 const fileScope
= (filename
) => {
25 let fScope
= path
.basename(filename
, '.js');
26 if (fScope
=== 'index') {
27 fScope
= path
.basename(path
.dirname(filename
));
29 return (scope
) => `${fScope}:${scope}`;
33 * Simple ETag from data.
34 * @param {String} filePath (currently unused)
35 * @param {fs.Stats} fileStat
36 * @param {crypto.BinaryLike} fileData content
39 const generateETag
= (_filePath
, fileStat
, fileData
) => {
40 const hash
= crypto
.createHash('sha256');
41 if (fileStat
?.mtimeMs
) {
42 hash
.update(fileStat
.mtimeMs
.toString());
44 hash
.update(fileData
);
45 const digest
= hash
.digest('base64').replace('=', '');
50 * Access property with default.
52 * @param {String} prop
53 * @param {*} def default value if prop does not exist for obj
56 const get = (obj
, prop
, def
) => obj
&& prop
&& (prop
in obj
) ? obj
[prop
] : def
;
59 * @param {http.ClientRequest} req
60 * @param {http.ServerResponse} res
62 * @deprecated after v1.2.5 (integrated into logger module)
64 const handlerLogData
= (req
, res
, ctx
) => ({
65 req: requestLogData(req
),
66 res: responseLogData(res
),
71 * Determine whether a client has already requested a resource,
72 * based on If-Modified-Since and If-None-Match headers.
73 * @param {http.ClientRequest} req
74 * @param {Number} modifiedTimeMs
75 * @param {String} eTag
78 const isClientCached
= (req
, modifiedTimeMs
, eTag
) => {
79 let clientCached
= false;
81 const ifModifiedSince
= req
.getHeader(Enum
.Header
.IfModifiedSince
);
82 if (ifModifiedSince
) {
83 const ifModifiedSinceMs
= Date
.parse(ifModifiedSince
);
84 if (modifiedTimeMs
< ifModifiedSinceMs
) {
89 const ifNoneMatch
= req
.getHeader(Enum
.Header
.IfNoneMatch
);
91 const matches
= ifNoneMatch
.split(',').map((m
) => m
.trim());
92 if (matches
.includes(eTag
)
93 || (ifNoneMatch
=== '*' && eTag
)) {
96 // If if-none-matched header is present, it takes precedence over modified-since.
105 * Shallow merge for enums, to be called by derived constructor.
106 * Expects only one-level deep, is not recursive!
107 * @param {Object} origEnum
108 * @param {Object} additionalEnum
111 const mergeEnum
= (origEnum
, additionalEnum
) => {
112 for (const e
of Object
.keys(additionalEnum
)) {
113 if (typeof additionalEnum
[e
] === 'object') {
114 if (! (e
in origEnum
)) {
117 Object
.assign(origEnum
[e
], additionalEnum
[e
]);
119 origEnum
[e
] = additionalEnum
[e
];
126 * Isolate the general category of an http status code.
127 * @param {Number} statusCode
130 const httpStatusCodeClass
= (statusCode
) => Math
.floor(statusCode
/ 100);
132 const _isObject
= (obj
) => obj
&& typeof obj
=== 'object';
133 const _isArray
= (obj
) => Array
.isArray(obj
);
135 * Return a new object with all objects combined, later properties taking precedence.
136 * @param {...Object} objects
139 const mergeDeep
= (...objects
) => {
140 return objects
.reduce((acc
, obj
) => {
141 const objectProperties
= [...Object
.getOwnPropertyNames(obj
), ...Object
.getOwnPropertySymbols(obj
)];
142 objectProperties
.forEach((k
) => {
145 if (_isArray(oVal
)) {
146 acc
[k
] = (_isArray(aVal
) ? aVal : []).concat(oVal
);
147 } else if (_isObject(oVal
)) {
148 acc
[k
] = mergeDeep(_isObject(aVal
) ? aVal : {}, oVal
);
159 * Return a new object with selected props.
160 * @param {Object} obj
161 * @param {String[]} props
164 const pick
= (obj
, props
) => {
166 props
.forEach((prop
) => {
168 picked
[prop
] = obj
[prop
];
175 * Return a subset of a request object, suitable for logging.
176 * Obscures sensitive header values.
177 * @param {http.ClientRequest} req
178 * @deprecated after v1.2.5 (integrated into logger module)
180 const requestLogData
= (req
) => {
181 const data
= pick(req
, [
188 scrubHeaderObject(data
);
194 * Remove sensitive header data.
195 * @param {Object} data
196 * @param {Object} data.headers
197 * @deprecated after v1.2.5 (integrated into logger module)
199 const scrubHeaderObject
= (data
) => {
200 if (data
?.headers
&& 'authorization' in data
.headers
) {
201 data
.headers
= Object
.assign({}, data
.headers
, {
202 authorization: obscureAuthorizationHeader(data
.headers
['authorization']),
209 * Hide sensitive part of an Authorization header.
210 * @param {String} authHeader
212 * @deprecated after v1.2.5 (integrated into logger module)
214 const obscureAuthorizationHeader
= (authHeader
) => {
218 const space
= authHeader
.indexOf(' ');
219 // This blurs entire string if no space found, because -1.
220 return authHeader
.slice(0, space
+ 1) + '*'.repeat(authHeader
.length
- (space
+ 1));
225 * Return a subset of a response object, suitable for logging.
226 * @param {http.ServerResponse} res
227 * @deprecated after v1.2.5 (integrated into logger module)
229 const responseLogData
= (res
) => {
230 const response
= pick(res
, [
234 response
.headers
= res
.getHeaders();
240 * Store all properties in defaultOptions on target from either options or defaultOptions.
241 * @param {Object} target
242 * @param {Object} defaultOptions
243 * @param {Object} options
245 const setOptions
= (target
, defaultOptions
, options
) => {
246 Object
.assign(target
, defaultOptions
, pick(options
, Object
.keys(defaultOptions
)));
250 * Return a two-item list of src, split at first delimiter encountered.
251 * @param {String} src
252 * @param {String} delimiter
253 * @param {String} fill trailing stand-in if no delimiter in src
255 const splitFirst
= (src
, delimiter
, fill
) => {
256 const idx
= src
.indexOf(delimiter
);
258 return [ src
.slice(0, idx
), src
.slice(idx
+ 1) ];
260 return [ src
, fill
];
265 * Generate a new request identifier, a time/host-based uuid.
268 const requestId
= () => {
275 const nop
= () => { /**/ };
278 * A logger object which does nothing.
289 * Populates any absent logger level functions on a logger object.
290 * @param {Object} logger
293 const ensureLoggerLevels
= (logger
= {}) => {
294 for (const level
in nullLogger
) {
295 if (! (level
in logger
)) {
296 logger
[level
] = nullLogger
[level
];
303 * Merges folded header lines
304 * @param {String[]} lines
307 const unfoldHeaderLines
= (lines
) => {
308 const foldedLineRE
= /^(\t| +)(.*)$/;
310 lines
.reduceRight((_
, line
, idx
) => {
311 const result
= foldedLineRE
.exec(line
);
313 const prevIdx
= idx
- 1;
314 const mergedLine
= `${lines[prevIdx]} ${result[2]}`;
315 lines
.splice(prevIdx
, 2, mergedLine
);
335 obscureAuthorizationHeader
,