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

825 lines (771 loc) 30.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _asyncToGenerator = require('@babel/runtime/helpers/asyncToGenerator'); var _classCallCheck = require('@babel/runtime/helpers/classCallCheck'); var _createClass = require('@babel/runtime/helpers/createClass'); var _defineProperty = require('@babel/runtime/helpers/defineProperty'); var _regeneratorRuntime = require('@babel/runtime/regenerator'); var _server_config = require('../.server.config'); var BodyParser = require('./BodyParser'); var Engine = require('./Engine'); var FileServer = require('./FileServer'); var Logger = require('./Logger'); var Router = require('./Router'); var fs = require('fs'); var http2 = require('http2'); var https = require('https'); var http = require('http'); var path = require('path'); var utils = require('@teclone/utils'); var Wrapper = require('./Wrapper'); var EntityTooLargeException = require('../Exceptions/EntityTooLargeException'); var dotenv = require('dotenv'); var Utils = require('./Utils'); var Response = require('./Response'); var Request = require('./Request'); var Constants = require('./Constants'); 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 _asyncToGenerator__default = /*#__PURE__*/_interopDefaultLegacy(_asyncToGenerator); var _classCallCheck__default = /*#__PURE__*/_interopDefaultLegacy(_classCallCheck); var _createClass__default = /*#__PURE__*/_interopDefaultLegacy(_createClass); var _defineProperty__default = /*#__PURE__*/_interopDefaultLegacy(_defineProperty); var _regeneratorRuntime__default = /*#__PURE__*/_interopDefaultLegacy(_regeneratorRuntime); var fs__namespace = /*#__PURE__*/_interopNamespace(fs); 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; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty__default["default"](target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } var Server = /*#__PURE__*/function () { function Server(options) { _classCallCheck__default["default"](this, Server); _defineProperty__default["default"](this, "server", null); _defineProperty__default["default"](this, "secureServer", null); _defineProperty__default["default"](this, "entryPath", ''); _defineProperty__default["default"](this, "config", {}); _defineProperty__default["default"](this, "router", void 0); _defineProperty__default["default"](this, "mountedRouters", []); _defineProperty__default["default"](this, "logger", void 0); _defineProperty__default["default"](this, "bodyParser", void 0); _defineProperty__default["default"](this, "fileServer", void 0); _defineProperty__default["default"](this, "errorCallback", null); _defineProperty__default["default"](this, "env", void 0); var _ref = options || {}, configFile = _ref.configFile, _ref$config = _ref.config, config = _ref$config === void 0 ? {} : _ref$config; this.entryPath = process.cwd(); this.config = this.resolveConfig(this.entryPath, configFile || Constants.DEFAULT_CONFIG_FILE, config); this.env = process.env.NODE_ENV || 'development'; this.loadEnv(this.entryPath, this.env); this.logger = new Logger.Logger({ accessLogFile: this.config.accessLog, errorLogFile: this.config.errorLog }); this.bodyParser = new BodyParser.BodyParser(); this.router = new Router.Router({ inheritMiddlewares: false, basePath: options === null || options === void 0 ? void 0 : options.basePath }); this.fileServer = new FileServer.FileServer(this.entryPath, this.config); this.createServers(options); } /** * resolves and merges the configuration objects */ _createClass__default["default"](Server, [{ key: "resolveConfig", value: function resolveConfig(entryPath, configFile, config) { var configFromFile; try { var absPath = path.resolve(entryPath, configFile); configFromFile = require(absPath); } catch (ex) { console.log("Failed to load server configuration at ".concat(configFile, ". proceeding without it")); } var resolvedConfig = utils.copy({}, _server_config.rServerConfig, configFromFile, config); // resolve to numeric value resolvedConfig.maxMemory = utils.expandToNumeric(resolvedConfig.maxMemory); var directoriesToResolve = ['accessLog', 'errorLog', 'tempDir']; directoriesToResolve.forEach(function (key) { resolvedConfig[key] = path.resolve(entryPath, resolvedConfig[key]); }); return resolvedConfig; } }, { key: "createServers", value: function createServers(opts) { var _this = this; var https$1 = this.config.https; // create a non secure server if https is not enabled or // if we should redirect http to https if (!https$1.enabled || https$1.enforce) { this.server = http.createServer({ IncomingMessage: (opts === null || opts === void 0 ? void 0 : opts.Http1ServerRequest) || Request.Http1Request, // @ts-ignore ServerResponse: (opts === null || opts === void 0 ? void 0 : opts.Http1ServerResponse) || Response.Http1Response }, utils.scopeCallback(this.onRequest, this, false)); } // create secure server if enabled if (https$1.enabled) { var credentials = Object.keys(https$1.credentials).reduce(function (result, key) { var value = https$1.credentials[key]; result[key] = key === 'passphrase' ? value : fs__namespace.readFileSync(path.resolve(_this.entryPath, value)); return result; }, {}); switch (https$1.version) { case '1': // @ts-ignore this.secureServer = https.createServer(_objectSpread(_objectSpread({}, credentials), {}, { IncomingMessage: (opts === null || opts === void 0 ? void 0 : opts.Http1ServerRequest) || Request.Http1Request, // @ts-ignore ServerResponse: (opts === null || opts === void 0 ? void 0 : opts.Http1ServerResponse) || Response.Http1Response }), utils.scopeCallback(this.onRequest, this, true)); break; default: this.secureServer = http2.createSecureServer(_objectSpread(_objectSpread({}, credentials), {}, { Http1IncomingMessage: (opts === null || opts === void 0 ? void 0 : opts.Http1ServerRequest) || Request.Http1Request, // @ts-ignore Http1ServerResponse: (opts === null || opts === void 0 ? void 0 : opts.Http1ServerResponse) || Response.Http1Response, Http2ServerRequest: (opts === null || opts === void 0 ? void 0 : opts.Http2ServerRequest) || Request.Http2Request, // @ts-ignore Http2ServerResponse: (opts === null || opts === void 0 ? void 0 : opts.Http2ServerResponse) || Response.Http2Response, allowHTTP1: true }), utils.scopeCallback(this.onRequest, this, true)); } } } /** * server root directory */ }, { key: "rootDir", get: function get() { return this.entryPath; } /** * load env settings */ }, { key: "loadEnv", value: function loadEnv(entryPath, env) { try { dotenv.config(); dotenv.config({ path: path.resolve(entryPath, ".".concat(env)) }); } catch (ex) { // do nothing } } /** * returns server intro */ }, { key: "getServerIntro", value: function getServerIntro(server, isSecureServer) { var result = { name: isSecureServer ? 'https' : 'http', address: '' }; if (server.listening) { var _ref2 = server.address(), address = _ref2.address, port = _ref2.port; /* istanbul ignore else */ if (address === '::') { result.address = "".concat(result.name.toLowerCase(), "://localhost:").concat(port, "/"); } else { result.address = "".concat(result.name.toLowerCase(), "://").concat(address, ":").concat(port, "/"); } } return result; } /** * cordinates how routes are executed, including mounted routes */ }, { key: "cordinateRoutes", value: function () { var _cordinateRoutes = _asyncToGenerator__default["default"]( /*#__PURE__*/_regeneratorRuntime__default["default"].mark(function _callee(path, method, request, response) { var engine, _iterator, _step, mountedRouter, middlewareInstances; return _regeneratorRuntime__default["default"].wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: //create the engine engine = new Engine.Engine(path, method, request, response); // process main route _context.next = 3; return engine.process(this.router.getRoutes()[method], this.router.getMiddlewares()); case 3: if (!_context.sent) { _context.next = 5; break; } return _context.abrupt("return", true); case 5: // process mounted routes; _iterator = _createForOfIteratorHelper(this.mountedRouters); _context.prev = 6; _iterator.s(); case 8: if ((_step = _iterator.n()).done) { _context.next = 17; break; } mountedRouter = _step.value; middlewareInstances = mountedRouter.shouldInheritMiddlewares() ? this.router.getMiddlewares().concat(mountedRouter.getMiddlewares()) : mountedRouter.getMiddlewares(); /* istanbul ignore else */ _context.next = 13; return engine.process(mountedRouter.getRoutes()[method], middlewareInstances); case 13: if (!_context.sent) { _context.next = 15; break; } return _context.abrupt("return", true); case 15: _context.next = 8; break; case 17: _context.next = 22; break; case 19: _context.prev = 19; _context.t0 = _context["catch"](6); _iterator.e(_context.t0); case 22: _context.prev = 22; _iterator.f(); return _context.finish(22); case 25: return _context.abrupt("return", false); case 26: case "end": return _context.stop(); } }, _callee, this, [[6, 19, 22, 25]]); })); function cordinateRoutes(_x, _x2, _x3, _x4) { return _cordinateRoutes.apply(this, arguments); } return cordinateRoutes; }() /** * perform house keeping */ }, { key: "onResponseFinish", value: function onResponseFinish(request, response) { response.endedAt = new Date(); this.logger.profile(request, response); } /** * parse all request data */ }, { key: "parseRequestData", value: function parseRequestData(request) { //parse query request.query = this.bodyParser.parseQueryString(request.url); //parse the request body if (request.buffer.length > 0) { var contentType = request.headers['content-type'] || 'text/plain'; var data = this.bodyParser.parse(request.buffer, contentType); request.data = _objectSpread(_objectSpread({}, request.query), data); } else { request.data = _objectSpread({}, request.query); } } /** * handle onrequest end event */ }, { key: "onRequestEnd", value: function () { var _onRequestEnd = _asyncToGenerator__default["default"]( /*#__PURE__*/_regeneratorRuntime__default["default"].mark(function _callee2(request, response) { var pathname, method, routeFound, fileServed; return _regeneratorRuntime__default["default"].wrap(function _callee2$(_context2) { while (1) switch (_context2.prev = _context2.next) { case 0: if (!request.error) { _context2.next = 2; break; } return _context2.abrupt("return"); case 2: request.endedAt = response.startedAt = new Date(); pathname = request.parsedUrl.pathname; _context2.prev = 4; _context2.next = 7; return Utils.deCompressBuffer(request.buffer, request.headers['content-encoding']); case 7: request.buffer = _context2.sent; this.parseRequestData(request); method = request.method; _context2.next = 12; return this.cordinateRoutes(pathname, method, request, response); case 12: routeFound = _context2.sent; if (!routeFound) { _context2.next = 15; break; } return _context2.abrupt("return", true); case 15: _context2.next = 17; return this.fileServer.serve(pathname, method, request.headers, response); case 17: fileServed = _context2.sent; if (!fileServed) { _context2.next = 20; break; } return _context2.abrupt("return", true); case 20: _context2.next = 22; return this.fileServer.serveHttpErrorFile(404, response); case 22: _context2.next = 27; break; case 24: _context2.prev = 24; _context2.t0 = _context2["catch"](4); Utils.handleError(_context2.t0, response); case 27: case "end": return _context2.stop(); } }, _callee2, this, [[4, 24]]); })); function onRequestEnd(_x5, _x6) { return _onRequestEnd.apply(this, arguments); } return onRequestEnd; }() /** * handle request data event */ }, { key: "onRequestData", value: function onRequestData(chunk, request) { if (request.entityTooLarge) { return; } var newEntityLength = request.buffer.length + chunk.length; var maxMemory = this.config.maxMemory; if (maxMemory && newEntityLength >= maxMemory) { request.entityTooLarge = true; request.emit('error', new EntityTooLargeException.EntityTooLargeException()); return; } request.buffer = Buffer.concat([request.buffer, chunk]); } /** * handles onrequest events */ }, { key: "onRequest", value: function onRequest(request, response, isSecureServer) { request.init(isSecureServer); response.fileServer = this.fileServer; response.logger = this.logger; response.req = request; response.errorCallback = this.errorCallback; //enforce https if set var httpsConfig = this.config.https; if (httpsConfig.enabled && !request.encrypted && httpsConfig.enforce) { response.redirect("https://".concat(request.parsedUrl.hostname + ':' + this.address().https.port + request.parsedUrl.pathname + request.parsedUrl.search)); return; } //handle on request error request.on('error', function (err) { request.error = true; Utils.handleError(err, response, 400); }); //handle on data event request.on('data', utils.scopeCallback(this.onRequestData, this, [request, response])); //handle on end event request.on('end', utils.scopeCallback(this.onRequestEnd, this, [request, response])); //clean up resources once the response has been sent out response.on('finish', utils.scopeCallback(this.onResponseFinish, this, [request, response])); } /** * binds all event handlers on the server */ }, { key: "closeServer", value: function closeServer(server, resolve, reject) { if (server && server.listening) { server.close(function (err) { if (err) { reject(err); } else { resolve(true); } }); } else { resolve(true); } } /** * binds all event handlers on the server */ }, { key: "initServer", value: function initServer(server, isSecureServer, resolve, reject) { var _this2 = this; if (!server) { return resolve(); } server //handle error event .on('error', function (err) { var intro = _this2.getServerIntro(server, isSecureServer); _this2.logger.warn("".concat(intro.name, " Server Error: ").concat(err.code, " ").concat(err.message)); server.close(function () { return reject(err); }); }) // handle client error .on('clientError', function (err, socket) { if (err && err.code === 'ECONNRESET' || !socket || !socket.writable) { return; } socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); }) //handle server listening event .on('listening', function () { var intro = _this2.getServerIntro(server, isSecureServer); _this2.logger.info("".concat(intro.name, " server started at ").concat(intro.address)); resolve(true); }) // handle close event .on('close', function () { var intro = _this2.getServerIntro(server, isSecureServer); _this2.logger.info("".concat(intro.name, " connection closed successfully")); }); } /** * returns boolean indicating if the server is listening */ }, { key: "listening", get: function get() { var _this$server, _this$secureServer; return Boolean(((_this$server = this.server) === null || _this$server === void 0 ? void 0 : _this$server.listening) || ((_this$secureServer = this.secureServer) === null || _this$secureServer === void 0 ? void 0 : _this$secureServer.listening)); } /** * returns the server instance router */ }, { key: "getRouter", value: function getRouter() { return this.router; } /** * returns the server instance mounted routers */ }, { key: "getMountedRouters", value: function getMountedRouters() { return this.mountedRouters; } /** * returns the resolved server config object */ }, { key: "getConfig", value: function getConfig() { return this.config; } /** * sets routing base path that gets prepended to all route and middleware urls */ }, { key: "setBasePath", value: function setBasePath(basePath) { this.router.setBasePath(basePath); } /** * sets the app intance callback error handler * @param errorCallback app instance error callback */ }, { key: "setErrorCallback", value: function setErrorCallback(errorCallback) { this.errorCallback = errorCallback; } /** * stores route rules for http OPTIONS method * * @param url - route url * @param callback - route callback handler * @param options - route configuration object or middleware or array of middlewares */ }, { key: "options", value: function options(path, callback, use) { return this.router.options(path, callback, use); } /** * stores route rules for http HEAD method * * @param url - route url * @param callback - route callback handler * @param options - route configuration object or middleware or array of middlewares */ }, { key: "head", value: function head(path, callback, use) { return this.router.head(path, callback, use); } /** * stores route rules for http GET method * * @param url - route url * @param callback - route callback handler * @param options - route configuration object or middleware or array of middlewares */ }, { key: "get", value: function get(path, callback, use) { return this.router.get(path, callback, use); } /** * stores route rules for http POST method * * @param url - route url * @param callback - route callback handler * @param use - route configuration object or middleware or array of middlewares */ }, { key: "post", value: function post(path, callback, use) { return this.router.post(path, callback, use); } /** * stores route rules for http PUT method * @param url - route url * @param callback - route callback handler * @param options - route configuration object or middleware or array of middlewares */ }, { key: "put", value: function put(path, callback, use) { return this.router.put(path, callback, use); } /** * stores route rules for http DELETE method * * @param url - route url * @param callback - route callback handler * @param options - route configuration object or middleware or array of middlewares */ }, { key: "delete", value: function _delete(path, callback, use) { return this.router["delete"](path, callback, use); } /** * stores route rules for all http methods * * @param url - route url * @param callback - route callback handler * @param options - route configuration object or middleware or array of middlewares */ }, { key: "any", value: function any(path, callback, use) { this.router.any(path, callback, use); } /** * returns a route wrapper for the given url */ }, { key: "route", value: function route(path) { return new Wrapper.Wrapper(this.router, path); } /** * removes a given route * @param id route id */ }, { key: "removeRoute", value: function removeRoute(id) { if (!this.router.removeRoute(id)) { var _iterator2 = _createForOfIteratorHelper(this.mountedRouters), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var mountedRouter = _step2.value; if (mountedRouter.removeRoute(id)) { return true; } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } return false; } else { return true; } } /** * removes a given middleware * @param id middleware id */ }, { key: "removeMiddleware", value: function removeMiddleware(id) { if (!this.router.removeMiddleware(id)) { var _iterator3 = _createForOfIteratorHelper(this.mountedRouters), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var mountedRouter = _step3.value; if (mountedRouter.removeMiddleware(id)) { return true; } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } return false; } else { return true; } } /** * registers a middleware to be called whenever the given url is visited * * @param url - url to apply middleware to. use * to appy to all urls * @param middleware - the middleware or array of middlewares * @param options - middleware configuration option. here, you can specify the http method * that the middleware will run against */ }, { key: "use", value: function use(path, middleware, operation) { return this.router.use(path, middleware, operation); } /** * mounts a router to the server instance */ }, { key: "mount", value: function mount(basePath, router) { var mainRouterBasePath = this.router.getBasePath(); var resolve = function resolve(instance) { instance[1] = path.join(mainRouterBasePath, basePath, instance[1]); }; //resolve all routes, each apiRoutes is of the form [url, callback, options] var routes = router.getRoutes(); for (var _i = 0, _Object$keys = Object.keys(routes); _i < _Object$keys.length; _i++) { var api = _Object$keys[_i]; var apiRoutes = routes[api]; apiRoutes.forEach(resolve, this); } //resolve all middlewares. each middleware is of the format [url, callback, options] var middlewares = router.getMiddlewares(); middlewares.forEach(resolve); this.mountedRouters.push(router); } /** * starts the server at a given port */ }, { key: "listen", value: function listen(port) { var _this3 = this; if (this.listening) { this.logger.warn('Server already started. You must close the server first'); return Promise.resolve(true); } var resolvedPortConfig = {}; if (utils.isObject(port)) { resolvedPortConfig = port; } else if (port) { resolvedPortConfig = { httpPort: port }; } var _resolvedPortConfig = resolvedPortConfig, _resolvedPortConfig$h = _resolvedPortConfig.httpPort, httpPort = _resolvedPortConfig$h === void 0 ? this.config.port : _resolvedPortConfig$h, _resolvedPortConfig$h2 = _resolvedPortConfig.httpsPort, httpsPort = _resolvedPortConfig$h2 === void 0 ? this.config.https.port : _resolvedPortConfig$h2; return Promise.all([this.server ? new Promise(function (resolve, reject) { _this3.initServer(_this3.server, false, resolve, reject); _this3.server.listen(httpPort || 8000); }) : Promise.resolve(null), this.secureServer ? new Promise(function (resolve, reject) { _this3.initServer(_this3.secureServer, true, resolve, reject); _this3.secureServer.listen(httpsPort || 9000); }) : Promise.resolve(null)]).then(function () { return true; }); } /** * closes server * @param callback callback function to execute when connection closes */ }, { key: "close", value: function close() { var _this4 = this; if (!this.listening) { return Promise.resolve(true); } return Promise.all([new Promise(function (resolve, reject) { _this4.closeServer(_this4.server, resolve, reject); }), new Promise(function (resolve, reject) { _this4.closeServer(_this4.secureServer, resolve, reject); })]).then(function () { return true; }); } /** * returns http and https server address */ }, { key: "address", value: function address() { var _this$server2, _this$secureServer2; var result = { http: null, https: null }; if ((_this$server2 = this.server) !== null && _this$server2 !== void 0 && _this$server2.listening) { result.http = this.server.address(); } if ((_this$secureServer2 = this.secureServer) !== null && _this$secureServer2 !== void 0 && _this$secureServer2.listening) { result.https = this.secureServer.address(); } return result; } }]); return Server; }(); exports.Server = Server; //# sourceMappingURL=Server.js.map