UNPKG

@teclone/r-server

Version:

A lightweight, extensible web-server with inbuilt routing-engine, static file server, file upload handler, request body parser, middleware support and lots more

431 lines (402 loc) 16.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _classCallCheck = require('@babel/runtime/helpers/classCallCheck'); var _createClass = require('@babel/runtime/helpers/createClass'); var _defineProperty = require('@babel/runtime/helpers/defineProperty'); var crypto = require('crypto'); var fs = require('fs'); var path = require('path'); var utils = require('@teclone/utils'); var mime = require('mime-types'); var Constants = require('./Constants'); var Utils = require('./Utils'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var _classCallCheck__default = /*#__PURE__*/_interopDefaultLegacy(_classCallCheck); var _createClass__default = /*#__PURE__*/_interopDefaultLegacy(_createClass); var _defineProperty__default = /*#__PURE__*/_interopDefaultLegacy(_defineProperty); var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto); var fs__namespace = /*#__PURE__*/_interopNamespace(fs); var path__namespace = /*#__PURE__*/_interopNamespace(path); var mime__default = /*#__PURE__*/_interopDefaultLegacy(mime); function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } var FileServer = /*#__PURE__*/function () { function FileServer(rootDir, config) { _classCallCheck__default["default"](this, FileServer); _defineProperty__default["default"](this, "config", void 0); _defineProperty__default["default"](this, "rootDir", void 0); this.rootDir = rootDir; this.config = config; } /** * streams a file to client */ _createClass__default["default"](FileServer, [{ key: "streamFile", value: function streamFile(filePath, response, status, headers, options) { var readStream = fs__namespace.createReadStream(filePath, options); response.status(status).setHeaders(headers); return new Promise(function (resolve, reject) { readStream.on('error', function (err) { return reject(err); }); readStream.on('end', function () { return resolve(true); }); readStream.pipe(response, { end: false }); }).then(function () { return response.end(); })["catch"](function (err) { readStream.close(); return Utils.handleError(err, response); }); } /** * validates range request content. * @see https://tools.ietf.org/html/rfc7233 */ }, { key: "validateRangeRequest", value: function validateRangeRequest(headers, eTag, lastModified, fileSize) { var result = { isMultiRange: false, statusCode: 206, ranges: [] }; var ifRange = headers['if-range']; //check if we should send everything if (ifRange && ifRange !== eTag) { result.statusCode = 200; result.ranges.push({ start: 0, end: fileSize - 1, length: fileSize }); return result; } //we are not sending everything var ranges = headers['range'].replace(/^\s*[^=]*=\s*/, '').split(/,\s*/); result.isMultiRange = ranges.length > 0; var _iterator = _createForOfIteratorHelper(ranges), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var range = _step.value; var start = 0; var end = fileSize - 1; //suffix-byte-range-spec if (/^-(\d+)$/.test(range)) { var suffixLength = Number.parseInt(RegExp.$1); if (suffixLength === 0) { continue; } else if (suffixLength < fileSize) { start = fileSize - suffixLength; } } //byte-range-spec else if (/^(\d+)-(\d+)?$/.test(range)) { var first = Number.parseInt(RegExp.$1); var last = RegExp.$2 ? Number.parseInt(RegExp.$2) : end; if (first >= fileSize || last < first) { continue; } else { start = first; if (last < end) { end = last; } } } else { continue; } result.ranges.push({ start: start, end: end, length: end - start + 1 }); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return result; } /** * check if file is modified */ }, { key: "doesClientNeedContent", value: function doesClientNeedContent(headers, contentTag, contentLastModified) { if (!contentTag && !contentLastModified) { return true; } if (!headers['if-none-match'] && !headers['if-match'] && !headers['if-modified-since'] && !headers['if-unmodified-since']) { return true; } // check if match conditions if (headers['if-match']) { return contentTag === headers['if-match']; } if (headers['if-none-match']) { return contentTag !== headers['if-none-match']; } if (headers['if-modified-since']) { return !contentLastModified || contentLastModified > new Date(headers['if-modified-since']); } if (headers['if-unmodified-since']) { return !contentLastModified || contentLastModified <= new Date(headers['if-unmodified-since']); } return true; } /** * computes and returns a files eTag */ }, { key: "computeETag", value: function computeETag(fileMTime) { var length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 16; var hash = crypto__namespace.createHash('sha256'); hash.update(fileMTime.toString()); return hash.digest('hex').substring(0, length); } /** * returns default response headers */ }, { key: "getFileDefaultHeaders", value: function getFileDefaultHeaders(filePath) { var stat = fs__namespace.statSync(filePath); var eTag = this.computeETag(stat.mtime); var lastModified = stat.mtime; // it is important to remove milliseconds // to match date time format of last-modified header lastModified.setUTCMilliseconds(0); return { lastModified: lastModified, eTag: eTag, fileSize: stat.size, headers: { 'Content-Type': mime__default["default"].lookup(path__namespace.parse(filePath).ext.substring(1)) || 'application/octet-stream', 'Last-Modified': lastModified.toUTCString(), 'Content-Length': stat.size.toString(), ETag: eTag, 'Cache-Control': this.config.cacheControl } }; } /** * process file */ }, { key: "process", value: function process(filePath, requestMethod, requestHeaders, response) { requestMethod = requestMethod.toLowerCase(); if (requestMethod === 'options') { return response.status(200).setHeaders({ Allow: Constants.ALLOWED_METHODS.join(',') }).end(); } var _this$getFileDefaultH = this.getFileDefaultHeaders(filePath), resHeaders = _this$getFileDefaultH.headers, eTag = _this$getFileDefaultH.eTag, lastModified = _this$getFileDefaultH.lastModified, fileSize = _this$getFileDefaultH.fileSize; if (requestMethod === 'head') { resHeaders['Accept-Ranges'] = 'bytes'; return response.status(200).setHeaders(resHeaders).end(); } //if it is not a range request, negotiate content if (utils.isUndefined(requestHeaders['range'])) { if (!this.doesClientNeedContent(requestHeaders, eTag, lastModified)) { return response.status(304).end(); } else { return this.streamFile(filePath, response, 200, resHeaders); } } else { //we are dealing with range request var _this$validateRangeRe = this.validateRangeRequest(requestHeaders, eTag, lastModified, fileSize), statusCode = _this$validateRangeRe.statusCode, ranges = _this$validateRangeRe.ranges; //we are sending everything if (statusCode === 200) { return this.streamFile(filePath, response, 200, resHeaders); } delete resHeaders['Content-Disposition']; //range request is not satisfiable if (ranges.length === 0) { return response.status(416).setHeaders({ 'Content-Range': "bytes */".concat(fileSize) }).end(); } //single range request if (ranges.length === 1) { var _ranges$ = ranges[0], start = _ranges$.start, end = _ranges$.end, length = _ranges$.length; resHeaders['Content-Length'] = length.toString(); resHeaders['Content-Range'] = "bytes ".concat(start, "-").concat(end, "/").concat(fileSize); return this.streamFile(filePath, response, 206, resHeaders, { start: start, end: end }); } //multi range request. for now, we dont support multipart range. send everything resHeaders['Content-Length'] = fileSize.toString(); resHeaders['Content-Range'] = "bytes ".concat(0, "-", fileSize - 1, "/").concat(fileSize); return this.streamFile(filePath, response, 206, resHeaders); } } /** * returns the directory's default document if any */ }, { key: "getDefaultDocument", value: function getDefaultDocument(dir) { var _iterator2 = _createForOfIteratorHelper(this.config.defaultDocuments), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var file = _step2.value; if (fs__namespace.existsSync(path__namespace.join(dir, file))) { return file; } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } return null; } /** * validates the request method and returns the public file or directory path that * matches the request url */ }, { key: "validateRequest", value: function validateRequest(url, method) { if (!['head', 'get', 'options'].includes(method.toLowerCase())) { return null; } url = url.replace(/[#?].*/, '').replace(/\.\./g, ''); var _iterator3 = _createForOfIteratorHelper(this.config.publicPaths), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var publicPath = _step3.value; var testPath = path__namespace.resolve(this.rootDir, publicPath, url); if (fs__namespace.existsSync(testPath)) { var stat = fs__namespace.statSync(testPath); if (stat.isFile()) { return testPath; } else if (stat.isDirectory()) { //check if there is a default document in the folder var defaultDocument = this.getDefaultDocument(testPath); if (defaultDocument) { return path__namespace.join(testPath, defaultDocument); } } } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } return null; } /** * serves a static file response back to the client */ }, { key: "serve", value: function serve(requestPath, requestMethod, requestHeaders, response) { requestMethod = requestMethod.toLowerCase(); var filePath = this.validateRequest(utils.stripSlashes(requestPath), requestMethod); if (filePath) { return this.process(filePath, requestMethod, requestHeaders, response); } return Promise.resolve(false); } /** * serves server http error files. such as 504, 404, etc */ }, { key: "serveHttpErrorFile", value: function serveHttpErrorFile(errorStatusCode, response) { var httpErrors = this.config.httpErrors; var filePath = ''; var errorStatusCodeStr = errorStatusCode.toString(); if (httpErrors[errorStatusCodeStr]) { filePath = path.resolve(this.rootDir, httpErrors.baseDir, httpErrors[errorStatusCode]); } else { filePath = path.resolve(__dirname, "../httpErrors/".concat(errorStatusCode, ".html")); } if (!fs__namespace.existsSync(filePath) || fs__namespace.statSync(filePath).isDirectory()) { return response.status(errorStatusCode).end(); } else { return this.streamFile(filePath, response, errorStatusCode, this.getFileDefaultHeaders(filePath).headers); } } /** * serves file intended for download to the client */ }, { key: "serveDownload", value: function serveDownload(filePath, response, filename) { var found = true; var absPath = path.resolve(this.rootDir, filePath); if (!fs__namespace.existsSync(absPath) || fs__namespace.statSync(absPath).isDirectory()) { found = false; var _iterator4 = _createForOfIteratorHelper(this.config.publicPaths), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var current = _step4.value; absPath = path.resolve(this.rootDir, current, filePath); if (fs__namespace.existsSync(absPath) && fs__namespace.statSync(absPath).isFile()) { found = true; break; } } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } } if (!found) { return Promise.reject(new Error("Could not locate download file at ".concat(filePath))); } else { filename = utils.isString(filename) ? filename : path__namespace.parse(absPath).base; return this.streamFile(absPath, response, 200, { 'Content-Disposition': "attachment; filename=\"".concat(filename, "\"") }); } } }]); return FileServer; }(); exports.FileServer = FileServer; //# sourceMappingURL=FileServer.js.map