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 * @param {http.ClientRequest} req
163 const requestLogData
= (req
) => {
175 * Return a subset of a response object, suitable for logging.
176 * @param {http.ServerResponse} res
178 const responseLogData
= (res
) => {
179 const response
= pick(res
, [
183 response
.headers
= res
.getHeaders();
189 * Store updates to defaultOptions, but no new properties.
190 * @param {Object} target
191 * @param {Object} defaultOptions
192 * @param {Object} options
194 const setOptions
= (target
, defaultOptions
, options
) => {
195 Object
.assign(target
, defaultOptions
, pick(options
, Object
.keys(defaultOptions
)));
199 * Return a list of source split at first delimiter.
200 * @param {string} src
201 * @param {string} delimiter
202 * @param {string} fill trailing stand-in if no delimiter in src
204 const splitFirst
= (src
, delimiter
, fill
) => {
205 const idx
= src
.indexOf(delimiter
);
207 return [ src
.slice(0, idx
), src
.slice(idx
+ 1) ];
209 return [ src
, fill
];
214 * Generate a new request identifier.
217 const requestId
= () => {
221 const nop
= () => { /**/ };
231 * Populates any absent logger levels.
232 * @param {Object} logger
234 const ensureLoggerLevels
= (logger
= {}) => {
235 for (const level
in nullLogger
) {
236 if (! (level
in logger
)) {
237 logger
[level
] = nullLogger
[level
];
244 * Merges folded header lines
245 * @param {String[]} lines
247 const unfoldHeaderLines
= (lines
) => {
248 const foldedLineRE
= /^(\t| +)(.*)$/;
250 lines
.reduceRight((_
, line
, idx
) => {
251 const result
= foldedLineRE
.exec(line
);
253 const prevIdx
= idx
- 1;
254 const mergedLine
= `${lines[prevIdx]} ${result[2]}`;
255 lines
.splice(prevIdx
, 2, mergedLine
);