ENiGMA 1/2 WILL USE SPACES FROM THIS POINT ON VS TABS

* Really just to make GitHub formatting happy. Arg.
This commit is contained in:
Bryan Ashby 2018-06-21 23:15:04 -06:00
parent 5ddf04c882
commit e9787cee3e
135 changed files with 27397 additions and 27397 deletions

View file

@ -6,14 +6,14 @@ const Log = require('../../logger.js').log;
const { ServerModule } = require('../../server_module.js');
const Config = require('../../config.js').get;
const {
splitTextAtTerms,
isAnsi,
cleanControlCodes
splitTextAtTerms,
isAnsi,
cleanControlCodes
} = require('../../string_util.js');
const {
getMessageConferenceByTag,
getMessageAreaByTag,
getMessageListForArea,
getMessageConferenceByTag,
getMessageAreaByTag,
getMessageListForArea,
} = require('../../message_area.js');
const { sortAreasOrConfs } = require('../../conf_area_util.js');
const AnsiPrep = require('../../ansi_prep.js');
@ -26,221 +26,221 @@ const paths = require('path');
const moment = require('moment');
const ModuleInfo = exports.moduleInfo = {
name : 'Gopher',
desc : 'Gopher Server',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.gopher.server',
name : 'Gopher',
desc : 'Gopher Server',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.gopher.server',
};
const Message = require('../../message.js');
const ItemTypes = {
Invalid : '', // not really a type, of course!
Invalid : '', // not really a type, of course!
// Canonical, RFC-1436
TextFile : '0',
SubMenu : '1',
CCSONameserver : '2',
Error : '3',
BinHexFile : '4',
DOSFile : '5',
UuEncodedFile : '6',
FullTextSearch : '7',
Telnet : '8',
BinaryFile : '9',
AltServer : '+',
GIFFile : 'g',
ImageFile : 'I',
Telnet3270 : 'T',
// Canonical, RFC-1436
TextFile : '0',
SubMenu : '1',
CCSONameserver : '2',
Error : '3',
BinHexFile : '4',
DOSFile : '5',
UuEncodedFile : '6',
FullTextSearch : '7',
Telnet : '8',
BinaryFile : '9',
AltServer : '+',
GIFFile : 'g',
ImageFile : 'I',
Telnet3270 : 'T',
// Non-canonical
HtmlFile : 'h',
InfoMessage : 'i',
SoundFile : 's',
// Non-canonical
HtmlFile : 'h',
InfoMessage : 'i',
SoundFile : 's',
};
exports.getModule = class GopherModule extends ServerModule {
constructor() {
super();
constructor() {
super();
this.routes = new Map(); // selector->generator => gopher item
this.log = Log.child( { server : 'Gopher' } );
}
this.routes = new Map(); // selector->generator => gopher item
this.log = Log.child( { server : 'Gopher' } );
}
createServer() {
if(!this.enabled) {
return;
}
createServer() {
if(!this.enabled) {
return;
}
const config = Config();
this.publicHostname = config.contentServers.gopher.publicHostname;
this.publicPort = config.contentServers.gopher.publicPort;
const config = Config();
this.publicHostname = config.contentServers.gopher.publicHostname;
this.publicPort = config.contentServers.gopher.publicPort;
this.addRoute(/^\/?\r\n$/, this.defaultGenerator);
this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator);
this.addRoute(/^\/?\r\n$/, this.defaultGenerator);
this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator);
this.server = net.createServer( socket => {
socket.setEncoding('ascii');
this.server = net.createServer( socket => {
socket.setEncoding('ascii');
socket.on('data', data => {
this.routeRequest(data, socket);
});
socket.on('data', data => {
this.routeRequest(data, socket);
});
socket.on('error', err => {
if('ECONNRESET' !== err.code) { // normal
this.log.trace( { error : err.message }, 'Socket error');
}
});
});
}
socket.on('error', err => {
if('ECONNRESET' !== err.code) { // normal
this.log.trace( { error : err.message }, 'Socket error');
}
});
});
}
listen() {
if(!this.enabled) {
return true; // nothing to do, but not an error
}
listen() {
if(!this.enabled) {
return true; // nothing to do, but not an error
}
const config = Config();
const port = parseInt(config.contentServers.gopher.port);
if(isNaN(port)) {
this.log.warn( { port : config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' );
return false;
}
const config = Config();
const port = parseInt(config.contentServers.gopher.port);
if(isNaN(port)) {
this.log.warn( { port : config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' );
return false;
}
return this.server.listen(port);
}
return this.server.listen(port);
}
get enabled() {
return _.get(Config(), 'contentServers.gopher.enabled', false) && this.isConfigured();
}
get enabled() {
return _.get(Config(), 'contentServers.gopher.enabled', false) && this.isConfigured();
}
isConfigured() {
// public hostname & port must be set; responses contain them!
const config = Config();
return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) &&
isConfigured() {
// public hostname & port must be set; responses contain them!
const config = Config();
return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) &&
_.isNumber(_.get(config, 'contentServers.gopher.publicPort'));
}
}
addRoute(selectorRegExp, generatorHandler) {
if(_.isString(selectorRegExp)) {
try {
selectorRegExp = new RegExp(`${selectorRegExp}\r\n`);
} catch(e) {
this.log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' );
return false;
}
}
this.routes.set(selectorRegExp, generatorHandler.bind(this));
}
addRoute(selectorRegExp, generatorHandler) {
if(_.isString(selectorRegExp)) {
try {
selectorRegExp = new RegExp(`${selectorRegExp}\r\n`);
} catch(e) {
this.log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' );
return false;
}
}
this.routes.set(selectorRegExp, generatorHandler.bind(this));
}
routeRequest(selector, socket) {
let match;
for(let [regex, gen] of this.routes) {
match = selector.match(regex);
if(match) {
return gen(match, res => {
return socket.end(`${res}`);
});
}
}
this.notFoundGenerator(selector, res => {
return socket.end(`${res}`);
});
}
routeRequest(selector, socket) {
let match;
for(let [regex, gen] of this.routes) {
match = selector.match(regex);
if(match) {
return gen(match, res => {
return socket.end(`${res}`);
});
}
}
this.notFoundGenerator(selector, res => {
return socket.end(`${res}`);
});
}
makeItem(itemType, text, selector, hostname, port) {
selector = selector || ''; // e.g. for info
hostname = hostname || this.publicHostname;
port = port || this.publicPort;
return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`;
}
makeItem(itemType, text, selector, hostname, port) {
selector = selector || ''; // e.g. for info
hostname = hostname || this.publicHostname;
port = port || this.publicPort;
return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`;
}
defaultGenerator(selectorMatch, cb) {
this.log.trace( { selector : selectorMatch[0] }, 'Serving default content');
defaultGenerator(selectorMatch, cb) {
this.log.trace( { selector : selectorMatch[0] }, 'Serving default content');
let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc');
bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile);
fs.readFile(bannerFile, 'utf8', (err, banner) => {
if(err) {
return cb('You have reached an ENiGMA½ Gopher server!');
}
let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc');
bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile);
fs.readFile(bannerFile, 'utf8', (err, banner) => {
if(err) {
return cb('You have reached an ENiGMA½ Gopher server!');
}
banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join('');
banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea');
return cb(banner);
});
}
banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join('');
banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea');
return cb(banner);
});
}
notFoundGenerator(selector, cb) {
this.log.trace( { selector }, 'Serving not found content');
return cb('Not found');
}
notFoundGenerator(selector, cb) {
this.log.trace( { selector }, 'Serving not found content');
return cb('Not found');
}
isAreaAndConfExposed(confTag, areaTag) {
const conf = _.get(Config(), [ 'contentServers', 'gopher', 'messageConferences', confTag ]);
return Array.isArray(conf) && conf.includes(areaTag);
}
isAreaAndConfExposed(confTag, areaTag) {
const conf = _.get(Config(), [ 'contentServers', 'gopher', 'messageConferences', confTag ]);
return Array.isArray(conf) && conf.includes(areaTag);
}
prepareMessageBody(body, cb) {
if(isAnsi(body)) {
AnsiPrep(
body,
{
cols : 79, // Gopher std. wants 70, but we'll have to deal with it.
forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols|
},
(err, prepped) => {
return cb(prepped || body);
}
);
} else {
return cb(cleanControlCodes(body, { all : true } ));
}
}
prepareMessageBody(body, cb) {
if(isAnsi(body)) {
AnsiPrep(
body,
{
cols : 79, // Gopher std. wants 70, but we'll have to deal with it.
forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols|
},
(err, prepped) => {
return cb(prepped || body);
}
);
} else {
return cb(cleanControlCodes(body, { all : true } ));
}
}
shortenSubject(subject) {
return _.truncate(subject, { length : 30 } );
}
shortenSubject(subject) {
return _.truncate(subject, { length : 30 } );
}
messageAreaGenerator(selectorMatch, cb) {
this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content');
//
// Selector should be:
// /msgarea - list confs
// /msgarea/conftag - list areas in conf
// /msgarea/conftag/areatag - list messages in area
// /msgarea/conftag/areatag/<UUID> - message as text
// /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers
//
if(selectorMatch[3] || selectorMatch[4]) {
// message
//const raw = selectorMatch[4] ? true : false;
// :TODO: support 'raw'
const msgUuid = selectorMatch[3].replace(/\r\n|\//g, '');
const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const message = new Message();
messageAreaGenerator(selectorMatch, cb) {
this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content');
//
// Selector should be:
// /msgarea - list confs
// /msgarea/conftag - list areas in conf
// /msgarea/conftag/areatag - list messages in area
// /msgarea/conftag/areatag/<UUID> - message as text
// /msgarea/conftag/areatag/<UUID>_raw - full message as text + headers
//
if(selectorMatch[3] || selectorMatch[4]) {
// message
//const raw = selectorMatch[4] ? true : false;
// :TODO: support 'raw'
const msgUuid = selectorMatch[3].replace(/\r\n|\//g, '');
const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const message = new Message();
return message.load( { uuid : msgUuid }, err => {
if(err) {
this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existant message UUID!');
return this.notFoundGenerator(selectorMatch, cb);
}
return message.load( { uuid : msgUuid }, err => {
if(err) {
this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existant message UUID!');
return this.notFoundGenerator(selectorMatch, cb);
}
if(message.areaTag !== areaTag || !this.isAreaAndConfExposed(confTag, areaTag)) {
this.log.warn( { areaTag }, 'Attempted access to non-exposed conference and/or area!');
return this.notFoundGenerator(selectorMatch, cb);
}
if(message.areaTag !== areaTag || !this.isAreaAndConfExposed(confTag, areaTag)) {
this.log.warn( { areaTag }, 'Attempted access to non-exposed conference and/or area!');
return this.notFoundGenerator(selectorMatch, cb);
}
if(Message.isPrivateAreaTag(areaTag)) {
this.log.warn( { areaTag }, 'Attempted access to message in private area!');
return this.notFoundGenerator(selectorMatch, cb);
}
if(Message.isPrivateAreaTag(areaTag)) {
this.log.warn( { areaTag }, 'Attempted access to message in private area!');
return this.notFoundGenerator(selectorMatch, cb);
}
this.prepareMessageBody(message.message, msgBody => {
const response = `${'-'.repeat(70)}
this.prepareMessageBody(message.message, msgBody => {
const response = `${'-'.repeat(70)}
To : ${message.toUserName}
From : ${message.fromUserName}
When : ${moment(message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')}
@ -249,87 +249,87 @@ ID : ${message.messageUuid} (${message.messageId})
${'-'.repeat(70)}
${msgBody}
`;
return cb(response);
});
});
} else if(selectorMatch[2]) {
// list messages in area
const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const area = getMessageAreaByTag(areaTag);
return cb(response);
});
});
} else if(selectorMatch[2]) {
// list messages in area
const confTag = selectorMatch[1].substr(1).split('/')[0];
const areaTag = selectorMatch[2].replace(/\r\n|\//g, '');
const area = getMessageAreaByTag(areaTag);
if(Message.isPrivateAreaTag(areaTag)) {
this.log.warn( { areaTag }, 'Attempted access to private area!');
return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private'));
}
if(Message.isPrivateAreaTag(areaTag)) {
this.log.warn( { areaTag }, 'Attempted access to private area!');
return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private'));
}
if(!area || !this.isAreaAndConfExposed(confTag, areaTag)) {
this.log.warn( { confTag, areaTag }, 'Attempted access to non-exposed conference and/or area!');
return this.notFoundGenerator(selectorMatch, cb);
}
if(!area || !this.isAreaAndConfExposed(confTag, areaTag)) {
this.log.warn( { confTag, areaTag }, 'Attempted access to non-exposed conference and/or area!');
return this.notFoundGenerator(selectorMatch, cb);
}
return getMessageListForArea(null, areaTag, (err, msgList) => {
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...msgList.map(msg => this.makeItem(
ItemTypes.TextFile,
`${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(msg.subject)} (${msg.fromUserName} to ${msg.toUserName})`,
`/msgarea/${confTag}/${areaTag}/${msg.messageUuid}`
))
].join('');
return getMessageListForArea(null, areaTag, (err, msgList) => {
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...msgList.map(msg => this.makeItem(
ItemTypes.TextFile,
`${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(msg.subject)} (${msg.fromUserName} to ${msg.toUserName})`,
`/msgarea/${confTag}/${areaTag}/${msg.messageUuid}`
))
].join('');
return cb(response);
});
} else if(selectorMatch[1]) {
// list areas in conf
const sysConfig = Config();
const confTag = selectorMatch[1].replace(/\r\n|\//g, '');
const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag);
if(!conf) {
return this.notFoundGenerator(selectorMatch, cb);
}
return cb(response);
});
} else if(selectorMatch[1]) {
// list areas in conf
const sysConfig = Config();
const confTag = selectorMatch[1].replace(/\r\n|\//g, '');
const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag);
if(!conf) {
return this.notFoundGenerator(selectorMatch, cb);
}
const areas = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ], {})
.map(areaTag => Object.assign( { areaTag }, getMessageAreaByTag(areaTag)))
.filter(area => area && !Message.isPrivateAreaTag(area.areaTag));
const areas = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ], {})
.map(areaTag => Object.assign( { areaTag }, getMessageAreaByTag(areaTag)))
.filter(area => area && !Message.isPrivateAreaTag(area.areaTag));
if(0 === areas.length) {
return cb(this.makeItem(ItemTypes.InfoMessage, 'No message areas available'));
}
if(0 === areas.length) {
return cb(this.makeItem(ItemTypes.InfoMessage, 'No message areas available'));
}
sortAreasOrConfs(areas);
sortAreasOrConfs(areas);
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...areas.map(area => this.makeItem(ItemTypes.SubMenu, area.name, `/msgarea/${confTag}/${area.areaTag}`))
].join('');
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
...areas.map(area => this.makeItem(ItemTypes.SubMenu, area.name, `/msgarea/${confTag}/${area.areaTag}`))
].join('');
return cb(response);
} else {
// message area base (list confs)
const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {}))
.map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag)))
.filter(conf => conf); // remove any baddies
return cb(response);
} else {
// message area base (list confs)
const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {}))
.map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag)))
.filter(conf => conf); // remove any baddies
if(0 === confs.length) {
return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available'));
}
if(0 === confs.length) {
return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available'));
}
sortAreasOrConfs(confs);
sortAreasOrConfs(confs);
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, ''),
...confs.map(conf => this.makeItem(ItemTypes.SubMenu, conf.name, `/msgarea/${conf.confTag}`))
].join('');
const response = [
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'),
this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)),
this.makeItem(ItemTypes.InfoMessage, ''),
...confs.map(conf => this.makeItem(ItemTypes.SubMenu, conf.name, `/msgarea/${conf.confTag}`))
].join('');
return cb(response);
}
}
return cb(response);
}
}
};

View file

@ -15,169 +15,169 @@ const paths = require('path');
const mimeTypes = require('mime-types');
const ModuleInfo = exports.moduleInfo = {
name : 'Web',
desc : 'Web Server',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.web.server',
name : 'Web',
desc : 'Web Server',
author : 'NuSkooler',
packageName : 'codes.l33t.enigma.web.server',
};
class Route {
constructor(route) {
Object.assign(this, route);
if(this.method) {
this.method = this.method.toUpperCase();
}
constructor(route) {
Object.assign(this, route);
try {
this.pathRegExp = new RegExp(this.path);
} catch(e) {
Log.debug( { route : route }, 'Invalid regular expression for route path' );
}
}
if(this.method) {
this.method = this.method.toUpperCase();
}
isValid() {
return (
this.pathRegExp instanceof RegExp &&
( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) ||
try {
this.pathRegExp = new RegExp(this.path);
} catch(e) {
Log.debug( { route : route }, 'Invalid regular expression for route path' );
}
}
isValid() {
return (
this.pathRegExp instanceof RegExp &&
( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) ||
!_.isFunction(this.handler)
);
}
);
}
matchesRequest(req) {
return req.method === this.method && this.pathRegExp.test(req.url);
}
matchesRequest(req) {
return req.method === this.method && this.pathRegExp.test(req.url);
}
getRouteKey() { return `${this.method}:${this.path}`; }
getRouteKey() { return `${this.method}:${this.path}`; }
}
exports.getModule = class WebServerModule extends ServerModule {
constructor() {
super();
constructor() {
super();
const config = Config();
this.enableHttp = config.contentServers.web.http.enabled || false;
this.enableHttps = config.contentServers.web.https.enabled || false;
const config = Config();
this.enableHttp = config.contentServers.web.http.enabled || false;
this.enableHttps = config.contentServers.web.https.enabled || false;
this.routes = {};
this.routes = {};
if(this.isEnabled() && config.contentServers.web.staticRoot) {
this.addRoute({
method : 'GET',
path : '/static/.*$',
handler : this.routeStaticFile.bind(this),
});
}
}
if(this.isEnabled() && config.contentServers.web.staticRoot) {
this.addRoute({
method : 'GET',
path : '/static/.*$',
handler : this.routeStaticFile.bind(this),
});
}
}
buildUrl(pathAndQuery) {
//
// Create a URL such as
// https://l33t.codes:44512/ + |pathAndQuery|
//
// Prefer HTTPS over HTTP. Be explicit about the port
// only if non-standard. Allow users to override full prefix in config.
//
const config = Config();
if(_.isString(config.contentServers.web.overrideUrlPrefix)) {
return `${config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`;
}
buildUrl(pathAndQuery) {
//
// Create a URL such as
// https://l33t.codes:44512/ + |pathAndQuery|
//
// Prefer HTTPS over HTTP. Be explicit about the port
// only if non-standard. Allow users to override full prefix in config.
//
const config = Config();
if(_.isString(config.contentServers.web.overrideUrlPrefix)) {
return `${config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`;
}
let schema;
let port;
if(config.contentServers.web.https.enabled) {
schema = 'https://';
port = (443 === config.contentServers.web.https.port) ?
'' :
`:${config.contentServers.web.https.port}`;
} else {
schema = 'http://';
port = (80 === config.contentServers.web.http.port) ?
'' :
`:${config.contentServers.web.http.port}`;
}
return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`;
}
let schema;
let port;
if(config.contentServers.web.https.enabled) {
schema = 'https://';
port = (443 === config.contentServers.web.https.port) ?
'' :
`:${config.contentServers.web.https.port}`;
} else {
schema = 'http://';
port = (80 === config.contentServers.web.http.port) ?
'' :
`:${config.contentServers.web.http.port}`;
}
isEnabled() {
return this.enableHttp || this.enableHttps;
}
return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`;
}
createServer() {
if(this.enableHttp) {
this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) );
}
isEnabled() {
return this.enableHttp || this.enableHttps;
}
const config = Config();
if(this.enableHttps) {
const options = {
cert : fs.readFileSync(config.contentServers.web.https.certPem),
key : fs.readFileSync(config.contentServers.web.https.keyPem),
};
createServer() {
if(this.enableHttp) {
this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) );
}
// additional options
Object.assign(options, config.contentServers.web.https.options || {} );
const config = Config();
if(this.enableHttps) {
const options = {
cert : fs.readFileSync(config.contentServers.web.https.certPem),
key : fs.readFileSync(config.contentServers.web.https.keyPem),
};
this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) );
}
}
// additional options
Object.assign(options, config.contentServers.web.https.options || {} );
listen() {
let ok = true;
this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) );
}
}
const config = Config();
[ 'http', 'https' ].forEach(service => {
const name = `${service}Server`;
if(this[name]) {
const port = parseInt(config.contentServers.web[service].port);
if(isNaN(port)) {
ok = false;
return Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` );
}
return this[name].listen(port);
}
});
listen() {
let ok = true;
return ok;
}
const config = Config();
[ 'http', 'https' ].forEach(service => {
const name = `${service}Server`;
if(this[name]) {
const port = parseInt(config.contentServers.web[service].port);
if(isNaN(port)) {
ok = false;
return Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` );
}
return this[name].listen(port);
}
});
addRoute(route) {
route = new Route(route);
return ok;
}
if(!route.isValid()) {
Log.warn( { route : route }, 'Cannot add route: missing or invalid required members' );
return false;
}
addRoute(route) {
route = new Route(route);
const routeKey = route.getRouteKey();
if(routeKey in this.routes) {
Log.warn( { route : route, routeKey : routeKey }, 'Cannot add route: duplicate method/path combination exists' );
return false;
}
if(!route.isValid()) {
Log.warn( { route : route }, 'Cannot add route: missing or invalid required members' );
return false;
}
this.routes[routeKey] = route;
return true;
}
const routeKey = route.getRouteKey();
if(routeKey in this.routes) {
Log.warn( { route : route, routeKey : routeKey }, 'Cannot add route: duplicate method/path combination exists' );
return false;
}
routeRequest(req, resp) {
const route = _.find(this.routes, r => r.matchesRequest(req) );
this.routes[routeKey] = route;
return true;
}
if(!route && '/' === req.url) {
return this.routeIndex(req, resp);
}
routeRequest(req, resp) {
const route = _.find(this.routes, r => r.matchesRequest(req) );
return route ? route.handler(req, resp) : this.accessDenied(resp);
}
if(!route && '/' === req.url) {
return this.routeIndex(req, resp);
}
respondWithError(resp, code, bodyText, title) {
const customErrorPage = paths.join(Config().contentServers.web.staticRoot, `${code}.html`);
return route ? route.handler(req, resp) : this.accessDenied(resp);
}
fs.readFile(customErrorPage, 'utf8', (err, data) => {
resp.writeHead(code, { 'Content-Type' : 'text/html' } );
respondWithError(resp, code, bodyText, title) {
const customErrorPage = paths.join(Config().contentServers.web.staticRoot, `${code}.html`);
if(err) {
return resp.end(`<!doctype html>
fs.readFile(customErrorPage, 'utf8', (err, data) => {
resp.writeHead(code, { 'Content-Type' : 'text/html' } );
if(err) {
return resp.end(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
@ -190,74 +190,74 @@ exports.getModule = class WebServerModule extends ServerModule {
</article>
</body>
</html>`
);
}
);
}
return resp.end(data);
});
}
return resp.end(data);
});
}
accessDenied(resp) {
return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
}
accessDenied(resp) {
return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
}
fileNotFound(resp) {
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
}
fileNotFound(resp) {
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
}
routeIndex(req, resp) {
const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html');
routeIndex(req, resp) {
const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html');
return this.returnStaticPage(filePath, resp);
}
return this.returnStaticPage(filePath, resp);
}
routeStaticFile(req, resp) {
const fileName = req.url.substr(req.url.indexOf('/', 1));
const filePath = paths.join(Config().contentServers.web.staticRoot, fileName);
routeStaticFile(req, resp) {
const fileName = req.url.substr(req.url.indexOf('/', 1));
const filePath = paths.join(Config().contentServers.web.staticRoot, fileName);
return this.returnStaticPage(filePath, resp);
}
return this.returnStaticPage(filePath, resp);
}
returnStaticPage(filePath, resp) {
const self = this;
returnStaticPage(filePath, resp) {
const self = this;
fs.stat(filePath, (err, stats) => {
if(err || !stats.isFile()) {
return self.fileNotFound(resp);
}
fs.stat(filePath, (err, stats) => {
if(err || !stats.isFile()) {
return self.fileNotFound(resp);
}
const headers = {
'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size,
};
const headers = {
'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size,
};
const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers);
return readStream.pipe(resp);
});
}
const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers);
return readStream.pipe(resp);
});
}
routeTemplateFilePage(templatePath, preprocessCallback, resp) {
const self = this;
routeTemplateFilePage(templatePath, preprocessCallback, resp) {
const self = this;
fs.readFile(templatePath, 'utf8', (err, templateData) => {
if(err) {
return self.fileNotFound(resp);
}
fs.readFile(templatePath, 'utf8', (err, templateData) => {
if(err) {
return self.fileNotFound(resp);
}
preprocessCallback(templateData, (err, finalPage, contentType) => {
if(err || !finalPage) {
return self.respondWithError(resp, 500, 'Internal Server Error.', 'Internal Server Error');
}
preprocessCallback(templateData, (err, finalPage, contentType) => {
if(err || !finalPage) {
return self.respondWithError(resp, 500, 'Internal Server Error.', 'Internal Server Error');
}
const headers = {
'Content-Type' : contentType || mimeTypes.contentType('.html'),
'Content-Length' : finalPage.length,
};
const headers = {
'Content-Type' : contentType || mimeTypes.contentType('.html'),
'Content-Length' : finalPage.length,
};
resp.writeHead(200, headers);
return resp.end(finalPage);
});
});
}
resp.writeHead(200, headers);
return resp.end(finalPage);
});
});
}
};