web-dev-server
Version:
Node.js simple http server for common development or training purposes.
708 lines • 30.6 kB
JavaScript
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