UNPKG

web-dev-server

Version:

Node.js simple http server for common development or training purposes.

708 lines 30.6 kB
Object.defineProperty(exports, "__esModule", { value: true }); exports.Server = exports.Session = void 0; var tslib_1 = require("tslib"); var http_1 = require("http"); var fs_1 = require("fs"); var path_1 = require("path"); var url_1 = require("url"); var StringHelper_1 = require("./Tools/Helpers/StringHelper"); var Defaults_1 = require("./Handlers/Defaults"); var Register_1 = require("./Applications/Register"); var Error_1 = require("./Handlers/Error"); var File_1 = require("./Handlers/File"); var Directory_1 = require("./Handlers/Directory"); var Event_1 = require("./Event"); var Request_1 = require("./Request"); var Response_1 = require("./Response"); tslib_1.__exportStar(require("./Request"), exports); tslib_1.__exportStar(require("./Response"), exports); tslib_1.__exportStar(require("./Event"), exports); tslib_1.__exportStar(require("./Tools/Namespace"), exports); tslib_1.__exportStar(require("./Applications/IApplication"), exports); var Session_1 = require("./Applications/Session"); var Session = /** @class */ (function (_super) { tslib_1.__extends(Session, _super); function Session() { return _super !== null && _super.apply(this, arguments) || this; } return Session; }(Session_1.Session)); exports.Session = Session; ; (function (Session) { ; })(Session = exports.Session || (exports.Session = {})); exports.Session = Session; var Server = /** @class */ (function () { function Server() { this.state = 0; this.documentRoot = null; this.basePath = null; this.port = null; this.hostName = null; this.development = true; this.indexes = { scripts: ['index.js'], files: ['index.html', 'index.htm', 'default.html', 'default.htm'] }; this.httpServer = null; this.netSockets = null; this.customServerHandler = null; this.register = null; this.errorsHandler = null; this.filesHandler = null; this.directoriesHandler = null; this.customErrorHandler = null; this.customHttpPreHandlers = []; this.forbiddenPaths = [ /^\/node_modules/g, /\/package(-lock)?\.json/g, /\/tsconfig\.json/g, /\/\.([^\.]+)/g ]; } /** * @summary Create new server instance (no singleton implementation). */ Server.CreateNew = function () { return new Server(); }; /** * @summary Set development mode, `true` by default. If `true`, directories contents and errors are displayed, `false` otherwise. * @param development If `true`, directories contents and errors are displayed, `false` otherwise. */ Server.prototype.SetDevelopment = function (development) { this.development = development; return this; }; /** * @summary Set http server IP or domain to listening on, `127.0.0.1` by default. * @param hostname Server ip or domain to listening on. */ Server.prototype.SetHostname = function (hostname) { this.hostName = hostname; return this; }; /** * @summary Set http server port number, `8000` by default. * @param port Server port to listening on. */ Server.prototype.SetPort = function (port) { this.port = port; return this; }; /** * @summary Set http server root directory, required * @param dirname Server root directory as absolute path. */ Server.prototype.SetDocumentRoot = function (dirname) { this.documentRoot = StringHelper_1.StringHelper.TrimRight(path_1.resolve(dirname).replace(/\\/g, '/'), '/'); return this; }; /** * @summary Set http server base path, not required * @param basePath Base path (proxy path, if you are running the server under proxy). */ Server.prototype.SetBasePath = function (basePath) { this.basePath = StringHelper_1.StringHelper.TrimRight(basePath.replace(/\\/g, '/'), '/'); return this; }; /** * @summary Set custom http server handler like express module. * @see https://stackoverflow.com/a/17697134/7032987 * @param httpHandler */ Server.prototype.SetServerHandler = function (httpHandler) { this.customServerHandler = httpHandler; return this; }; /** * @summary Set custom error handler for uncatched errors and warnings * @param errorHandler Custom handler called on any uncatched error. */ Server.prototype.SetErrorHandler = function (errorHandler) { this.customErrorHandler = errorHandler; return this; }; /** * Set forbidden request paths to prevent requesting dangerous places (`["/node_modules", /\/package\.json/g, /\/tsconfig\.json/g, /\/\.([^\.]+)/g]` by default). All previous configuration will be overwritten. * @param forbiddenPaths Forbidden request path begins or regular expression patterns. */ Server.prototype.SetForbiddenPaths = function (forbiddenPaths) { var e_1, _a; try { for (var _b = tslib_1.__values(Object.entries(forbiddenPaths)), _c = _b.next(); !_c.done; _c = _b.next()) { var _d = tslib_1.__read(_c.value, 2), index = _d[0], forbiddenPath = _d[1]; if (!(forbiddenPath instanceof RegExp)) forbiddenPaths[index] = String(forbiddenPath).toLocaleLowerCase(); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } this.forbiddenPaths = forbiddenPaths; return this; }; /** * Add forbidden request paths to prevent requesting dangerous places (`["/node_modules", /\/package\.json/g, /\/tsconfig\.json/g, /\/\.([^\.]+)/g]` by default). * @param forbiddenPaths Forbidden request path begins or regular expression patterns. */ Server.prototype.AddForbiddenPaths = function (forbiddenPaths) { var e_2, _a; try { for (var _b = tslib_1.__values(Object.entries(forbiddenPaths)), _c = _b.next(); !_c.done; _c = _b.next()) { var _d = tslib_1.__read(_c.value, 2), index = _d[0], forbiddenPath = _d[1]; if (!(forbiddenPath instanceof RegExp)) forbiddenPaths[index] = String(forbiddenPath).toLocaleLowerCase(); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } this.forbiddenPaths = [].concat(this.forbiddenPaths, forbiddenPaths); return this; }; /** * Set directory index/default server script file names executed on server side as directory content. * All previous configuration will be replaced. * Default value is: `['index.js']`. * @param indexScripts Array of file names like `['index.js', 'default.js', 'app.js', ...]`. */ Server.prototype.SetIndexScripts = function (indexScripts) { var e_3, _a; try { for (var _b = tslib_1.__values(Object.entries(indexScripts)), _c = _b.next(); !_c.done; _c = _b.next()) { var _d = tslib_1.__read(_c.value, 2), index = _d[0], indexScript = _d[1]; indexScripts[index] = String(indexScript).toLocaleLowerCase(); } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_3) throw e_3.error; } } this.indexes.scripts = indexScripts; return this; }; /** * Add directory index/default server script file names executed on server side as directory content. * Default value is: `['index.js']`. * @param indexScripts Array of file names like `['default.js', 'app.js', ...]`. */ Server.prototype.AddIndexScripts = function (indexScripts) { var e_4, _a; try { for (var _b = tslib_1.__values(Object.entries(indexScripts)), _c = _b.next(); !_c.done; _c = _b.next()) { var _d = tslib_1.__read(_c.value, 2), index = _d[0], indexScript = _d[1]; indexScripts[index] = String(indexScript).toLocaleLowerCase(); } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_4) throw e_4.error; } } this.indexes.scripts = [].concat(this.indexes.scripts, indexScripts); return this; }; /** * Set directory index/default server file names staticly send to client as default directory content. * All previous configuration will be replaced. * Default value is: `['index.html','index.htm','default.html','default.htm']`. * @param indexFiles Array of file names like `['index.html','index.htm','default.html','default.htm', 'directory.html', ...]`. */ Server.prototype.SetIndexFiles = function (indexFiles) { this.indexes.files = indexFiles; return this; }; /** * Add directory index/default server file names staticly send to client as default directory content. * Default value is: `['index.html','index.htm','default.html','default.htm']`. * @param indexFiles Array of file names like `['directory.html', 'directory.htm', ...]`. */ Server.prototype.AddIndexFiles = function (indexFiles) { this.indexes.files = [].concat(this.indexes.scripts, indexFiles); return this; }; /** * @summary Add custom express http handler * @param handler Custom http request handler called every allowed request path before standard server handling. */ Server.prototype.AddPreHandler = function (handler) { this.customHttpPreHandlers.push(handler); return this; }; /** * @summary Return `true` if development flag is used. */ Server.prototype.IsDevelopment = function () { return this.development; }; /** * @summary Return configured domain or ip address. */ Server.prototype.GetHostname = function () { return this.hostName; }; /** * @summary Return configured port number. */ Server.prototype.GetPort = function () { return this.port; }; /** * @summary Return configured document root directory full path. */ Server.prototype.GetDocumentRoot = function () { return this.documentRoot; }; /** * @summary Return configured base url. */ Server.prototype.GetBasePath = function () { return this.basePath; }; /** * @summary Return configured custom errors handler. */ Server.prototype.GetErrorHandler = function () { return this.customErrorHandler; }; /** * Get forbidden request paths to prevent requesting dangerous places. */ Server.prototype.GetForbiddenPaths = function () { return this.forbiddenPaths; }; /** * Get directory index/default server script file names executed on server side as directory content. * Default value is: `['index.js']`. */ Server.prototype.GetIndexScripts = function () { return this.indexes.scripts; }; /** * Get directory index/default server file names staticly send to client as default directory content. * Default value is: `['index.html','index.htm','default.html','default.htm']`. */ Server.prototype.GetIndexFiles = function () { return this.indexes.files; }; /** * @summary Return used http server instance. */ Server.prototype.GetHttpServer = function () { return this.httpServer; }; /** * @summary Return set of connected sockets. */ Server.prototype.GetNetSockets = function () { return this.netSockets; }; /** * @summary Return server running state (`Server.STATES.<state>`). */ Server.prototype.GetState = function () { return this.state; }; /** * @summary Try to find cached record by server document root and requested path * and return directory full path from the cache record. * @param rawRequestUrl Raw requested path. */ Server.prototype.TryToFindIndexPath = function (rawRequestUrl) { var result = []; var qmPos = rawRequestUrl.indexOf('?'); if (qmPos !== -1) rawRequestUrl = rawRequestUrl.substr(0, qmPos); var searchingRequestPaths = this.getSearchingRequestPaths(rawRequestUrl); var parentDirIndexScriptModule = this.register .TryToFindParentDirectoryIndexScriptModule(searchingRequestPaths); if (parentDirIndexScriptModule !== null) result = [ parentDirIndexScriptModule.DirectoryFullPath, parentDirIndexScriptModule.IndexScriptFileName ]; return result; }; /** * @summary Start HTTP server */ Server.prototype.Start = function (callback) { var _this = this; if (this.state !== Server.STATES.CLOSED) return this; this.state = Server.STATES.STARTING; this.documentRoot = path_1.resolve(this.documentRoot || __dirname).replace(/\\/g, '/'); this.port = this.port || Server.DEFAULTS.PORT; this.hostName = this.hostName || Server.DEFAULTS.DOMAIN; this.register = new Register_1.Register(this); this.errorsHandler = new Error_1.ErrorsHandler(this, this.register); this.register.SetErrorsHandler(this.errorsHandler); this.filesHandler = new File_1.FilesHandler(this.errorsHandler); this.directoriesHandler = new Directory_1.DirectoriesHandler(this, this.register, this.filesHandler, this.errorsHandler); this.netSockets = new Set(); var serverOptions = { // @ts-ignore IncomingMessage: Request_1.Request, // @ts-ignore ServerResponse: Response_1.Response }; if (this.customServerHandler !== null) { this.httpServer = http_1.createServer(serverOptions, this.customServerHandler); } else { this.httpServer = http_1.createServer(serverOptions); } this.httpServer.on('connection', function (socket) { _this.netSockets.add(socket); socket.on('close', function () { return _this.netSockets.delete(socket); }); }); this.httpServer.on('close', function (req, res) { return tslib_1.__awaiter(_this, void 0, void 0, function () { return tslib_1.__generator(this, function (_a) { if (this.state === Server.STATES.CLOSING) return [2 /*return*/]; this.state = Server.STATES.CLOSING; this.stopHandler(callback); return [2 /*return*/]; }); }); }); this.httpServer.on('request', function (req, res) { return tslib_1.__awaiter(_this, void 0, void 0, function () { return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.handleReq(req, res)]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }); this.httpServer.on('error', function (err) { _this.state = Server.STATES.CLOSED; if (!callback) { console.error(err); } else { callback(false, err); callback = null; } }); this.httpServer['__wds'] = this; this.httpServer.listen(this.port, this.hostName, function () { _this.state = Server.STATES.STARTED; if (!callback) { console.info("HTTP server has been started. \n" + "(`" + _this.documentRoot + "` => `http://" + _this.hostName + ":" + _this.port.toString() + "`)."); } else { callback(true, null); callback = null; } }); return this; }; /** * @summary Close all registered app instances, close and destroy all connected sockets and stop http server. * @param callback */ Server.prototype.Stop = function (callback) { if (this.state !== Server.STATES.STARTED) return this; this.state = Server.STATES.CLOSING; this.stopHandler(callback); return this; }; /** * @summary Handle all HTTP requests */ Server.prototype.handleReq = function (req, res) { return tslib_1.__awaiter(this, void 0, void 0, function () { var basePath, requestPath, qmPos, pathAllowed, fullPathVirtual; var _this = this; return tslib_1.__generator(this, function (_a) { basePath = req.GetBasePath(); if (this.basePath != null) basePath = basePath.substr(this.basePath.length); requestPath = basePath + req.GetRequestPath(); requestPath = StringHelper_1.StringHelper.DecodeUri(requestPath); qmPos = requestPath.indexOf('?'); if (qmPos !== -1) requestPath = requestPath.substr(0, qmPos); pathAllowed = this.isPathAllowed(requestPath); if (!pathAllowed) { return [2 /*return*/, this.directoriesHandler.HandleForbidden(res)]; } fullPathVirtual = path_1.resolve(this.documentRoot + requestPath).replace(/\\/g, '/'); fullPathVirtual = StringHelper_1.StringHelper.TrimRight(fullPathVirtual, '/'); if (this.development) this.errorsHandler.SetHandledRequestProperties(req, res); (function () { return tslib_1.__awaiter(_this, void 0, void 0, function () { var event, preHandler, i, l, err_1, err, stats; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: if (!(this.customHttpPreHandlers.length > 0)) return [3 /*break*/, 8]; event = new Event_1.Event(req, res, fullPathVirtual); i = 0, l = this.customHttpPreHandlers.length; _a.label = 1; case 1: if (!(i < l)) return [3 /*break*/, 7]; preHandler = this.customHttpPreHandlers[i]; _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, preHandler.call(null, req, res, event)]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: err_1 = _a.sent(); this.errorsHandler .LogError(err_1, 500, req, res) .PrintError(err_1, 500, req, res); event.PreventDefault(); return [3 /*break*/, 5]; case 5: if (event.IsPreventDefault()) return [3 /*break*/, 7]; _a.label = 6; case 6: i++; return [3 /*break*/, 1]; case 7: if (event.IsPreventDefault()) return [2 /*return*/]; _a.label = 8; case 8: err = null; return [4 /*yield*/, new Promise(function (resolve, reject) { fs_1.stat(fullPathVirtual, function (errLocal, stats) { if (errLocal) err = errLocal; resolve(stats); }); })]; case 9: stats = _a.sent(); if (stats) { this.handleReqExistingPath(fullPathVirtual, requestPath, stats, req, res); } else if (err && err.code == 'ENOENT') { this.handleReqNonExistingPath(requestPath, req, res); } else { this.errorsHandler.PrintError(err); } return [2 /*return*/]; } }); }); })(); return [2 /*return*/]; }); }); }; /** * @summary Close all registered app instances, close and destroy all connected sockets and stop http server. * @param callback */ Server.prototype.stopHandler = function (callback) { var _this = this; this.register.StopAll(function () { _this.netSockets.forEach(function (socket) { socket.destroy(); _this.netSockets.delete(socket); }); _this.httpServer.close(function (err) { _this.state = Server.STATES.CLOSED; if (!callback) { console.info("HTTP server has been closed. \n" + "(`http://" + _this.hostName + ":" + _this.port.toString() + "`)."); } else { callback(err == null, err); } }); }); }; /** * Get if path is allowed by `this.forbiddenPaths` configuration. * @param path Path including start slash, excluding base url and excluding params. */ Server.prototype.isPathAllowed = function (path) { var result = true, pathLower = path.toLocaleLowerCase(), beginPathLower, regExp, match; for (var i = 0, l = this.forbiddenPaths.length; i < l; i++) { if (this.forbiddenPaths[i] instanceof RegExp) { regExp = this.forbiddenPaths[i]; match = path.match(regExp); if (match !== null && match.length > 0) { result = false; break; } } else { beginPathLower = this.forbiddenPaths[i].toString(); if (pathLower.indexOf(beginPathLower) === 0) { result = false; break; } } } return result; }; /** * @summary Process request content found */ Server.prototype.handleReqExistingPath = function (fullPath, requestPath, stats, req, res) { var _this = this; if (stats.isDirectory()) { var httpReq = req; var originalPathname = url_1.parse(httpReq.url, false).pathname; if (originalPathname.charAt(originalPathname.length - 1) !== '/') { res.Redirect(originalPathname + '/', 301, "Correcting directory request", true); } else { fs_1.readdir(fullPath, function (err, dirItems) { if (err != null) { _this.errorsHandler .LogError(err, 403, req, res) .PrintError(err, 403, req, res); return; } _this.directoriesHandler.HandleDirectory(fullPath, requestPath, stats, dirItems, 200, req, res); }); } } else if (stats.isFile()) { var dirFullPath, fileName, lastSlashPos; fullPath = StringHelper_1.StringHelper.TrimRight(fullPath, '/'); lastSlashPos = fullPath.lastIndexOf('/'); if (lastSlashPos !== -1) { fileName = fullPath.substr(lastSlashPos + 1); dirFullPath = fullPath.substr(0, lastSlashPos); } else { fileName = fullPath; dirFullPath = ''; } if (this.indexes.scripts.indexOf(fileName.toLocaleLowerCase()) != -1) { this.directoriesHandler.HandleIndexScript(dirFullPath, fileName, stats.mtime.getTime(), req, res); } else { this.filesHandler.HandleFile(fullPath, fileName, stats, res); } } /* else ( stats.isBlockDevice() || stats.isCharacterDevice() || stats.isSymbolicLink() || stats.isFIFO() || stats.isSocket() ) { cb(); }*/ }; /** * @summary Display error 500/404 (and try to list first existing parent folder content): */ Server.prototype.handleReqNonExistingPath = function (requestPath, req, res) { var _this = this; var searchingRequestPaths = this.getSearchingRequestPaths(requestPath); var parentDirIndexScriptModule = this.register .TryToFindParentDirectoryIndexScriptModule(searchingRequestPaths); if (parentDirIndexScriptModule != null) { if (!this.development) { this.directoriesHandler.HandleIndexScript(parentDirIndexScriptModule.DirectoryFullPath, parentDirIndexScriptModule.IndexScriptFileName, parentDirIndexScriptModule.IndexScriptModTime, req, res); } else { fs_1.stat(parentDirIndexScriptModule.DirectoryFullPath, function (err, stats) { if (err) { return console.error(err); } _this.directoriesHandler.HandleIndexScript(parentDirIndexScriptModule.DirectoryFullPath, parentDirIndexScriptModule.IndexScriptFileName, stats.mtime.getTime(), req, res); }); } } else { this.handleReqNonExistPath(searchingRequestPaths, 0, function (newFullPath, newRequestPath, foundParentDirStats) { fs_1.readdir(newFullPath, function (err, dirItems) { if (err != null) { _this.errorsHandler .LogError(err, 403, req, res) .PrintError(err, 403, req, res); return; } _this.directoriesHandler.HandleDirectory(newFullPath, newRequestPath, foundParentDirStats, dirItems, 404, req, res); }); }, function (err) { var error = null; try { throw new Error("Path not found: `" + requestPath + "`."); } catch (e) { error = e; } _this.errorsHandler .LogError(error, 404, req, res) .PrintError(error, 404, req, res); }); } }; /** * @summary Try to get file system directory stats - recursively on first existing parent directory. */ Server.prototype.handleReqNonExistPath = function (pathsToFound, index, successCallback, errorCallback) { var _this = this; var pathToFound = pathsToFound[index]; var newRequestPath = StringHelper_1.StringHelper.TrimLeft(pathToFound, '/'); fs_1.stat(this.documentRoot + pathToFound, function (err, dirStats) { if (err == null) { var newFullPath = StringHelper_1.StringHelper.TrimRight(_this.documentRoot + '/' + newRequestPath, '/'); successCallback(newFullPath, newRequestPath, dirStats); } else { index += 1; if (index == pathsToFound.length) { errorCallback(err); } else { _this.handleReqNonExistPath(pathsToFound, index, successCallback, errorCallback); } } }); }; Server.prototype.getSearchingRequestPaths = function (requestPath) { var pathExploded = StringHelper_1.StringHelper.Trim(requestPath, '/').split('/'), searchingRequestPath = '', searchingRequestPaths = []; pathExploded.forEach(function (item) { searchingRequestPath += '/' + item; searchingRequestPaths.push(searchingRequestPath); }); searchingRequestPaths.reverse(); if (searchingRequestPaths.length === 1 && searchingRequestPaths[0] != '/') searchingRequestPaths.push('/'); return searchingRequestPaths; }; Server.VERSION = '3.0.27'; Server.STATES = { CLOSED: 0, STARTING: 1, CLOSING: 2, STARTED: 4 }; Server.DEFAULTS = { PORT: 8000, DOMAIN: '127.0.0.1', RESPONSES: Defaults_1.Defaults }; return Server; }()); exports.Server = Server; //# sourceMappingURL=Server.js.map