clean up some lint issues
[squeep-api-dingus] / lib / common.js
1 /* eslint-disable security/detect-object-injection */
2 'use strict';
3
4 /**
5 * Utility and miscellaneous functions.
6 */
7
8 const path = require('path');
9 const crypto = require('crypto');
10 const uuid = require('uuid');
11 const Enum = require('./enum');
12
13 /**
14 * @callback ScopeFn
15 * @param {String} scope
16 * @returns {String}
17 */
18 /**
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
22 * @returns {ScopeFn}
23 */
24 const fileScope = (filename) => {
25 let fScope = path.basename(filename, '.js');
26 if (fScope === 'index') {
27 fScope = path.basename(path.dirname(filename));
28 }
29 return (scope) => `${fScope}:${scope}`;
30 };
31
32 /**
33 * Simple ETag from data.
34 * @param {String} filePath (currently unused)
35 * @param {fs.Stats} fileStat
36 * @param {crypto.BinaryLike} fileData content
37 * @returns {String}
38 */
39 const generateETag = (_filePath, fileStat, fileData) => {
40 const hash = crypto.createHash('sha256');
41 if (fileStat && fileStat.mtimeMs) {
42 hash.update(fileStat.mtimeMs.toString());
43 }
44 hash.update(fileData);
45 const digest = hash.digest('base64').replace('=', '');
46 return `"${digest}"`;
47 };
48
49 /**
50 * Access property with default.
51 * @param {Object} obj
52 * @param {String} prop
53 * @param {*} def default value if prop does not exist for obj
54 * @return {*}
55 */
56 const get = (obj, prop, def) => obj && prop && (prop in obj) ? obj[prop] : def;
57
58 /**
59 * @param {http.ClientRequest} req
60 * @param {http.ServerResponse} res
61 * @param {Object} ctx
62 * @deprecated after v1.2.5 (integrated into logger module)
63 */
64 const handlerLogData = (req, res, ctx) => ({
65 req: requestLogData(req),
66 res: responseLogData(res),
67 ctx,
68 });
69
70 /**
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
76 * @returns {Boolean}
77 */
78 const isClientCached = (req, modifiedTimeMs, eTag) => {
79 let clientCached = false;
80
81 const ifModifiedSince = req.getHeader(Enum.Header.IfModifiedSince);
82 if (ifModifiedSince) {
83 const ifModifiedSinceMs = Date.parse(ifModifiedSince);
84 if (modifiedTimeMs < ifModifiedSinceMs) {
85 clientCached = true;
86 }
87 }
88
89 const ifNoneMatch = req.getHeader(Enum.Header.IfNoneMatch);
90 if (ifNoneMatch) {
91 const matches = ifNoneMatch.split(',').map((m) => m.trim());
92 if (matches.includes(eTag)
93 || (ifNoneMatch === '*' && eTag)) {
94 clientCached = true;
95 } else {
96 // If if-none-matched header is present, it takes precedence over modified-since.
97 clientCached = false;
98 }
99 }
100
101 return clientCached;
102 };
103
104 /**
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
109 * @returns {Object}
110 */
111 const mergeEnum = (origEnum, additionalEnum) => {
112 for (const e of Object.keys(additionalEnum)) {
113 if (typeof additionalEnum[e] === 'object') {
114 if (! (e in origEnum)) {
115 origEnum[e] = {};
116 }
117 Object.assign(origEnum[e], additionalEnum[e]);
118 } else {
119 origEnum[e] = additionalEnum[e];
120 }
121 }
122 return origEnum;
123 };
124
125 /**
126 * Isolate the general category of an http status code.
127 * @param {Number} statusCode
128 * @returns {Number}
129 */
130 const httpStatusCodeClass = (statusCode) => Math.floor(statusCode / 100);
131
132 const _isObject = (obj) => obj && typeof obj === 'object';
133 const _isArray = (obj) => Array.isArray(obj);
134 /**
135 * Return a new object with all objects combined, later properties taking precedence.
136 * @param {...Object} objects
137 * @returns {Object}
138 */
139 const mergeDeep = (...objects) => {
140 return objects.reduce((acc, obj) => {
141 const objectProperties = [...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)];
142 objectProperties.forEach((k) => {
143 const aVal = acc[k];
144 const oVal = obj[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);
149 } else {
150 acc[k] = oVal;
151 }
152 });
153 return acc;
154 }, {});
155 };
156
157
158 /**
159 * Return a new object with selected props.
160 * @param {Object} obj
161 * @param {String[]} props
162 * @returns {Object}
163 */
164 const pick = (obj, props) => {
165 const picked = {};
166 props.forEach((prop) => {
167 if (prop in obj) {
168 picked[prop] = obj[prop];
169 }
170 });
171 return picked;
172 };
173
174 /**
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)
179 */
180 const requestLogData = (req) => {
181 const data = pick(req, [
182 'method',
183 'url',
184 'httpVersion',
185 'headers',
186 'trailers',
187 ]);
188 scrubHeaderObject(data);
189 return data;
190 };
191
192
193 /**
194 * Remove sensitive header data.
195 * @param {Object} data
196 * @param {Object} data.headers
197 * @deprecated after v1.2.5 (integrated into logger module)
198 */
199 const scrubHeaderObject = (data) => {
200 if (data && data.headers && 'authorization' in data.headers) {
201 data.headers = Object.assign({}, data.headers, {
202 authorization: obscureAuthorizationHeader(data.headers['authorization']),
203 });
204 }
205 };
206
207
208 /**
209 * Hide sensitive part of an Authorization header.
210 * @param {String} authHeader
211 * @returns {String}
212 * @deprecated after v1.2.5 (integrated into logger module)
213 */
214 const obscureAuthorizationHeader = (authHeader) => {
215 if (!authHeader) {
216 return authHeader;
217 }
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));
221 };
222
223
224 /**
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)
228 */
229 const responseLogData = (res) => {
230 const response = pick(res, [
231 'statusCode',
232 'statusMessage',
233 ]);
234 response.headers = res.getHeaders();
235 return response;
236 };
237
238
239 /**
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
244 */
245 const setOptions = (target, defaultOptions, options) => {
246 Object.assign(target, defaultOptions, pick(options, Object.keys(defaultOptions)));
247 };
248
249 /**
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
254 */
255 const splitFirst = (src, delimiter, fill) => {
256 const idx = src.indexOf(delimiter);
257 if (idx >= 0) {
258 return [ src.slice(0, idx), src.slice(idx + 1) ];
259 } else {
260 return [ src, fill ];
261 }
262 };
263
264 /**
265 * Generate a new request identifier, a time/host-based uuid.
266 * @returns {String}
267 */
268 const requestId = () => {
269 return uuid.v1();
270 };
271
272 /**
273 * Do nothing.
274 */
275 const nop = () => { /**/ };
276
277 /**
278 * A logger object which does nothing.
279 */
280 const nullLogger = {
281 error: nop,
282 warn: nop,
283 info: nop,
284 log: nop,
285 debug: nop,
286 };
287
288 /**
289 * Populates any absent logger level functions on a logger object.
290 * @param {Object} logger
291 * @returns {Object}
292 */
293 const ensureLoggerLevels = (logger = {}) => {
294 for (const level in nullLogger) {
295 if (! (level in logger)) {
296 logger[level] = nullLogger[level];
297 }
298 }
299 return logger;
300 };
301
302 /**
303 * Merges folded header lines
304 * @param {String[]} lines
305 * @returns {String}
306 */
307 const unfoldHeaderLines = (lines) => {
308 const foldedLineRE = /^(\t| +)(.*)$/;
309 if (lines) {
310 lines.reduceRight((_, line, idx) => {
311 const result = foldedLineRE.exec(line);
312 if (result && idx) {
313 const prevIdx = idx - 1;
314 const mergedLine = `${lines[prevIdx]} ${result[2]}`;
315 lines.splice(prevIdx, 2, mergedLine);
316 return mergedLine;
317 }
318 }, null);
319 }
320 return lines;
321 };
322
323 module.exports = {
324 ensureLoggerLevels,
325 fileScope,
326 generateETag,
327 get,
328 handlerLogData,
329 httpStatusCodeClass,
330 isClientCached,
331 mergeDeep,
332 mergeEnum,
333 nop,
334 nullLogger,
335 obscureAuthorizationHeader,
336 pick,
337 requestId,
338 requestLogData,
339 responseLogData,
340 scrubHeaderObject,
341 setOptions,
342 splitFirst,
343 unfoldHeaderLines,
344 };