f839e7a05cf16790779a75a2d6c15d19689d9f98
[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?.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 * 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
64 * @returns {Boolean}
65 */
66 const isClientCached = (req, modifiedTimeMs, eTag) => {
67 let clientCached = false;
68
69 const ifModifiedSince = req.getHeader(Enum.Header.IfModifiedSince);
70 if (ifModifiedSince) {
71 const ifModifiedSinceMs = Date.parse(ifModifiedSince);
72 if (modifiedTimeMs < ifModifiedSinceMs) {
73 clientCached = true;
74 }
75 }
76
77 const ifNoneMatch = req.getHeader(Enum.Header.IfNoneMatch);
78 if (ifNoneMatch) {
79 const matches = ifNoneMatch.split(',').map((m) => m.trim());
80 if (matches.includes(eTag)
81 || (ifNoneMatch === '*' && eTag)) {
82 clientCached = true;
83 } else {
84 // If if-none-matched header is present, it takes precedence over modified-since.
85 clientCached = false;
86 }
87 }
88
89 return clientCached;
90 };
91
92 /**
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
97 * @returns {Object}
98 */
99 const mergeEnum = (origEnum, additionalEnum) => {
100 for (const e of Object.keys(additionalEnum)) {
101 if (typeof additionalEnum[e] === 'object') {
102 if (! (e in origEnum)) {
103 origEnum[e] = {};
104 }
105 Object.assign(origEnum[e], additionalEnum[e]);
106 } else {
107 origEnum[e] = additionalEnum[e];
108 }
109 }
110 return origEnum;
111 };
112
113 /**
114 * Isolate the general category of an http status code.
115 * @param {Number} statusCode
116 * @returns {Number}
117 */
118 const httpStatusCodeClass = (statusCode) => Math.floor(statusCode / 100);
119
120 const _isObject = (obj) => obj && typeof obj === 'object';
121 const _isArray = (obj) => Array.isArray(obj);
122 /**
123 * Return a new object with all objects combined, later properties taking precedence.
124 * @param {...Object} objects
125 * @returns {Object}
126 */
127 const mergeDeep = (...objects) => {
128 return objects.reduce((acc, obj) => {
129 const objectProperties = [...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)];
130 objectProperties.forEach((k) => {
131 const aVal = acc[k];
132 const oVal = obj[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);
137 } else {
138 acc[k] = oVal;
139 }
140 });
141 return acc;
142 }, {});
143 };
144
145
146 /**
147 * Return a new object with selected props.
148 * @param {Object} obj
149 * @param {String[]} props
150 * @returns {Object}
151 */
152 const pick = (obj, props) => {
153 const picked = {};
154 props.forEach((prop) => {
155 if (prop in obj) {
156 picked[prop] = obj[prop];
157 }
158 });
159 return picked;
160 };
161
162 /**
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
167 */
168 const setOptions = (target, defaultOptions, options) => {
169 Object.assign(target, defaultOptions, pick(options, Object.keys(defaultOptions)));
170 };
171
172 /**
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
177 */
178 const splitFirst = (src, delimiter, fill) => {
179 const idx = src.indexOf(delimiter);
180 if (idx >= 0) {
181 return [ src.slice(0, idx), src.slice(idx + 1) ];
182 } else {
183 return [ src, fill ];
184 }
185 };
186
187 /**
188 * Generate a new request identifier, a time/host-based uuid.
189 * @returns {String}
190 */
191 const requestId = () => {
192 return uuid.v1();
193 };
194
195 /**
196 * Merges folded header lines
197 * @param {String[]} lines
198 * @returns {String}
199 */
200 const unfoldHeaderLines = (lines) => {
201 const foldedLineRE = /^(\t| +)(.*)$/;
202 if (lines) {
203 lines.reduceRight((_, line, idx) => { // NOSONAR
204 const result = foldedLineRE.exec(line);
205 if (result && idx) {
206 const prevIdx = idx - 1;
207 const mergedLine = `${lines[prevIdx]} ${result[2]}`;
208 lines.splice(prevIdx, 2, mergedLine);
209 return mergedLine;
210 }
211 }, null);
212 }
213 return lines;
214 };
215
216 module.exports = {
217 fileScope,
218 generateETag,
219 get,
220 httpStatusCodeClass,
221 isClientCached,
222 mergeDeep,
223 mergeEnum,
224 pick,
225 requestId,
226 setOptions,
227 splitFirst,
228 unfoldHeaderLines,
229 };