a5f77ab19fa02f1df8d8fec140ef5347c94eb5f2
[squeep-api-dingus] / lib / dingus.js
1 /* eslint-disable security/detect-object-injection */
2 'use strict';
3
4 /**
5 * A very minimal API server framework.
6 * Just a self-contained router and some request glue.
7 */
8
9 require('./patches');
10 const { promises: fsPromises } = require('fs');
11 const path = require('path');
12 const querystring = require('querystring');
13 const common = require('./common');
14 const ContentNegotiation = require('./content-negotiation');
15 const Enum = require('./enum');
16 const { ResponseError, RouterNoPathError, RouterNoMethodError } = require('./errors');
17 const { extensionToMime } = require('./mime-helper');
18 const Router = require('./router');
19 const Template = require('./template');
20
21 // For logging.
22 const { fileScope } = require('@squeep/log-helper');
23 const _fileScope = fileScope(__filename);
24
25 const defaultOptions = {
26 ignoreTrailingSlash: true,
27 proxyPrefix: '',
28 strictAccept: true,
29 selfBaseUrl: '',
30 staticMetadata: true,
31 staticPath: undefined, // No reasonable default
32 trustProxy: true,
33 intrinsicHeadMethod: true,
34 intrinsicHeadPersistBody: false,
35 querystring,
36 };
37
38 class Dingus {
39 /**
40 * @param {Object} logger object which implements logging methods
41 * @param {Object} options
42 * @param {Boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route
43 * @param {string} options.proxyPrefix leading part of url path to strip
44 * @param {Boolean} options.strictAccept whether to error on unsupported Accept type
45 * @param {string} options.selfBaseUrl for constructing links
46 * @param {Boolean} options.staticMetadata serve static headers with static files
47 * @param {Boolean} options.trustProxy trust some header data to be provided by proxy
48 * @param {Boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method
49 * @param {Boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests
50 * @param {Object} options.querystring alternate qs parser to use
51 */
52 constructor(logger = console, options = {}) {
53 common.setOptions(this, defaultOptions, options);
54
55 this.router = new Router(options);
56
57 if (!this.proxyPrefix) {
58 this._stripPrefix = (p) => p;
59 }
60
61 this.responseTypes = [
62 Enum.ContentType.TextHTML,
63 Enum.ContentType.TextPlain,
64 Enum.ContentType.ApplicationJson,
65 ];
66
67 this.logger = logger;
68 }
69
70
71 /**
72 * Resolve relative and empty paths in url
73 * @param {string} p path
74 */
75 _normalizePath(p) {
76 const pathNorm = path.normalize(p); // This isn't perfectly correct, but it's easy...
77 return this._stripPrefix(pathNorm);
78 }
79
80
81 /**
82 * Remove a leading portion of url path
83 * N.B. This method gets obliterated if there is no prefix defined at construction
84 * @param {string} p path
85 */
86 _stripPrefix(p) {
87 if (p.startsWith(this.proxyPrefix)) {
88 return p.slice(this.proxyPrefix.length);
89 }
90 return p;
91 }
92
93
94 /**
95 * Returns the path part, and querystring object, from a request url.
96 * @param {string} url
97 */
98 _splitUrl(url) {
99 const [ p, qs ] = common.splitFirst(url, '?');
100 return {
101 pathPart: this._normalizePath(p),
102 queryParams: this.querystring.parse(qs),
103 };
104 }
105
106
107 /**
108 * Insert a new path handler
109 * @param {string} method
110 * @param {string} urlPath
111 * @param {fn} handler
112 */
113 on(method, urlPath, handler, ...handlerArgs) {
114 this.router.on(method, urlPath, handler, handlerArgs);
115 }
116
117
118 /**
119 * Common header tagging for all requests.
120 * Add our own identifier, and persist any external transit identifiers.
121 * Sets requestId on ctx to a new uuid.
122 * If X-Request-Id or X-Correlation-Id exist on incoming headers, sets them
123 * on outgoing headers and sets on ctx.
124 * @param {http.ClientRequest} req
125 * @param {http.ServerResponse} res
126 * @param {object} ctx
127 */
128 static tagContext(req, res, ctx) {
129 const requestId = common.requestId();
130 ctx.requestId = requestId;
131 res.setHeader(Enum.Header.RequestId, requestId);
132 [Enum.Header.XRequestId, Enum.Header.XCorrelationId].forEach((h) => {
133 const v = req.getHeader(h);
134 if (v) {
135 ctx[h.replace(/-/g, '')] = v;
136 res.setHeader(h, v);
137 }
138 });
139 return requestId;
140 }
141
142
143 /**
144 *
145 * @param {http.ClientRequest} req
146 */
147 _getAddress(req) {
148 // TODO: RFC7239 Forwarded support
149 const address = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedFor)) ||
150 (this.trustProxy && req?.getHeader(Enum.Header.XRealIP)) ||
151 (req?.connection?.remoteAddress) ||
152 '';
153 return address.split(/\s*,\s*/u)[0];
154 }
155
156
157 /**
158 *
159 * @param {http.ClientRequest} req
160 */
161 _getProtocol(req) {
162 // TODO: RFC7239 Forwarded support
163 const protocol = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedProto)) ||
164 ((req?.connection?.encrypted) ? 'https' : 'http');
165 return protocol.split(/\s*,\s*/u)[0];
166 }
167
168
169 /**
170 * Sets ctx.clientAddress and ctx.clientProtocol.
171 * @param {http.ClientRequest} req
172 * @param {http.ServerResponse} res
173 * @param {object} ctx
174 */
175 clientAddressContext(req, res, ctx) {
176 ctx.clientAddress = this._getAddress(req);
177 ctx.clientProtocol = this._getProtocol(req);
178 }
179
180
181 /**
182 * Called before every request handler.
183 * Sets tracking identifiers and client information on ctx.
184 * @param {http.ClientRequest} req
185 * @param {http.ServerResponse} res
186 * @param {object} ctx
187 */
188 async preHandler(req, res, ctx) {
189 Dingus.tagContext(req, res, ctx);
190 this.clientAddressContext(req, res, ctx);
191 }
192
193
194 /**
195 * Helper for collecting chunks as array of buffers.
196 * @param {Buffer[]} chunks
197 * @param {string|Buffer} chunk
198 * @param {string} encoding
199 */
200 static pushBufChunk(chunks, chunk, encoding = 'utf8') {
201 if (chunk) {
202 if (typeof chunk === 'string') {
203 chunk = Buffer.from(chunk, encoding);
204 }
205 chunks.push(chunk);
206 }
207 }
208
209
210 /**
211 * Sets ctx.responseBody and calls handler upon res.end().
212 * @param {http.ClientRequest} req
213 * @param {http.ServerResponse} res
214 * @param {object} ctx
215 * @param {*} handler fn(req, res, ctx)
216 */
217 static setEndBodyHandler(req, res, ctx, handler) {
218 const origWrite = res.write.bind(res);
219 const origEnd = res.end.bind(res);
220 const chunks = [];
221 res.write = function (chunk, encoding, ...rest) {
222 Dingus.pushBufChunk(chunks, chunk, encoding);
223 return origWrite(chunk, encoding, ...rest);
224 };
225 res.end = function (data, encoding, ...rest) {
226 Dingus.pushBufChunk(chunks, data, encoding);
227 ctx.responseBody = Buffer.concat(chunks);
228 handler(req, res, ctx);
229 return origEnd(data, encoding, ...rest);
230 };
231 }
232
233
234 /**
235 * Intercept writes for head requests, do not send to client,
236 * but send length, and make body available in context.
237 * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging.
238 * @param {http.ClientRequest} req
239 * @param {http.ServerResponse} res
240 * @param {object} ctx
241 * @param {Boolean} persistResponseBody
242 */
243 static setHeadHandler(req, res, ctx, persistResponseBody = false) {
244 if (req.method === 'HEAD') {
245 const origEnd = res.end.bind(res);
246 const chunks = [];
247 res.write = function (chunk, encoding) {
248 Dingus.pushBufChunk(chunks, chunk, encoding);
249 // No call to original res.write.
250 };
251 res.end = function (data, encoding, ...rest) {
252 Dingus.pushBufChunk(chunks, data, encoding);
253 const responseBody = Buffer.concat(chunks);
254 res.setHeader(Enum.Header.ContentLength, Buffer.byteLength(responseBody));
255 if (persistResponseBody) {
256 ctx.responseBody = responseBody;
257 }
258 return origEnd(undefined, encoding, ...rest);
259 };
260 }
261 }
262
263
264 /**
265 * Resolve the handler to invoke for a request.
266 * @param {http.ClientRequest} req
267 * @param {http.ServerResponse} res
268 * @param {object} ctx
269 * @returns {object}
270 */
271 _determineHandler(req, res, ctx) {
272 const _scope = _fileScope('_determineHandler');
273
274 const { pathPart, queryParams } = this._splitUrl(req.url);
275 ctx.queryParams = queryParams;
276
277 let handler, handlerArgs = [];
278 try {
279 ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx));
280 } catch (e) {
281 if (e instanceof URIError) {
282 handler = this.handlerBadRequest.bind(this);
283 } else if (e instanceof RouterNoPathError) {
284 handler = this.handlerNotFound.bind(this);
285 } else if (e instanceof RouterNoMethodError) {
286 if (this.intrinsicHeadMethod && req.method === 'HEAD') {
287 ({ handler, handlerArgs } = this._determineHeadHandler(req, res, ctx, pathPart));
288 } else {
289 handler = this.handlerMethodNotAllowed.bind(this);
290 }
291 } else {
292 this.logger.error(_scope, 'unexpected error', { error: e });
293 handler = this.handlerInternalServerError.bind(this);
294 }
295 }
296 return { handler, handlerArgs };
297 }
298
299
300 /**
301 * For intrinsic HEAD requests, resolve the handler to invoke.
302 * @param {http.ClientRequest} req
303 * @param {http.ServerResponse} res
304 * @param {object} ctx
305 * @param {string} pathPart
306 * @returns {object}
307 */
308 _determineHeadHandler(req, res, ctx, pathPart) {
309 const _scope = _fileScope('_determineHeadHandler');
310 let handler, handlerArgs = [];
311 try {
312 ({ handler, handlerArgs } = this.router.lookup('GET', pathPart, ctx));
313 Dingus.setHeadHandler(req, res, ctx, this.intrinsicHeadPersistBody);
314 } catch (e) {
315 if (e instanceof RouterNoMethodError) {
316 handler = this.handlerMethodNotAllowed.bind(this);
317 } else {
318 this.logger.error(_scope, 'unexpected error', { error: e });
319 handler = this.handlerInternalServerError.bind(this);
320 }
321 }
322 return { handler, handlerArgs };
323 }
324
325
326 /**
327 * Dispatch the handler for a request
328 * @param {http.ClientRequest} req
329 * @param {http.ServerResponse} res
330 * @param {object} ctx
331 */
332 async dispatch(req, res, ctx = {}) {
333 const { handler, handlerArgs } = this._determineHandler(req, res, ctx);
334 try {
335 await this.preHandler(req, res, ctx);
336 return await handler(req, res, ctx, ...handlerArgs);
337 } catch (e) {
338 ctx.error = e;
339 this.sendErrorResponse(e, req, res, ctx);
340 }
341 }
342
343
344 /**
345 * Return normalized type, without any parameters.
346 * @param {http.ClientRequest} req
347 * @returns {string}
348 */
349 static getRequestContentType(req) {
350 const contentType = req.getHeader(Enum.Header.ContentType);
351 return (contentType || '').split(';')[0].trim().toLowerCase();
352 }
353
354
355 /**
356 * Parse rawBody as contentType into ctx.parsedBody.
357 * @param {string} contentType
358 * @param {object} ctx
359 * @param {string|buffer} rawBody
360 */
361 parseBody(contentType, ctx, rawBody) {
362 const _scope = _fileScope('parseBody');
363
364 switch (contentType) {
365 case Enum.ContentType.ApplicationForm:
366 ctx.parsedBody = this.querystring.parse(rawBody);
367 break;
368
369 case Enum.ContentType.ApplicationJson:
370 try {
371 ctx.parsedBody = JSON.parse(rawBody);
372 } catch (e) {
373 this.logger.debug(_scope, 'JSON parse failed', { requestId: ctx.requestId, error: e });
374 throw new ResponseError(Enum.ErrorResponse.BadRequest, e.message);
375 }
376 break;
377
378 default:
379 this.logger.debug(_scope, 'unhandled content-type', { requestId: ctx.requestId, contentType });
380 throw new ResponseError(Enum.ErrorResponse.UnsupportedMediaType);
381 }
382 }
383
384
385 /**
386 * Return all body data from a request.
387 * @param {http.ClientRequest} req
388 * @param {Number=} maximumBodySize
389 * @param {Boolean=} toString
390 */
391 async bodyData(req, maximumBodySize, toString = true) {
392 const _scope = _fileScope('bodyData');
393 return new Promise((resolve, reject) => {
394 const body = [];
395 let length = 0;
396 req.on('data', (chunk) => {
397 body.push(chunk);
398 length += Buffer.byteLength(chunk);
399 if (maximumBodySize && length > maximumBodySize) {
400 this.logger.debug(_scope, 'body data exceeded limit', { length, maximumBodySize });
401 reject(new ResponseError(Enum.ErrorResponse.RequestEntityTooLarge));
402 }
403 });
404 req.on('end', () => {
405 const bodyBuffer = Buffer.concat(body);
406 resolve(toString ? bodyBuffer.toString() : bodyBuffer);
407 });
408 req.on('error', (e) => {
409 this.logger.error(_scope, 'failed', { error: e });
410 reject(e);
411 });
412 });
413 }
414
415
416 /**
417 * Read and parse request body data.
418 * Sets ctx.parsedBody, and optionally ctx.rawBody.
419 * @param {http.ClientRequest} req
420 * @param {http.ServerResponse} res
421 * @param {object} ctx
422 * @param {object}
423 * @param {Boolean} .parseEmptyBody
424 * @param {Boolean} .persistRawBody
425 */
426 async ingestBody(req, res, ctx, { parseEmptyBody = true, persistRawBody = false, maximumBodySize } = {}) {
427 const rawBody = await this.bodyData(req, maximumBodySize);
428 if (persistRawBody) {
429 ctx.rawBody = rawBody;
430 }
431 if (rawBody || parseEmptyBody) {
432 const contentType = Dingus.getRequestContentType(req);
433 this.parseBody(contentType, ctx, rawBody);
434 }
435 }
436
437
438 /**
439 * Return the best matching response type.
440 * @param {string[]} responseTypes
441 * @param {http.ClientRequest} req
442 */
443 static getResponseContentType(responseTypes, req) {
444 const acceptHeader = req.getHeader(Enum.Header.Accept);
445 return ContentNegotiation.accept(responseTypes, acceptHeader);
446 }
447
448
449 /**
450 * Returns a list of the most-preferred content encodings for the response.
451 * @param {string[]} responseEncodings
452 * @param {http.ClientRequest} req
453 */
454 static getResponseEncoding(responseEncodings, req) {
455 const acceptEncodingHeader = req.getHeader(Enum.Header.AcceptEncoding);
456 return ContentNegotiation.preferred(responseEncodings, acceptEncodingHeader);
457 }
458
459
460 /**
461 * Set the best content type for the response.
462 * Sets ctx.responseType, and Content-Type header.
463 * @param {string[]} responseTypes default first
464 * @param {http.ClientRequest} req
465 * @param {http.ServerResponse} res
466 * @param {object} ctx
467 */
468 setResponseType(responseTypes, req, res, ctx) {
469 const _scope = _fileScope('setResponseType');
470 ctx.responseType = Dingus.getResponseContentType(responseTypes, req);
471 if (!ctx.responseType) {
472 if (this.strictAccept) {
473 this.logger.debug(_scope, 'unhandled strict accept', { requestId: req.requestId });
474 throw new ResponseError(Enum.ErrorResponse.NotAcceptable);
475 } else {
476 ctx.responseType = responseTypes[0];
477 }
478 }
479 res.setHeader(Enum.Header.ContentType, ctx.responseType);
480 }
481
482
483 /**
484 * Inserts an encoding into Content-Encoding header.
485 * @param {http.ServerResponse} res
486 * @param {string} encoding
487 */
488 static addEncodingHeader(res, encoding) {
489 const existingEncodings = res.getHeader(Enum.Header.ContentEncoding);
490 if (existingEncodings) {
491 encoding = `${encoding}, ${existingEncodings}`;
492 }
493 res.setHeader(Enum.Header.ContentEncoding, encoding);
494 }
495
496
497 /**
498 * Attempt to fetch both data and metadata for a file.
499 * @param {string} filePath
500 */
501 async _readFileInfo(filePath) {
502 const _scope = _fileScope('_readFileInfo');
503 let result;
504 try {
505 // eslint-disable-next-line security/detect-non-literal-fs-filename
506 const stat = fsPromises.stat(filePath);
507 // eslint-disable-next-line security/detect-non-literal-fs-filename
508 const data = fsPromises.readFile(filePath);
509 result = await Promise.all([stat, data]);
510 } catch (e) {
511 if (['ENOENT', 'EACCES', 'EISDIR', 'ENAMETOOLONG', 'EINVAL'].includes(e.code)) {
512 return [null, null];
513 }
514 this.logger.error(_scope, 'fs error', { error: e, filePath });
515 throw e;
516 }
517 return result;
518 }
519
520
521 /**
522 * Potentially add additional headers from static file meta-file.
523 * @param {http.ServerResponse} res
524 * @param {string} directory
525 * @param {string} fileName - already normalized and filtered
526 */
527 async _serveFileMetaHeaders(res, directory, fileName) {
528 const _scope = _fileScope('_serveFileMetaHeaders');
529
530 let added = false;
531 const metaPrefix = '.';
532 const metaSuffix = '.meta';
533 const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`;
534 const metaFilePath = path.join(directory, metaFileName);
535
536 const [stat, data] = await this._readFileInfo(metaFilePath);
537 if (!stat) {
538 return added;
539 }
540
541 const lineBreakRE = /\r\n|\n|\r/;
542 const lines = data.toString().split(lineBreakRE);
543 common.unfoldHeaderLines(lines);
544
545 const headerParseRE = /^(?<name>[^:]+): +(?<value>.*)$/;
546 lines.forEach((line) => {
547 if (line) {
548 const result = headerParseRE.exec(line);
549 const { groups: header } = result;
550 res.setHeader(header.name, header.value);
551 added = true;
552 }
553 });
554 return added;
555 }
556
557
558 /**
559 * Serve a file from a directory, with rudimentary cache awareness.
560 * This will also serve pre-encoded variations if available and requested.
561 * @param {http.ClientRequest} req
562 * @param {http.ServerResponse} res
563 * @param {object} ctx
564 * @param {string} directory
565 * @param {string} fileName
566 */
567 async serveFile(req, res, ctx, directory, fileName) {
568 const _scope = _fileScope('serveFile');
569 this.logger.debug(_scope, 'called', { req, ctx });
570
571 // Require a directory field.
572 if (!directory) {
573 this.logger.debug(_scope, 'rejected unset directory', { fileName });
574 return this.handlerNotFound(req, res, ctx);
575 }
576
577 // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present.
578 fileName = path.normalize(fileName);
579
580 // We will not deal with any subdirs, nor any dot-files.
581 // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.)
582 if (fileName.indexOf(path.sep) >= 0
583 || fileName.startsWith('.')) {
584 this.logger.debug(_scope, 'rejected filename', { fileName });
585 return this.handlerNotFound(req, res, ctx);
586 }
587
588 const filePath = path.join(directory, fileName);
589
590 // File must exist, before any alternate static encodings will be considered.
591 let [stat, data] = await this._readFileInfo(filePath);
592 if (!stat) {
593 return this.handlerNotFound(req, res, ctx);
594 }
595
596 // If encodings were requested, check for static versions to serve.
597 // Update stat and data if matching version is found.
598 ctx.availableEncodings = Dingus.getResponseEncoding(Object.values(Enum.EncodingType), req);
599 if (ctx.availableEncodings.length === 0) {
600 // Identity encoding was specifically denied, and nothing else available.
601 this.logger.debug(_scope, 'no suitable encodings', { ctx });
602 return this.handlerMethodNotAllowed(req, res, ctx);
603 }
604 for (const encoding of ctx.availableEncodings) {
605 if (encoding === Enum.EncodingType.Identity) {
606 break;
607 }
608 const suffix = Enum.EncodingTypeSuffix[encoding];
609 if (!suffix) {
610 this.logger.error(_scope, 'supported encoding missing mapped suffix', { ctx, encoding });
611 continue;
612 }
613 const encodedFilePath = `${filePath}${suffix}`;
614 const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath);
615 if (encodedStat) {
616 ([ stat, data ] = [ encodedStat, encodedData ]);
617 ctx.selectedEncoding = encoding;
618 Dingus.addEncodingHeader(res, encoding);
619 res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding);
620 this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath });
621 }
622 break;
623 }
624
625 const lastModifiedDate = new Date(stat.mtimeMs);
626 res.setHeader(Enum.Header.LastModified, lastModifiedDate.toGMTString());
627
628 const eTag = common.generateETag(filePath, stat, data);
629 res.setHeader(Enum.Header.ETag, eTag);
630
631 if (common.isClientCached(req, stat.mtimeMs, eTag)) {
632 this.logger.debug(_scope, 'client cached file', { filePath });
633 res.statusCode = 304; // Not Modified
634 res.end();
635 return;
636 }
637
638 // Set the type based on extension of un-encoded filename.
639 const ext = path.extname(filePath).slice(1); // Drop the dot
640 const contentType = extensionToMime(ext);
641 res.setHeader(Enum.Header.ContentType, contentType);
642
643 // We presume static files are relatively cacheable.
644 res.setHeader(Enum.Header.CacheControl, 'public');
645
646 if (this.staticMetadata) {
647 ctx.metaHeaders = await this._serveFileMetaHeaders(res, directory, fileName);
648 }
649
650 this.logger.debug(_scope, 'serving file', { filePath, contentType });
651 res.end(data);
652 }
653
654
655 /**
656 * Return a content-type appropriate rendering of an errorResponse object.
657 * @param {string} type content-type of response
658 * @param {object} err either an Error object, or an error response
659 * @param {number} err.statusCode
660 * @param {string} err.errorMessage
661 * @param {string|string[]} err.details
662 */
663 // eslint-disable-next-line class-methods-use-this
664 renderError(contentType, err) {
665 switch (contentType) {
666 case Enum.ContentType.ApplicationJson:
667 return JSON.stringify(err);
668
669 case Enum.ContentType.TextHTML:
670 return Template.errorHTML(err);
671
672 case Enum.ContentType.TextPlain:
673 default:
674 return [err.errorMessage, err.details].join('\r\n');
675 }
676 }
677
678
679 /**
680 * Send an error response. Terminal.
681 * Logs any non-error-response errors as such.
682 * @param {object} err either an Error object, or an error response
683 * @param {http.ClientRequest} req
684 * @param {http.ServerResponse} res
685 * @param {object} ctx
686 */
687 sendErrorResponse(err, req, res, ctx) {
688 const _scope = _fileScope('sendErrorResponse');
689 let body;
690
691 // Default to a content type if one is not yet present
692 if (!res.hasHeader(Enum.Header.ContentType)) {
693 res.setHeader(Enum.Header.ContentType, Enum.ContentType.TextPlain);
694 }
695
696 if (err?.statusCode) {
697 res.statusCode = err.statusCode;
698 body = this.renderError(res.getHeader(Enum.Header.ContentType), err);
699 this.logger.debug(_scope, 'handler error', { err, req, res, ctx });
700 } else {
701 res.statusCode = 500;
702 body = this.renderError(res.getHeader(Enum.Header.ContentType), Enum.ErrorResponse.InternalServerError);
703 this.logger.error(_scope, 'handler exception', { err, req, res, ctx });
704 }
705 res.end(body);
706 }
707
708
709 /**
710 * @param {http.ClientRequest} req
711 * @param {http.ServerResponse} res
712 * @param {object} ctx
713 * @param {String} file - override ctx.params.file
714 */
715 async handlerGetStaticFile(req, res, ctx, file) {
716 Dingus.setHeadHandler(req, res, ctx);
717
718 // Set a default response type to handle any errors; will be re-set to serve actual static content type.
719 this.setResponseType(this.responseTypes, req, res, ctx);
720
721 await this.serveFile(req, res, ctx, this.staticPath, file || ctx.params.file);
722 }
723
724
725 /**
726 * @param {http.ClientRequest} req
727 * @param {http.ServerResponse} res
728 * @param {Object} ctx
729 * @param {String} newPath
730 * @param {Number} statusCode
731 */
732 async handlerRedirect(req, res, ctx, newPath, statusCode = 307) {
733 this.setResponseType(this.responseTypes, req, res, ctx);
734 res.setHeader(Enum.Header.Location, newPath);
735 res.statusCode = statusCode;
736 res.end();
737 }
738
739
740 /**
741 * @param {http.ClientRequest} req
742 * @param {http.ServerResponse} res
743 * @param {object} ctx
744 */
745 async handlerMethodNotAllowed(req, res, ctx) {
746 this.setResponseType(this.responseTypes, req, res, ctx);
747 throw new ResponseError(Enum.ErrorResponse.MethodNotAllowed);
748 }
749
750
751 /**
752 * @param {http.ClientRequest} req
753 * @param {http.ServerResponse} res
754 * @param {object} ctx
755 */
756 async handlerNotFound(req, res, ctx) {
757 this.setResponseType(this.responseTypes, req, res, ctx);
758 throw new ResponseError(Enum.ErrorResponse.NotFound);
759 }
760
761
762 /**
763 * @param {http.ClientRequest} req
764 * @param {http.ServerResponse} res
765 * @param {object} ctx
766 */
767 async handlerBadRequest(req, res, ctx) {
768 this.setResponseType(this.responseTypes, req, res, ctx);
769 throw new ResponseError(Enum.ErrorResponse.BadRequest);
770 }
771
772
773 /**
774 * @param {http.ClientRequest} req
775 * @param {http.ServerResponse} res
776 * @param {object} ctx
777 */
778 async handlerInternalServerError(req, res, ctx) {
779 this.setResponseType(this.responseTypes, req, res, ctx);
780 throw new ResponseError(Enum.ErrorResponse.InternalServerError);
781 }
782
783 }
784
785 module.exports = Dingus;