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 * Determine whether a client has already requested a resource,
60 * based on If-Modified-Since and If-None-Match headers.
61 * @param {http.ClientRequest} req
62 * @param {Number} modifiedTimeMs
63 * @param {String} eTag
66 const isClientCached
= (req
, modifiedTimeMs
, eTag
) => {
67 let clientCached
= false;
69 const ifModifiedSince
= req
.getHeader(Enum
.Header
.IfModifiedSince
);
70 if (ifModifiedSince
) {
71 const ifModifiedSinceMs
= Date
.parse(ifModifiedSince
);
72 if (modifiedTimeMs
< ifModifiedSinceMs
) {
77 const ifNoneMatch
= req
.getHeader(Enum
.Header
.IfNoneMatch
);
79 const matches
= ifNoneMatch
.split(',').map((m
) => m
.trim());
80 if (matches
.includes(eTag
)
81 || (ifNoneMatch
=== '*' && eTag
)) {
84 // If if-none-matched header is present, it takes precedence over modified-since.
93 * Shallow merge for enums, to be called by derived constructor.
94 * Expects only one-level deep, is not recursive!
95 * @param {Object} origEnum
96 * @param {Object} additionalEnum
99 const mergeEnum
= (origEnum
, additionalEnum
) => {
100 for (const e
of Object
.keys(additionalEnum
)) {
101 if (typeof additionalEnum
[e
] === 'object') {
102 if (! (e
in origEnum
)) {
105 Object
.assign(origEnum
[e
], additionalEnum
[e
]);
107 origEnum
[e
] = additionalEnum
[e
];
114 * Isolate the general category of an http status code.
115 * @param {Number} statusCode
118 const httpStatusCodeClass
= (statusCode
) => Math
.floor(statusCode
/ 100);
120 const _isObject
= (obj
) => obj
&& typeof obj
=== 'object';
121 const _isArray
= (obj
) => Array
.isArray(obj
);
123 * Return a new object with all objects combined, later properties taking precedence.
124 * @param {...Object} objects
127 const mergeDeep
= (...objects
) => {
128 return objects
.reduce((acc
, obj
) => {
129 const objectProperties
= [...Object
.getOwnPropertyNames(obj
), ...Object
.getOwnPropertySymbols(obj
)];
130 objectProperties
.forEach((k
) => {
133 if (_isArray(oVal
)) {
134 acc
[k
] = (_isArray(aVal
) ? aVal : []).concat(oVal
);
135 } else if (_isObject(oVal
)) {
136 acc
[k
] = mergeDeep(_isObject(aVal
) ? aVal : {}, oVal
);
147 * Return a new object with selected props.
148 * @param {Object} obj
149 * @param {String[]} props
152 const pick
= (obj
, props
) => {
154 props
.forEach((prop
) => {
156 picked
[prop
] = obj
[prop
];
163 * Store all properties in defaultOptions on target from either options or defaultOptions.
164 * @param {Object} target
165 * @param {Object} defaultOptions
166 * @param {Object} options
168 const setOptions
= (target
, defaultOptions
, options
) => {
169 Object
.assign(target
, defaultOptions
, pick(options
, Object
.keys(defaultOptions
)));
173 * Return a two-item list of src, split at first delimiter encountered.
174 * @param {String} src
175 * @param {String} delimiter
176 * @param {String} fill trailing stand-in if no delimiter in src
178 const splitFirst
= (src
, delimiter
, fill
) => {
179 const idx
= src
.indexOf(delimiter
);
181 return [ src
.slice(0, idx
), src
.slice(idx
+ 1) ];
183 return [ src
, fill
];
188 * Generate a new request identifier, a time/host-based uuid.
191 const requestId
= () => {
196 * Merges folded header lines
197 * @param {String[]} lines
200 const unfoldHeaderLines
= (lines
) => {
201 const foldedLineRE
= /^(\t| +)(.*)$/;
203 lines
.reduceRight((_
, line
, idx
) => { // NOSONAR
204 const result
= foldedLineRE
.exec(line
);
206 const prevIdx
= idx
- 1;
207 const mergedLine
= `${lines[prevIdx]} ${result[2]}`;
208 lines
.splice(prevIdx
, 2, mergedLine
);