* Add FileBaseFilters
* Add HTTP(S) file web server with temp URLs * Get temp web d/l from file list * Add File area filter editor (all file area stuff will be rename to file "base" later) * Concept of "listening servers" vs "login servers" * Ability to get servers by their package name * New MCI: %FN: File Base active filter name * Some ES6 updates * VC resetInitialFocus() to set focus to explicit/detected initial focus field * Limit what is dumped out when logging form data
This commit is contained in:
parent
712cf512f0
commit
a7c0f2b7b0
136
core/bbs.js
136
core/bbs.js
|
@ -76,9 +76,6 @@ function bbsMain() {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function listenConnections(callback) {
|
|
||||||
return startListening(callback);
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
function complete(err) {
|
function complete(err) {
|
||||||
// note this is escaped:
|
// note this is escaped:
|
||||||
|
@ -113,6 +110,12 @@ function shutdownSystem() {
|
||||||
}
|
}
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
|
function stopListeningServers(callback) {
|
||||||
|
return require('./listening_server.js').shutdown( () => {
|
||||||
|
// :TODO: log err
|
||||||
|
return callback(null); // ignore err
|
||||||
|
});
|
||||||
|
},
|
||||||
function stopEventScheduler(callback) {
|
function stopEventScheduler(callback) {
|
||||||
if(initServices.eventScheduler) {
|
if(initServices.eventScheduler) {
|
||||||
return initServices.eventScheduler.shutdown( () => {
|
return initServices.eventScheduler.shutdown( () => {
|
||||||
|
@ -122,6 +125,12 @@ function shutdownSystem() {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
function stopFileAreaWeb(callback) {
|
||||||
|
require('./file_area_web.js').startup(err => {
|
||||||
|
// :TODO: Log me if err
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
function stopMsgNetwork(callback) {
|
function stopMsgNetwork(callback) {
|
||||||
require('./msg_network.js').shutdown(callback);
|
require('./msg_network.js').shutdown(callback);
|
||||||
}
|
}
|
||||||
|
@ -222,6 +231,12 @@ function initialize(cb) {
|
||||||
function readyMessageNetworkSupport(callback) {
|
function readyMessageNetworkSupport(callback) {
|
||||||
return require('./msg_network.js').startup(callback);
|
return require('./msg_network.js').startup(callback);
|
||||||
},
|
},
|
||||||
|
function listenConnections(callback) {
|
||||||
|
return require('./listening_server.js').startup(callback);
|
||||||
|
},
|
||||||
|
function readyFileAreaWeb(callback) {
|
||||||
|
return require('./file_area_web.js').startup(callback);
|
||||||
|
},
|
||||||
function readyEventScheduler(callback) {
|
function readyEventScheduler(callback) {
|
||||||
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
||||||
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
||||||
|
@ -235,118 +250,3 @@ function initialize(cb) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListening(cb) {
|
|
||||||
if(!conf.config.loginServers) {
|
|
||||||
// :TODO: Log error ... output to stderr as well. We can do it all with the logger
|
|
||||||
return cb(new Error('No login servers configured'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
|
||||||
|
|
||||||
moduleUtil.loadModulesForCategory('loginServers', (err, module) => {
|
|
||||||
if(err) {
|
|
||||||
if('EENIGMODDISABLED' === err.code) {
|
|
||||||
logger.log.debug(err.message);
|
|
||||||
} else {
|
|
||||||
logger.log.info( { err : err }, 'Failed loading module');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = parseInt(module.runtime.config.port);
|
|
||||||
if(isNaN(port)) {
|
|
||||||
logger.log.error( { port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleInst = new module.getModule();
|
|
||||||
let server;
|
|
||||||
try {
|
|
||||||
server = moduleInst.createServer();
|
|
||||||
} catch(e) {
|
|
||||||
logger.log.warn(e, 'Exception caught creating server!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// :TODO: handle maxConnections, e.g. conf.maxConnections
|
|
||||||
|
|
||||||
server.on('client', function newClient(client, clientSock) {
|
|
||||||
//
|
|
||||||
// Start tracking the client. We'll assign it an ID which is
|
|
||||||
// just the index in our connections array.
|
|
||||||
//
|
|
||||||
if(_.isUndefined(client.session)) {
|
|
||||||
client.session = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
client.session.serverName = module.moduleInfo.name;
|
|
||||||
client.session.isSecure = module.moduleInfo.isSecure || false;
|
|
||||||
|
|
||||||
clientConns.addNewClient(client, clientSock);
|
|
||||||
|
|
||||||
client.on('ready', function clientReady(readyOptions) {
|
|
||||||
|
|
||||||
client.startIdleMonitor();
|
|
||||||
|
|
||||||
// Go to module -- use default error handler
|
|
||||||
prepareClient(client, function clientPrepared() {
|
|
||||||
require('./connect.js').connectEntry(client, readyOptions.firstMenu);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('end', function onClientEnd() {
|
|
||||||
clientConns.removeClient(client);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', function onClientError(err) {
|
|
||||||
logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('close', function onClientClose(hadError) {
|
|
||||||
const logFunc = hadError ? logger.log.info : logger.log.debug;
|
|
||||||
logFunc( { clientId : client.session.id }, 'Connection closed');
|
|
||||||
|
|
||||||
clientConns.removeClient(client);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('idle timeout', function idleTimeout() {
|
|
||||||
client.log.info('User idle timeout expired');
|
|
||||||
|
|
||||||
client.menuStack.goto('idleLogoff', function goMenuRes(err) {
|
|
||||||
if(err) {
|
|
||||||
// likely just doesn't exist
|
|
||||||
client.term.write('\nIdle timeout expired. Goodbye!\n');
|
|
||||||
client.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', function serverErr(err) {
|
|
||||||
logger.log.info(err); // 'close' should be handled after
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(port);
|
|
||||||
|
|
||||||
logger.log.info(
|
|
||||||
{ server : module.moduleInfo.name, port : port }, 'Listening for connections');
|
|
||||||
}, err => {
|
|
||||||
cb(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareClient(client, cb) {
|
|
||||||
const theme = require('./theme.js');
|
|
||||||
|
|
||||||
// :TODO: it feels like this should go somewhere else... and be a bit more elegant.
|
|
||||||
|
|
||||||
if('*' === conf.config.preLoginTheme) {
|
|
||||||
client.user.properties.theme_id = theme.getRandomTheme() || '';
|
|
||||||
} else {
|
|
||||||
client.user.properties.theme_id = conf.config.preLoginTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
theme.setClientTheme(client, client.user.properties.theme_id);
|
|
||||||
return cb(null); // note: currently useless to use cb here - but this may change...again...
|
|
||||||
}
|
|
|
@ -12,6 +12,7 @@ exports.getActiveConnections = getActiveConnections;
|
||||||
exports.getActiveNodeList = getActiveNodeList;
|
exports.getActiveNodeList = getActiveNodeList;
|
||||||
exports.addNewClient = addNewClient;
|
exports.addNewClient = addNewClient;
|
||||||
exports.removeClient = removeClient;
|
exports.removeClient = removeClient;
|
||||||
|
exports.getConnectionByUserId = getConnectionByUserId;
|
||||||
|
|
||||||
const clientConnections = [];
|
const clientConnections = [];
|
||||||
exports.clientConnections = clientConnections;
|
exports.clientConnections = clientConnections;
|
||||||
|
@ -93,3 +94,7 @@ function removeClient(client) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getConnectionByUserId(userId) {
|
||||||
|
return getActiveConnections().find( ac => userId === ac.user.userId );
|
||||||
|
}
|
|
@ -211,6 +211,23 @@ function getDefaultConfig() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contentServers : {
|
||||||
|
web : {
|
||||||
|
domain : 'another-fine-enigma-bbs.org',
|
||||||
|
|
||||||
|
http : {
|
||||||
|
enabled : false,
|
||||||
|
port : 8080,
|
||||||
|
},
|
||||||
|
https : {
|
||||||
|
enabled : false,
|
||||||
|
port : 8443,
|
||||||
|
certPem : paths.join(__dirname, './../misc/https_cert.pem'),
|
||||||
|
keyPem : paths.join(__dirname, './../misc/https_cert_key.pem'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
archives : {
|
archives : {
|
||||||
archivers : {
|
archivers : {
|
||||||
'7Zip' : {
|
'7Zip' : {
|
||||||
|
@ -362,6 +379,12 @@ function getDefaultConfig() {
|
||||||
// :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc.
|
// :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc.
|
||||||
],
|
],
|
||||||
|
|
||||||
|
web : {
|
||||||
|
path : '/f/',
|
||||||
|
routePath : '/f/[a-zA-Z0-9]+$',
|
||||||
|
expireMinutes : 1440, // 1 day
|
||||||
|
},
|
||||||
|
|
||||||
areas: {
|
areas: {
|
||||||
message_attachment : {
|
message_attachment : {
|
||||||
name : 'Message attachments',
|
name : 'Message attachments',
|
||||||
|
|
|
@ -40,11 +40,11 @@ function ConfigCache() {
|
||||||
this.gaze.on('changed', function fileChanged(filePath) {
|
this.gaze.on('changed', function fileChanged(filePath) {
|
||||||
assert(filePath in self.cache);
|
assert(filePath in self.cache);
|
||||||
|
|
||||||
Log.info( { filePath : filePath }, 'Configuration file changed; recaching');
|
Log.info( { path : filePath }, 'Configuration file changed; re-caching');
|
||||||
|
|
||||||
self.reCacheConfigFromFile(filePath, function reCached(err) {
|
self.reCacheConfigFromFile(filePath, function reCached(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
Log.error( { error : err, filePath : filePath } , 'Error recaching HJSON config');
|
Log.error( { error : err.message, path : filePath } , 'Failed re-caching configuration');
|
||||||
} else {
|
} else {
|
||||||
self.emit('recached', filePath);
|
self.emit('recached', filePath);
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,5 +333,12 @@ const DB_INIT_TABLE = {
|
||||||
UNIQUE(hash_tag_id, file_id)
|
UNIQUE(hash_tag_id, file_id)
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
dbs.file.run(
|
||||||
|
`CREATE TABLE IF NOT EXISTS file_web_serve (
|
||||||
|
hash_id VARCHAR NOT NULL PRIMARY KEY,
|
||||||
|
expire_timestamp DATETIME NOT NULL
|
||||||
|
);`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -4,22 +4,22 @@
|
||||||
const FileEntry = require('./file_entry.js');
|
const FileEntry = require('./file_entry.js');
|
||||||
|
|
||||||
module.exports = class DownloadQueue {
|
module.exports = class DownloadQueue {
|
||||||
constructor(user) {
|
constructor(client) {
|
||||||
this.user = user;
|
this.client = client;
|
||||||
|
|
||||||
this.user.downloadQueue = this.user.downloadQueue || [];
|
this.loadFromProperty(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle(fileEntry) {
|
toggle(fileEntry) {
|
||||||
if(this.isQueued(fileEntry)) {
|
if(this.isQueued(fileEntry)) {
|
||||||
this.user.downloadQueue = this.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
|
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId);
|
||||||
} else {
|
} else {
|
||||||
this.add(fileEntry);
|
this.add(fileEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add(fileEntry) {
|
add(fileEntry) {
|
||||||
this.user.downloadQueue.push({
|
this.client.user.downloadQueue.push({
|
||||||
fileId : fileEntry.fileId,
|
fileId : fileEntry.fileId,
|
||||||
areaTag : fileEntry.areaTag,
|
areaTag : fileEntry.areaTag,
|
||||||
fileName : fileEntry.fileName,
|
fileName : fileEntry.fileName,
|
||||||
|
@ -32,16 +32,18 @@ module.exports = class DownloadQueue {
|
||||||
entryOrId = entryOrId.fileId;
|
entryOrId = entryOrId.fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false;
|
return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
toProperty() { return JSON.stringify(this.user.downloadQueue); }
|
toProperty() { return JSON.stringify(this.client.user.downloadQueue); }
|
||||||
|
|
||||||
loadFromProperty(prop) {
|
loadFromProperty(prop) {
|
||||||
try {
|
try {
|
||||||
this.user.downloadQueue = JSON.parse(prop);
|
this.client.user.downloadQueue = JSON.parse(prop);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.user.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property');
|
this.client.user.downloadQueue = [];
|
||||||
|
|
||||||
|
this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,8 +21,9 @@ const iconv = require('iconv-lite');
|
||||||
|
|
||||||
exports.getAvailableFileAreas = getAvailableFileAreas;
|
exports.getAvailableFileAreas = getAvailableFileAreas;
|
||||||
exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas;
|
exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas;
|
||||||
exports.getDefaultFileAreaTag = getDefaultFileAreaTag;
|
exports.getDefaultFileAreaTag = getDefaultFileAreaTag;
|
||||||
exports.getFileAreaByTag = getFileAreaByTag;
|
exports.getFileAreaByTag = getFileAreaByTag;
|
||||||
|
exports.getFileEntryPath = getFileEntryPath;
|
||||||
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
|
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
|
||||||
//exports.addOrUpdateFileEntry = addOrUpdateFileEntry;
|
//exports.addOrUpdateFileEntry = addOrUpdateFileEntry;
|
||||||
exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
||||||
|
@ -46,15 +47,7 @@ function getAvailableFileAreas(client, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortedAvailableFileAreas(client, options) {
|
function getSortedAvailableFileAreas(client, options) {
|
||||||
const areas = _.map(getAvailableFileAreas(client, options), (v, k) => {
|
const areas = _.map(getAvailableFileAreas(client, options), v => v);
|
||||||
const areaInfo = {
|
|
||||||
areaTag : k,
|
|
||||||
area : v
|
|
||||||
};
|
|
||||||
|
|
||||||
return areaInfo;
|
|
||||||
});
|
|
||||||
|
|
||||||
sortAreasOrConfs(areas, 'area');
|
sortAreasOrConfs(areas, 'area');
|
||||||
return areas;
|
return areas;
|
||||||
}
|
}
|
||||||
|
@ -124,6 +117,13 @@ function getAreaStorageDirectory(areaInfo) {
|
||||||
return paths.join(Config.fileBase.areaStoragePrefix, areaInfo.storageDir || '');
|
return paths.join(Config.fileBase.areaStoragePrefix, areaInfo.storageDir || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFileEntryPath(fileEntry) {
|
||||||
|
const areaInfo = getFileAreaByTag(fileEntry.areaTag);
|
||||||
|
if(areaInfo) {
|
||||||
|
return paths.join(areaInfo.storageDirectory, fileEntry.fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getExistingFileEntriesBySha1(sha1, cb) {
|
function getExistingFileEntriesBySha1(sha1, cb) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const Config = require('./config.js').config;
|
||||||
|
const FileDb = require('./database.js').dbs.file;
|
||||||
|
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||||
|
const FileEntry = require('./file_entry.js');
|
||||||
|
const getServer = require('./listening_server.js').getServer;
|
||||||
|
const Errors = require('./enig_error.js').Errors;
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const hashids = require('hashids');
|
||||||
|
const moment = require('moment');
|
||||||
|
const paths = require('path');
|
||||||
|
const async = require('async');
|
||||||
|
const fs = require('fs');
|
||||||
|
const mimeTypes = require('mime-types');
|
||||||
|
|
||||||
|
const WEB_SERVER_PACKAGE_NAME = 'codes.l33t.enigma.web.server';
|
||||||
|
|
||||||
|
/*
|
||||||
|
:TODO:
|
||||||
|
* Load temp download URLs @ startup & set expire timers via scheduler.
|
||||||
|
* At creation, set expire timer via scheduler
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class FileAreaWebAccess {
|
||||||
|
constructor() {
|
||||||
|
this.hashids = new hashids(Config.general.boardName);
|
||||||
|
}
|
||||||
|
|
||||||
|
startup(cb) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function initFromDb(callback) {
|
||||||
|
// :TODO: Init from DB & register expiration timers
|
||||||
|
return callback(null);
|
||||||
|
},
|
||||||
|
function addWebRoute(callback) {
|
||||||
|
const webServer = getServer(WEB_SERVER_PACKAGE_NAME);
|
||||||
|
if(!webServer) {
|
||||||
|
return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeAdded = webServer.instance.addRoute({
|
||||||
|
method : 'GET',
|
||||||
|
path : '/f/[a-zA-Z0-9]+$', // :TODO: allow this to be configurable
|
||||||
|
handler : self.routeWebRequest.bind(self),
|
||||||
|
});
|
||||||
|
|
||||||
|
return callback(routeAdded ? null : Errors.General('Failed adding route'));
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown(cb) {
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
load(cb) {
|
||||||
|
return cb(null); // :TODO: Load from db
|
||||||
|
}
|
||||||
|
|
||||||
|
loadServedHashId(hashId, cb) {
|
||||||
|
FileDb.get(
|
||||||
|
`SELECT expire_timestamp FROM
|
||||||
|
file_web_serve
|
||||||
|
WHERE hash_id = ?`,
|
||||||
|
[ hashId ],
|
||||||
|
(err, result) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = this.hashids.decode(hashId);
|
||||||
|
if(!result || 2 !== decoded.length) {
|
||||||
|
return cb(Errors.Invalid('Invalid or unknown hash ID'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
hashId : hashId,
|
||||||
|
userId : decoded[0],
|
||||||
|
fileId : decoded[1],
|
||||||
|
expireTimestamp : moment(result.expire_timestamp),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHashId(client, fileEntry) {
|
||||||
|
//
|
||||||
|
// Hashid is a unique combination of userId & fileId
|
||||||
|
//
|
||||||
|
return this.hashids.encode(client.user.userId, fileEntry.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTempDownloadLink(client, fileEntry, hashId) {
|
||||||
|
hashId = hashId || this.getHashId(client, fileEntry);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a URL such as
|
||||||
|
// https://l33t.codes:44512/f/qFdxyZr
|
||||||
|
//
|
||||||
|
// :TODO: build from config
|
||||||
|
|
||||||
|
//
|
||||||
|
// Prefer HTTPS over HTTP. Be explicit about the port
|
||||||
|
// only if required.
|
||||||
|
//
|
||||||
|
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}${Config.fileBase.web.path}${hashId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
||||||
|
const hashId = this.getHashId(client, fileEntry);
|
||||||
|
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
servedItem.url = this.buildTempDownloadLink(client, fileEntry);
|
||||||
|
|
||||||
|
return cb(null, servedItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createAndServeTempDownload(client, fileEntry, options, cb) {
|
||||||
|
const hashId = this.getHashId(client, fileEntry);
|
||||||
|
const url = this.buildTempDownloadLink(client, fileEntry, hashId);
|
||||||
|
options.expireTime = options.expireTime || moment().add(2, 'days');
|
||||||
|
|
||||||
|
// add/update rec with hash id and (latest) timestamp
|
||||||
|
FileDb.run(
|
||||||
|
`REPLACE INTO file_web_serve (hash_id, expire_timestamp)
|
||||||
|
VALUES (?, ?);`,
|
||||||
|
[ hashId, getISOTimestampString(options.expireTime) ],
|
||||||
|
err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: setup tracking of expiration time so we can clean up the entry
|
||||||
|
|
||||||
|
return cb(null, url);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileNotFound(resp) {
|
||||||
|
resp.writeHead(404, { 'Content-Type' : 'text/html' } );
|
||||||
|
|
||||||
|
// :TODO: allow custom 404
|
||||||
|
return resp.end('<html><body>Not found</html>');
|
||||||
|
}
|
||||||
|
|
||||||
|
routeWebRequest(req, resp) {
|
||||||
|
const hashId = paths.basename(req.url);
|
||||||
|
|
||||||
|
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||||
|
|
||||||
|
if(err) {
|
||||||
|
return this.fileNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileEntry = new FileEntry();
|
||||||
|
fileEntry.load(servedItem.fileId, err => {
|
||||||
|
if(err) {
|
||||||
|
return this.fileNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = fileEntry.filePath;
|
||||||
|
if(!filePath) {
|
||||||
|
return this.fileNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.stat(filePath, (err, stats) => {
|
||||||
|
if(err) {
|
||||||
|
return this.fileNotFound(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.on('close', () => {
|
||||||
|
// connection closed *before* the response was fully sent
|
||||||
|
// :TODO: Log and such
|
||||||
|
});
|
||||||
|
|
||||||
|
resp.on('finish', () => {
|
||||||
|
// transfer completed fully
|
||||||
|
// :TODO: we need to update the users stats - bytes xferred, credit stuff, etc.
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type' : mimeTypes.contentType(paths.extname(filePath)) || mimeTypes.contentType('.bin'),
|
||||||
|
'Content-Length' : stats.size,
|
||||||
|
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const readStream = fs.createReadStream(filePath);
|
||||||
|
resp.writeHead(200, headers);
|
||||||
|
return readStream.pipe(resp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new FileAreaWebAccess();
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
const uuids = require('node-uuid');
|
||||||
|
|
||||||
|
module.exports = class FileBaseFilters {
|
||||||
|
constructor(client) {
|
||||||
|
this.client = client;
|
||||||
|
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get OrderByValues() {
|
||||||
|
return [ 'ascending', 'descending' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get SortByValues() {
|
||||||
|
return [
|
||||||
|
'upload_timestamp',
|
||||||
|
'upload_by_username',
|
||||||
|
'dl_count',
|
||||||
|
'user_rating',
|
||||||
|
'est_release_year',
|
||||||
|
'byte_size',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray() {
|
||||||
|
return _.map(this.filters, (filter, uuid) => {
|
||||||
|
return Object.assign( { uuid : uuid }, filter );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get(filterUuid) {
|
||||||
|
return this.filters[filterUuid];
|
||||||
|
}
|
||||||
|
|
||||||
|
add(filterInfo) {
|
||||||
|
const filterUuid = uuids.v4();
|
||||||
|
|
||||||
|
filterInfo.tags = this.cleanTags(filterInfo.tags);
|
||||||
|
|
||||||
|
this.filters[filterUuid] = filterInfo;
|
||||||
|
|
||||||
|
return filterUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(filterUuid) {
|
||||||
|
delete this.filters[filterUuid];
|
||||||
|
}
|
||||||
|
|
||||||
|
load(prop) {
|
||||||
|
prop = prop || this.client.user.properties.file_base_filters;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.filters = JSON.parse(prop);
|
||||||
|
} catch(e) {
|
||||||
|
this.filters = {};
|
||||||
|
|
||||||
|
this.client.log.error( { error : e.message, property : prop }, 'Failed parsing file base filters property' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
persist(cb) {
|
||||||
|
return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanTags(tags) {
|
||||||
|
return tags.toLowerCase().replace(/,?\s+|\,/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setActive(filterUuid) {
|
||||||
|
const activeFilter = this.get(filterUuid);
|
||||||
|
|
||||||
|
if(activeFilter) {
|
||||||
|
this.activeFilter = activeFilter;
|
||||||
|
this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getActiveFilter(client) {
|
||||||
|
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid);
|
||||||
|
}
|
||||||
|
};
|
|
@ -3,11 +3,13 @@
|
||||||
|
|
||||||
const fileDb = require('./database.js').dbs.file;
|
const fileDb = require('./database.js').dbs.file;
|
||||||
const Errors = require('./enig_error.js').Errors;
|
const Errors = require('./enig_error.js').Errors;
|
||||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||||
|
const Config = require('./config.js').config;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const paths = require('path');
|
||||||
|
|
||||||
const FILE_TABLE_MEMBERS = [
|
const FILE_TABLE_MEMBERS = [
|
||||||
'file_id', 'area_tag', 'file_sha1', 'file_name',
|
'file_id', 'area_tag', 'file_sha1', 'file_name',
|
||||||
|
@ -130,6 +132,17 @@ module.exports = class FileEntry {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get filePath() {
|
||||||
|
const areaInfo = Config.fileAreas.areas[this.areaTag];
|
||||||
|
if(areaInfo) {
|
||||||
|
return paths.join(
|
||||||
|
Config.fileBase.areaStoragePrefix,
|
||||||
|
areaInfo.storageDir || '',
|
||||||
|
this.fileName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static persistMetaValue(fileId, name, value, cb) {
|
static persistMetaValue(fileId, name, value, cb) {
|
||||||
fileDb.run(
|
fileDb.run(
|
||||||
`REPLACE INTO file_meta (file_id, meta_name, meta_value)
|
`REPLACE INTO file_meta (file_id, meta_name, meta_value)
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const logger = require('./logger.js');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const listeningServers = {}; // packageName -> info
|
||||||
|
|
||||||
|
exports.startup = startup;
|
||||||
|
exports.shutdown = shutdown;
|
||||||
|
exports.getServer = getServer;
|
||||||
|
|
||||||
|
function startup(cb) {
|
||||||
|
return startListening(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(cb) {
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServer(packageName) {
|
||||||
|
return listeningServers[packageName];
|
||||||
|
}
|
||||||
|
|
||||||
|
function startListening(cb) {
|
||||||
|
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
||||||
|
|
||||||
|
async.each( [ 'login', 'content' ], (category, next) => {
|
||||||
|
moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => {
|
||||||
|
// :TODO: use enig error here!
|
||||||
|
if(err) {
|
||||||
|
if('EENIGMODDISABLED' === err.code) {
|
||||||
|
logger.log.debug(err.message);
|
||||||
|
} else {
|
||||||
|
logger.log.info( { err : err }, 'Failed loading module');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleInst = new module.getModule();
|
||||||
|
try {
|
||||||
|
moduleInst.createServer();
|
||||||
|
if(!moduleInst.listen()) {
|
||||||
|
throw new Error('Failed listening');
|
||||||
|
}
|
||||||
|
|
||||||
|
listeningServers[module.moduleInfo.packageName] = {
|
||||||
|
instance : moduleInst,
|
||||||
|
info : module.moduleInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
logger.log.error(e, 'Exception caught creating server!');
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
}, err => {
|
||||||
|
return cb(err);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const conf = require('./config.js');
|
||||||
|
const logger = require('./logger.js');
|
||||||
|
const ServerModule = require('./server_module.js').ServerModule;
|
||||||
|
const clientConns = require('./client_connections.js');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
module.exports = class LoginServerModule extends ServerModule {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// :TODO: we need to max connections -- e.g. from config 'maxConnections'
|
||||||
|
|
||||||
|
prepareClient(client, cb) {
|
||||||
|
const theme = require('./theme.js');
|
||||||
|
|
||||||
|
//
|
||||||
|
// Choose initial theme before we have user context
|
||||||
|
//
|
||||||
|
if('*' === conf.config.preLoginTheme) {
|
||||||
|
client.user.properties.theme_id = theme.getRandomTheme() || '';
|
||||||
|
} else {
|
||||||
|
client.user.properties.theme_id = conf.config.preLoginTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.setClientTheme(client, client.user.properties.theme_id);
|
||||||
|
return cb(null); // note: currently useless to use cb here - but this may change...again...
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewClient(client, clientSock, modInfo) {
|
||||||
|
//
|
||||||
|
// Start tracking the client. We'll assign it an ID which is
|
||||||
|
// just the index in our connections array.
|
||||||
|
//
|
||||||
|
if(_.isUndefined(client.session)) {
|
||||||
|
client.session = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
client.session.serverName = modInfo.name;
|
||||||
|
client.session.isSecure = modInfo.isSecure || false;
|
||||||
|
|
||||||
|
clientConns.addNewClient(client, clientSock);
|
||||||
|
|
||||||
|
client.on('ready', readyOptions => {
|
||||||
|
|
||||||
|
client.startIdleMonitor();
|
||||||
|
|
||||||
|
// Go to module -- use default error handler
|
||||||
|
this.prepareClient(client, () => {
|
||||||
|
require('./connect.js').connectEntry(client, readyOptions.firstMenu);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('end', () => {
|
||||||
|
clientConns.removeClient(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', err => {
|
||||||
|
logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('close', err => {
|
||||||
|
const logFunc = err ? logger.log.info : logger.log.debug;
|
||||||
|
logFunc( { clientId : client.session.id }, 'Connection closed');
|
||||||
|
|
||||||
|
clientConns.removeClient(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('idle timeout', () => {
|
||||||
|
client.log.info('User idle timeout expired');
|
||||||
|
|
||||||
|
client.menuStack.goto('idleLogoff', err => {
|
||||||
|
if(err) {
|
||||||
|
// likely just doesn't exist
|
||||||
|
client.term.write('\nIdle timeout expired. Goodbye!\n');
|
||||||
|
client.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -59,9 +59,6 @@ function loadModuleEx(options, cb) {
|
||||||
return cb(new Error('Invalid or missing "getModule" method for module!'));
|
return cb(new Error('Invalid or missing "getModule" method for module!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ref configuration, if any, for convience to the module
|
|
||||||
mod.runtime = { config : modConfig };
|
|
||||||
|
|
||||||
return cb(null, mod);
|
return cb(null, mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +86,7 @@ function loadModulesForCategory(category, iterator, complete) {
|
||||||
});
|
});
|
||||||
|
|
||||||
async.each(jsModules, (file, next) => {
|
async.each(jsModules, (file, next) => {
|
||||||
loadModule(paths.basename(file, '.js'), category, (err, mod) => {
|
+ loadModule(paths.basename(file, '.js'), category, (err, mod) => {
|
||||||
iterator(err, mod);
|
iterator(err, mod);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
|
||||||
const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag;
|
const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag;
|
||||||
const clientConnections = require('./client_connections.js');
|
const clientConnections = require('./client_connections.js');
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
|
const FileBaseFilters = require('./file_base_filter.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const packageJson = require('../package.json');
|
const packageJson = require('../package.json');
|
||||||
|
@ -80,6 +81,10 @@ function getPredefinedMCIValue(client, code) {
|
||||||
ND : function connectedNode() { return client.node.toString(); },
|
ND : function connectedNode() { return client.node.toString(); },
|
||||||
IP : function clientIpAddress() { return client.address().address; },
|
IP : function clientIpAddress() { return client.address().address; },
|
||||||
ST : function serverName() { return client.session.serverName; },
|
ST : function serverName() { return client.session.serverName; },
|
||||||
|
FN : function activeFileBaseFilterName() {
|
||||||
|
const activeFilter = FileBaseFilters.getActiveFilter(client);
|
||||||
|
return activeFilter ? activeFilter.name : '';
|
||||||
|
},
|
||||||
|
|
||||||
MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
|
MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
|
||||||
CS : function currentStatus() { return client.currentStatus; },
|
CS : function currentStatus() { return client.currentStatus; },
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const Log = require('../../logger.js').log;
|
||||||
|
const ServerModule = require('../../server_module.js').ServerModule;
|
||||||
|
const Config = require('../../config.js').config;
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const ModuleInfo = exports.moduleInfo = {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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); }
|
||||||
|
|
||||||
|
getRouteKey() { return `${this.method}:${this.path}`; }
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getModule = class WebServerModule extends ServerModule {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.enableHttp = Config.contentServers.web.http.enabled || true;
|
||||||
|
this.enableHttps = Config.contentServers.web.https.enabled || false;
|
||||||
|
|
||||||
|
this.routes = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
createServer() {
|
||||||
|
if(this.enableHttp) {
|
||||||
|
this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.enableHttps) {
|
||||||
|
const options = {
|
||||||
|
cert : fs.readFileSync(Config.contentServers.web.https.certPem),
|
||||||
|
key : fs.readFileSync(Config.contentServers.web.https.keyPem),
|
||||||
|
};
|
||||||
|
|
||||||
|
// additional options
|
||||||
|
Object.assign(options, Config.contentServers.web.https.options || {} );
|
||||||
|
|
||||||
|
this.httpsServer = https.createServer(options, this.routeRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
let ok = true;
|
||||||
|
|
||||||
|
[ '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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
addRoute(route) {
|
||||||
|
route = new Route(route);
|
||||||
|
|
||||||
|
if(!route.isValid()) {
|
||||||
|
Log( { route : route }, 'Cannot add route: missing or invalid required members' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeKey = route.getRouteKey();
|
||||||
|
if(routeKey in this.routes) {
|
||||||
|
Log( { route : route }, 'Cannot add route: duplicate method/path combination exists' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.routes[routeKey] = route;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
routeRequest(req, resp) {
|
||||||
|
const route = _.find(this.routes, r => r.matchesRequest(req) );
|
||||||
|
return route ? route.handler(req, resp) : this.accessDenied(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
accessDenied(resp) {
|
||||||
|
resp.writeHead(401, { 'Content-Type' : 'text/html' } );
|
||||||
|
return resp.end('<html><body>Access denied</body></html>');
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,14 +2,14 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const Config = require('../../config.js').config;
|
const Config = require('../../config.js').config;
|
||||||
const baseClient = require('../../client.js');
|
const baseClient = require('../../client.js');
|
||||||
const Log = require('../../logger.js').log;
|
const Log = require('../../logger.js').log;
|
||||||
const ServerModule = require('../../server_module.js').ServerModule;
|
const LoginServerModule = require('../../login_server_module.js');
|
||||||
const userLogin = require('../../user_login.js').userLogin;
|
const userLogin = require('../../user_login.js').userLogin;
|
||||||
const enigVersion = require('../../../package.json').version;
|
const enigVersion = require('../../../package.json').version;
|
||||||
const theme = require('../../theme.js');
|
const theme = require('../../theme.js');
|
||||||
const stringFormat = require('../../string_format.js');
|
const stringFormat = require('../../string_format.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const ssh2 = require('ssh2');
|
const ssh2 = require('ssh2');
|
||||||
|
@ -18,15 +18,14 @@ const util = require('util');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
const ModuleInfo = exports.moduleInfo = {
|
||||||
name : 'SSH',
|
name : 'SSH',
|
||||||
desc : 'SSH Server',
|
desc : 'SSH Server',
|
||||||
author : 'NuSkooler',
|
author : 'NuSkooler',
|
||||||
isSecure : true,
|
isSecure : true,
|
||||||
|
packageName : 'codes.l33t.enigma.ssh.server',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = SSHServerModule;
|
|
||||||
|
|
||||||
function SSHClient(clientConn) {
|
function SSHClient(clientConn) {
|
||||||
baseClient.Client.apply(this, arguments);
|
baseClient.Client.apply(this, arguments);
|
||||||
|
|
||||||
|
@ -226,40 +225,45 @@ util.inherits(SSHClient, baseClient.Client);
|
||||||
|
|
||||||
SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ];
|
SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ];
|
||||||
|
|
||||||
function SSHServerModule() {
|
exports.getModule = class SSHServerModule extends LoginServerModule {
|
||||||
ServerModule.call(this);
|
constructor() {
|
||||||
}
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
util.inherits(SSHServerModule, ServerModule);
|
createServer() {
|
||||||
|
const serverConf = {
|
||||||
|
hostKeys : [
|
||||||
|
{
|
||||||
|
key : fs.readFileSync(Config.loginServers.ssh.privateKeyPem),
|
||||||
|
passphrase : Config.loginServers.ssh.privateKeyPass,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
ident : 'enigma-bbs-' + enigVersion + '-srv',
|
||||||
|
|
||||||
|
// Note that sending 'banner' breaks at least EtherTerm!
|
||||||
|
debug : (sshDebugLine) => {
|
||||||
|
if(true === Config.loginServers.ssh.traceConnections) {
|
||||||
|
Log.trace(`SSH: ${sshDebugLine}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
SSHServerModule.prototype.createServer = function() {
|
this.server = ssh2.Server(serverConf);
|
||||||
SSHServerModule.super_.prototype.createServer.call(this);
|
this.server.on('connection', (conn, info) => {
|
||||||
|
Log.info(info, 'New SSH connection');
|
||||||
|
this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const serverConf = {
|
listen() {
|
||||||
hostKeys : [
|
const port = parseInt(Config.loginServers.ssh.port);
|
||||||
{
|
if(isNaN(port)) {
|
||||||
key : fs.readFileSync(Config.loginServers.ssh.privateKeyPem),
|
Log.error( { server : ModuleInfo.name, port : Config.loginServers.ssh.port }, 'Cannot load server (invalid port)' );
|
||||||
passphrase : Config.loginServers.ssh.privateKeyPass,
|
return false;
|
||||||
}
|
}
|
||||||
],
|
|
||||||
ident : 'enigma-bbs-' + enigVersion + '-srv',
|
|
||||||
|
|
||||||
// Note that sending 'banner' breaks at least EtherTerm!
|
|
||||||
debug : (sshDebugLine) => {
|
|
||||||
if(true === Config.loginServers.ssh.traceConnections) {
|
|
||||||
Log.trace(`SSH: ${sshDebugLine}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const server = ssh2.Server(serverConf);
|
this.server.listen(port);
|
||||||
server.on('connection', function onConnection(conn, info) {
|
Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' );
|
||||||
Log.info(info, 'New SSH connection');
|
return true;
|
||||||
|
}
|
||||||
const client = new SSHClient(conn);
|
|
||||||
|
|
||||||
this.emit('client', client, conn._sock);
|
|
||||||
});
|
|
||||||
|
|
||||||
return server;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const baseClient = require('../../client.js');
|
const baseClient = require('../../client.js');
|
||||||
const Log = require('../../logger.js').log;
|
const Log = require('../../logger.js').log;
|
||||||
const ServerModule = require('../../server_module.js').ServerModule;
|
const LoginServerModule = require('../../login_server_module.js');
|
||||||
const Config = require('../../config.js').config;
|
const Config = require('../../config.js').config;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
|
@ -16,16 +16,14 @@ const util = require('util');
|
||||||
|
|
||||||
//var debug = require('debug')('telnet');
|
//var debug = require('debug')('telnet');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
const ModuleInfo = exports.moduleInfo = {
|
||||||
name : 'Telnet',
|
name : 'Telnet',
|
||||||
desc : 'Telnet Server',
|
desc : 'Telnet Server',
|
||||||
author : 'NuSkooler',
|
author : 'NuSkooler',
|
||||||
isSecure : false,
|
isSecure : false,
|
||||||
|
packageName : 'codes.l33t.enigma.telnet.server',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getModule = TelnetServerModule;
|
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Telnet Protocol Resources
|
// Telnet Protocol Resources
|
||||||
// * http://pcmicro.com/netfoss/telnet.html
|
// * http://pcmicro.com/netfoss/telnet.html
|
||||||
|
@ -767,22 +765,34 @@ Object.keys(OPTIONS).forEach(function(name) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function TelnetServerModule() {
|
exports.getModule = class TelnetServerModule extends LoginServerModule {
|
||||||
ServerModule.call(this);
|
constructor() {
|
||||||
}
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
util.inherits(TelnetServerModule, ServerModule);
|
createServer() {
|
||||||
|
this.server = net.createServer( sock => {
|
||||||
|
const client = new TelnetClient(sock, sock);
|
||||||
|
|
||||||
TelnetServerModule.prototype.createServer = function() {
|
client.banner();
|
||||||
TelnetServerModule.super_.prototype.createServer.call(this);
|
|
||||||
|
|
||||||
const server = net.createServer( (sock) => {
|
this.handleNewClient(client, sock, ModuleInfo);
|
||||||
const client = new TelnetClient(sock, sock);
|
});
|
||||||
|
|
||||||
client.banner();
|
|
||||||
|
|
||||||
server.emit('client', client, sock);
|
this.server.on('error', err => {
|
||||||
});
|
Log.info( { error : err.message }, 'Telnet server error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return server;
|
listen() {
|
||||||
|
const port = parseInt(Config.loginServers.telnet.port);
|
||||||
|
if(isNaN(port)) {
|
||||||
|
Log.error( { server : ModuleInfo.name, port : Config.loginServers.telnet.port }, 'Cannot load server (invalid port)' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server.listen(port);
|
||||||
|
Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const setClientTheme = require('./theme.js').setClientTheme;
|
const setClientTheme = require('./theme.js').setClientTheme;
|
||||||
const clientConnections = require('./client_connections.js').clientConnections;
|
const clientConnections = require('./client_connections.js').clientConnections;
|
||||||
const userDb = require('./database.js').dbs.user;
|
|
||||||
const StatLog = require('./stat_log.js');
|
const StatLog = require('./stat_log.js');
|
||||||
const logger = require('./logger.js');
|
const logger = require('./logger.js');
|
||||||
|
|
||||||
|
@ -21,66 +20,64 @@ function userLogin(client, username, password, cb) {
|
||||||
// :TODO: if username exists, record failed login attempt to properties
|
// :TODO: if username exists, record failed login attempt to properties
|
||||||
// :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true
|
// :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true
|
||||||
|
|
||||||
cb(err);
|
return cb(err);
|
||||||
} else {
|
|
||||||
const now = new Date();
|
|
||||||
const user = client.user;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Ensure this user is not already logged in.
|
|
||||||
// Loop through active connections -- which includes the current --
|
|
||||||
// and check for matching user ID. If the count is > 1, disallow.
|
|
||||||
//
|
|
||||||
var existingClientConnection;
|
|
||||||
clientConnections.forEach(function connEntry(cc) {
|
|
||||||
if(cc.user !== user && cc.user.userId === user.userId) {
|
|
||||||
existingClientConnection = cc;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(existingClientConnection) {
|
|
||||||
client.log.info( {
|
|
||||||
existingClientId : existingClientConnection.session.id,
|
|
||||||
username : user.username,
|
|
||||||
userId : user.userId },
|
|
||||||
'Already logged in'
|
|
||||||
);
|
|
||||||
|
|
||||||
var existingConnError = new Error('Already logged in as supplied user');
|
|
||||||
existingConnError.existingConn = true;
|
|
||||||
|
|
||||||
return cb(existingClientConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// update client logger with addition of username
|
|
||||||
client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username });
|
|
||||||
client.log.info('Successful login');
|
|
||||||
|
|
||||||
async.parallel(
|
|
||||||
[
|
|
||||||
function setTheme(callback) {
|
|
||||||
setClientTheme(client, user.properties.theme_id);
|
|
||||||
callback(null);
|
|
||||||
},
|
|
||||||
function updateSystemLoginCount(callback) {
|
|
||||||
StatLog.incrementSystemStat('login_count', 1, callback);
|
|
||||||
},
|
|
||||||
function recordLastLogin(callback) {
|
|
||||||
StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback);
|
|
||||||
},
|
|
||||||
function updateUserLoginCount(callback) {
|
|
||||||
StatLog.incrementUserStat(user, 'login_count', 1, callback);
|
|
||||||
},
|
|
||||||
function recordLoginHistory(callback) {
|
|
||||||
const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers
|
|
||||||
StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
function complete(err) {
|
|
||||||
cb(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const user = client.user;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Ensure this user is not already logged in.
|
||||||
|
// Loop through active connections -- which includes the current --
|
||||||
|
// and check for matching user ID. If the count is > 1, disallow.
|
||||||
|
//
|
||||||
|
let existingClientConnection =
|
||||||
|
clientConnections.forEach(function connEntry(cc) {
|
||||||
|
if(cc.user !== user && cc.user.userId === user.userId) {
|
||||||
|
existingClientConnection = cc;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(existingClientConnection) {
|
||||||
|
client.log.info( {
|
||||||
|
existingClientId : existingClientConnection.session.id,
|
||||||
|
username : user.username,
|
||||||
|
userId : user.userId },
|
||||||
|
'Already logged in'
|
||||||
|
);
|
||||||
|
|
||||||
|
var existingConnError = new Error('Already logged in as supplied user');
|
||||||
|
existingConnError.existingConn = true;
|
||||||
|
|
||||||
|
return cb(existingClientConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// update client logger with addition of username
|
||||||
|
client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username });
|
||||||
|
client.log.info('Successful login');
|
||||||
|
|
||||||
|
async.parallel(
|
||||||
|
[
|
||||||
|
function setTheme(callback) {
|
||||||
|
setClientTheme(client, user.properties.theme_id);
|
||||||
|
callback(null);
|
||||||
|
},
|
||||||
|
function updateSystemLoginCount(callback) {
|
||||||
|
StatLog.incrementSystemStat('login_count', 1, callback);
|
||||||
|
},
|
||||||
|
function recordLastLogin(callback) {
|
||||||
|
StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback);
|
||||||
|
},
|
||||||
|
function updateUserLoginCount(callback) {
|
||||||
|
StatLog.incrementUserStat(user, 'login_count', 1, callback);
|
||||||
|
},
|
||||||
|
function recordLoginHistory(callback) {
|
||||||
|
const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers
|
||||||
|
StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
function complete(err) {
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -6,7 +6,6 @@ var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory;
|
||||||
var menuUtil = require('./menu_util.js');
|
var menuUtil = require('./menu_util.js');
|
||||||
var asset = require('./asset.js');
|
var asset = require('./asset.js');
|
||||||
var ansi = require('./ansi_term.js');
|
var ansi = require('./ansi_term.js');
|
||||||
const Log = require('./logger.js');
|
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
var events = require('events');
|
var events = require('events');
|
||||||
|
@ -449,6 +448,12 @@ ViewController.prototype.setFocus = function(focused) {
|
||||||
this.setViewFocusWithEvents(this.focusedView, focused);
|
this.setViewFocusWithEvents(this.focusedView, focused);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ViewController.prototype.resetInitialFocus = function() {
|
||||||
|
if(this.formInitialFocusId) {
|
||||||
|
return this.switchFocus(this.formInitialFocusId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ViewController.prototype.switchFocus = function(id) {
|
ViewController.prototype.switchFocus = function(id) {
|
||||||
//
|
//
|
||||||
// Perform focus switching validation now
|
// Perform focus switching validation now
|
||||||
|
@ -618,7 +623,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var formIdKey = options.formId ? options.formId.toString() : '0';
|
var formIdKey = options.formId ? options.formId.toString() : '0';
|
||||||
var initialFocusId = 1; // default to first
|
this.formInitialFocusId = 1; // default to first
|
||||||
var formConfig;
|
var formConfig;
|
||||||
|
|
||||||
// :TODO: honor options.withoutForm
|
// :TODO: honor options.withoutForm
|
||||||
|
@ -671,7 +676,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
||||||
function applyViewConfiguration(callback) {
|
function applyViewConfiguration(callback) {
|
||||||
if(_.isObject(formConfig)) {
|
if(_.isObject(formConfig)) {
|
||||||
self.applyViewConfig(formConfig, function configApplied(err, info) {
|
self.applyViewConfig(formConfig, function configApplied(err, info) {
|
||||||
initialFocusId = info.initialFocusId;
|
self.formInitialFocusId = info.initialFocusId;
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -746,12 +751,12 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) {
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
function drawAllViews(callback) {
|
function drawAllViews(callback) {
|
||||||
self.redrawAll(initialFocusId);
|
self.redrawAll(self.formInitialFocusId);
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
function setInitialViewFocus(callback) {
|
function setInitialViewFocus(callback) {
|
||||||
if(initialFocusId) {
|
if(self.formInitialFocusId) {
|
||||||
self.switchFocus(initialFocusId);
|
self.switchFocus(self.formInitialFocusId);
|
||||||
}
|
}
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
|
@ -794,7 +799,7 @@ ViewController.prototype.getFormData = function(key) {
|
||||||
|
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
var formData = {
|
const formData = {
|
||||||
id : this.formId,
|
id : this.formId,
|
||||||
submitId : this.focusedView.id,
|
submitId : this.focusedView.id,
|
||||||
value : {},
|
value : {},
|
||||||
|
@ -804,6 +809,26 @@ ViewController.prototype.getFormData = function(key) {
|
||||||
formData.key = key;
|
formData.key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viewData;
|
||||||
|
_.each(this.views, view => {
|
||||||
|
try {
|
||||||
|
// don't fill forms with static, non user-editable data data
|
||||||
|
if(!view.acceptsInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewData = view.getData();
|
||||||
|
if(_.isUndefined(viewData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.value[ view.submitArgName ? view.submitArgName : view.id ] = viewData;
|
||||||
|
} catch(e) {
|
||||||
|
this.client.log.error( { error : e.message }, 'Exception caught gathering form data' );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
|
||||||
var viewData;
|
var viewData;
|
||||||
var view;
|
var view;
|
||||||
for(var id in this.views) {
|
for(var id in this.views) {
|
||||||
|
@ -820,10 +845,10 @@ ViewController.prototype.getFormData = function(key) {
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.client.log.error(e); // :TODO: Log better ;)
|
this.client.log.error(e); // :TODO: Log better ;)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
}
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
ViewController.prototype.formatMenuArgs = function(args) {
|
ViewController.prototype.formatMenuArgs = function(args) {
|
||||||
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
/* jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ENiGMA½
|
||||||
|
const MenuModule = require('../core/menu_module.js').MenuModule;
|
||||||
|
const ViewController = require('../core/view_controller.js').ViewController;
|
||||||
|
const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas;
|
||||||
|
const FileBaseFilters = require('../core/file_base_filter.js');
|
||||||
|
const stringFormat = require('../core/string_format.js');
|
||||||
|
|
||||||
|
// deps
|
||||||
|
const async = require('async');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
exports.moduleInfo = {
|
||||||
|
name : 'File Area Filter Editor',
|
||||||
|
desc : 'Module for adding, deleting, and modifying file base filters',
|
||||||
|
author : 'NuSkooler',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MciViewIds = {
|
||||||
|
editor : {
|
||||||
|
searchTerms : 1,
|
||||||
|
tags : 2,
|
||||||
|
area : 3,
|
||||||
|
sort : 4,
|
||||||
|
order : 5,
|
||||||
|
filterName : 6,
|
||||||
|
navMenu : 7,
|
||||||
|
|
||||||
|
selectedFilterInfo : 10, // { ...filter object ... }
|
||||||
|
activeFilterInfo : 11, // { ...filter object ... }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getModule = class FileAreaFilterEdit extends MenuModule {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
|
||||||
|
this.currentFilterIndex = 0; // into |filtersArray|
|
||||||
|
|
||||||
|
this.menuMethods = {
|
||||||
|
saveFilter : (formData, extraArgs, cb) => {
|
||||||
|
return this.saveCurrentFilter(formData, cb);
|
||||||
|
|
||||||
|
},
|
||||||
|
prevFilter : (formData, extraArgs, cb) => {
|
||||||
|
this.currentFilterIndex -= 1;
|
||||||
|
if(this.currentFilterIndex < 0) {
|
||||||
|
this.currentFilterIndex = this.filtersArray.length - 1;
|
||||||
|
}
|
||||||
|
this.loadDataForFilter(this.currentFilterIndex);
|
||||||
|
return cb(null);
|
||||||
|
},
|
||||||
|
nextFilter : (formData, extraArgs, cb) => {
|
||||||
|
this.currentFilterIndex += 1;
|
||||||
|
if(this.currentFilterIndex >= this.filtersArray.length) {
|
||||||
|
this.currentFilterIndex = 0;
|
||||||
|
}
|
||||||
|
this.loadDataForFilter(this.currentFilterIndex);
|
||||||
|
return cb(null);
|
||||||
|
},
|
||||||
|
makeFilterActive : (formData, extraArgs, cb) => {
|
||||||
|
const filters = new FileBaseFilters(this.client);
|
||||||
|
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
|
||||||
|
|
||||||
|
this.updateActiveLabel();
|
||||||
|
|
||||||
|
// :TODO: Need to update %FN somehow
|
||||||
|
return cb(null);
|
||||||
|
},
|
||||||
|
newFilter : (formData, extraArgs, cb) => {
|
||||||
|
this.currentFilterIndex = this.filtersArray.length; // next avail slot
|
||||||
|
this.clearForm(true); // true=reset focus
|
||||||
|
return cb(null);
|
||||||
|
},
|
||||||
|
deleteFilter : (formData, extraArgs, cb) => {
|
||||||
|
const filterUuid = this.filtersArray[this.currentFilterIndex].uuid;
|
||||||
|
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
|
||||||
|
|
||||||
|
// remove from stored properties
|
||||||
|
const filters = new FileBaseFilters(this.client);
|
||||||
|
filters.remove(filterUuid);
|
||||||
|
filters.persist( () => {
|
||||||
|
|
||||||
|
//
|
||||||
|
// If the item was also the active filter, we need to make a new one active
|
||||||
|
//
|
||||||
|
if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) {
|
||||||
|
const newActive = this.filtersArray[this.currentFilterIndex];
|
||||||
|
if(newActive) {
|
||||||
|
filters.setActive(newActive.uuid);
|
||||||
|
} else {
|
||||||
|
// nothing to set active to
|
||||||
|
// :TODO: is this what we want?
|
||||||
|
this.client.user.properties.file_base_filter_active_uuid = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
if(this.filtersArray.length > 0) {
|
||||||
|
this.loadDataForFilter(this.currentFilterIndex);
|
||||||
|
} else {
|
||||||
|
this.clearForm(true); // true=reset focus
|
||||||
|
}
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mciReady(mciData, cb) {
|
||||||
|
super.mciReady(mciData, err => {
|
||||||
|
if(err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) );
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function loadFromConfig(callback) {
|
||||||
|
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback);
|
||||||
|
},
|
||||||
|
function populateAreas(callback) {
|
||||||
|
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []);
|
||||||
|
|
||||||
|
const areasView = vc.getView(MciViewIds.editor.area);
|
||||||
|
if(areasView) {
|
||||||
|
areasView.setItems( self.availAreas.map( a => a.name ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateActiveLabel();
|
||||||
|
self.loadDataForFilter(self.currentFilterIndex);
|
||||||
|
self.viewControllers.editor.resetInitialFocus();
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentFilter() {
|
||||||
|
return this.filtersArray[this.currentFilterIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(mciId, text) {
|
||||||
|
const view = this.viewControllers.editor.getView(mciId);
|
||||||
|
if(view) {
|
||||||
|
view.setText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActiveLabel() {
|
||||||
|
const activeFilter = FileBaseFilters.getActiveFilter(this.client);
|
||||||
|
if(activeFilter) {
|
||||||
|
const activeFormat = this.menuConfig.config.activeFormat || '{name}';
|
||||||
|
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocusItemIndex(mciId, index) {
|
||||||
|
const view = this.viewControllers.editor.getView(mciId);
|
||||||
|
if(view) {
|
||||||
|
view.setFocusItemIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearForm(setFocus) {
|
||||||
|
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => {
|
||||||
|
this.setText(mciId, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => {
|
||||||
|
this.setFocusItemIndex(mciId, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(setFocus) {
|
||||||
|
this.viewControllers.editor.resetInitialFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedAreaTag(index) {
|
||||||
|
if(0 === index) {
|
||||||
|
return ''; // -ALL-
|
||||||
|
}
|
||||||
|
const area = this.availAreas[index];
|
||||||
|
if(!area) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return area.areaTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrderBy(index) {
|
||||||
|
return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
setAreaIndexFromCurrentFilter() {
|
||||||
|
let index;
|
||||||
|
const filter = this.getCurrentFilter();
|
||||||
|
if(filter) {
|
||||||
|
// special treatment: areaTag saved as blank ("") if -ALL-
|
||||||
|
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0;
|
||||||
|
} else {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
this.setFocusItemIndex(MciViewIds.editor.area, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderByFromCurrentFilter() {
|
||||||
|
let index;
|
||||||
|
const filter = this.getCurrentFilter();
|
||||||
|
if(filter) {
|
||||||
|
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0;
|
||||||
|
} else {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
this.setFocusItemIndex(MciViewIds.editor.order, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortByFromCurrentFilter() {
|
||||||
|
let index;
|
||||||
|
const filter = this.getCurrentFilter();
|
||||||
|
if(filter) {
|
||||||
|
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0;
|
||||||
|
} else {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
this.setFocusItemIndex(MciViewIds.editor.sort, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortBy(index) {
|
||||||
|
return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilterValuesFromFormData(filter, formData) {
|
||||||
|
filter.name = formData.value.name;
|
||||||
|
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
|
||||||
|
filter.terms = formData.value.searchTerms;
|
||||||
|
filter.tags = formData.value.tags;
|
||||||
|
filter.order = this.getOrderBy(formData.value.orderByIndex);
|
||||||
|
filter.sort = this.getSortBy(formData.value.sortByIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCurrentFilter(formData, cb) {
|
||||||
|
const filters = new FileBaseFilters(this.client);
|
||||||
|
const selectedFilter = this.filtersArray[this.currentFilterIndex];
|
||||||
|
if(selectedFilter) {
|
||||||
|
// *update* currently selected filter
|
||||||
|
this.setFilterValuesFromFormData(selectedFilter, formData);
|
||||||
|
} else {
|
||||||
|
// add a new entry; note that UUID will be generated
|
||||||
|
const newFilter = {};
|
||||||
|
this.setFilterValuesFromFormData(newFilter, formData);
|
||||||
|
|
||||||
|
// set current to what we just saved
|
||||||
|
newFilter.uuid = filters.add(newFilter);
|
||||||
|
|
||||||
|
// add to our array (at current index position)
|
||||||
|
this.filtersArray[this.currentFilterIndex] = newFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters.persist(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDataForFilter(filterIndex) {
|
||||||
|
const filter = this.filtersArray[filterIndex];
|
||||||
|
if(filter) {
|
||||||
|
this.setText(MciViewIds.editor.searchTerms, filter.terms);
|
||||||
|
this.setText(MciViewIds.editor.tags, filter.tags);
|
||||||
|
this.setText(MciViewIds.editor.filterName, filter.name);
|
||||||
|
|
||||||
|
this.setAreaIndexFromCurrentFilter();
|
||||||
|
this.setSortByFromCurrentFilter();
|
||||||
|
this.setOrderByFromCurrentFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -13,6 +13,7 @@ const Errors = require('../core/enig_error.js').Errors;
|
||||||
const ArchiveUtil = require('../core/archive_util.js');
|
const ArchiveUtil = require('../core/archive_util.js');
|
||||||
const Config = require('../core/config.js').config;
|
const Config = require('../core/config.js').config;
|
||||||
const DownloadQueue = require('../core/download_queue.js');
|
const DownloadQueue = require('../core/download_queue.js');
|
||||||
|
const FileAreaWeb = require('../core/file_area_web.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
@ -43,7 +44,6 @@ const MciViewIds = {
|
||||||
browse : {
|
browse : {
|
||||||
desc : 1,
|
desc : 1,
|
||||||
navMenu : 2,
|
navMenu : 2,
|
||||||
queueToggle : 3, // active queue toggle indicator - others avail in customs as {isQueued}
|
|
||||||
// 10+ = customs
|
// 10+ = customs
|
||||||
},
|
},
|
||||||
details : {
|
details : {
|
||||||
|
@ -74,7 +74,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
this.filterCriteria = options.extraArgs.filterCriteria;
|
this.filterCriteria = options.extraArgs.filterCriteria;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dlQueue = new DownloadQueue(this.client.user);
|
this.dlQueue = new DownloadQueue(this.client);
|
||||||
|
|
||||||
this.filterCriteria = this.filterCriteria || {
|
this.filterCriteria = this.filterCriteria || {
|
||||||
// :TODO: set area tag - all in current area by default
|
// :TODO: set area tag - all in current area by default
|
||||||
|
@ -112,6 +112,9 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
this.updateQueueIndicator();
|
this.updateQueueIndicator();
|
||||||
return cb(null);
|
return cb(null);
|
||||||
},
|
},
|
||||||
|
showWebDownloadLink : (formData, extraArgs, cb) => {
|
||||||
|
return this.fetchAndDisplayWebDownloadLink(cb);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +144,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
populateCurrentEntryInfo() {
|
populateCurrentEntryInfo(cb) {
|
||||||
const config = this.menuConfig.config;
|
const config = this.menuConfig.config;
|
||||||
const currEntry = this.currentFileEntry;
|
const currEntry = this.currentFileEntry;
|
||||||
|
|
||||||
|
@ -163,6 +166,8 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
|
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
|
||||||
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
|
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
|
||||||
isQueued : this.dlQueue.isQueued(this.currentFileEntry) ? isQueuedIndicator : isNotQueuedIndicator,
|
isQueued : this.dlQueue.isQueued(this.currentFileEntry) ? isQueuedIndicator : isNotQueuedIndicator,
|
||||||
|
webDlLink : '', // :TODO: fetch web any existing web d/l link
|
||||||
|
webDlExpire : '', // :TODO: fetch web d/l link expire time
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -194,9 +199,27 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
if(entryInfo.userRating < 5) {
|
if(entryInfo.userRating < 5) {
|
||||||
entryInfo.userRatingString += new Array( (5 - entryInfo.userRating) + 1).join(userRatingUnticked);
|
entryInfo.userRatingString += new Array( (5 - entryInfo.userRating) + 1).join(userRatingUnticked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
|
||||||
|
if(err) {
|
||||||
|
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated';
|
||||||
|
entryInfo.webDlExpire = '';
|
||||||
|
} else {
|
||||||
|
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||||
|
|
||||||
|
entryInfo.webDlLink = serveItem.url;
|
||||||
|
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
populateCustomLabels(category, startId) {
|
populateCustomLabels(category, startId) {
|
||||||
|
return this.updateCustomLabelsWithFilter(category, startId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCustomLabelsWithFilter(category, startId, filter) {
|
||||||
let textView;
|
let textView;
|
||||||
let customMciId = startId;
|
let customMciId = startId;
|
||||||
const config = this.menuConfig.config;
|
const config = this.menuConfig.config;
|
||||||
|
@ -205,7 +228,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
const key = `${category}InfoFormat${customMciId}`;
|
const key = `${category}InfoFormat${customMciId}`;
|
||||||
const format = config[key];
|
const format = config[key];
|
||||||
|
|
||||||
if(format) {
|
if(format && (!filter || filter.find(f => format.indexOf(f) > - 1))) {
|
||||||
textView.setText(stringFormat(format, this.currentFileEntry.entryInfo));
|
textView.setText(stringFormat(format, this.currentFileEntry.entryInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,8 +318,11 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
self.currentFileEntry = new FileEntry();
|
self.currentFileEntry = new FileEntry();
|
||||||
|
|
||||||
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => {
|
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => {
|
||||||
self.populateCurrentEntryInfo();
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.populateCurrentEntryInfo(callback);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function populateViews(callback) {
|
function populateViews(callback) {
|
||||||
|
@ -360,8 +386,63 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchAndDisplayWebDownloadLink(cb) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
async.series(
|
||||||
|
[
|
||||||
|
function generateLinkIfNeeded(callback) {
|
||||||
|
|
||||||
|
if(self.currentFileEntry.webDlExpireTime < moment()) {
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
|
||||||
|
|
||||||
|
FileAreaWeb.createAndServeTempDownload(
|
||||||
|
self.client,
|
||||||
|
self.currentFileEntry,
|
||||||
|
{ expireTime : expireTime },
|
||||||
|
(err, url) => {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentFileEntry.webDlExpireTime = expireTime;
|
||||||
|
|
||||||
|
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
|
||||||
|
|
||||||
|
self.currentFileEntry.entryInfo.webDlLink = url;
|
||||||
|
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat);
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function updateActiveViews(callback) {
|
||||||
|
self.updateCustomLabelsWithFilter( 'browse', 10, [ '{webDlLink}', '{webDlExpire}' ] );
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
err => {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateQueueIndicator() {
|
updateQueueIndicator() {
|
||||||
|
const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y';
|
||||||
|
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
|
||||||
|
|
||||||
|
this.currentFileEntry.entryInfo.isQueued = stringFormat(
|
||||||
|
this.dlQueue.isQueued(this.currentFileEntry) ?
|
||||||
|
isQueuedIndicator :
|
||||||
|
isNotQueuedIndicator
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateCustomLabelsWithFilter( 'browse', 10, [ '{isQueued}' ] );
|
||||||
|
/*
|
||||||
const indicatorView = this.viewControllers.browse.getView(MciViewIds.browse.queueToggle);
|
const indicatorView = this.viewControllers.browse.getView(MciViewIds.browse.queueToggle);
|
||||||
|
|
||||||
if(indicatorView) {
|
if(indicatorView) {
|
||||||
|
@ -374,7 +455,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||||
isNotQueuedIndicator
|
isNotQueuedIndicator
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheArchiveEntries(cb) {
|
cacheArchiveEntries(cb) {
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"ptyw.js": "NuSkooler/ptyw.js",
|
"ptyw.js": "NuSkooler/ptyw.js",
|
||||||
"sqlite3": "^3.1.1",
|
"sqlite3": "^3.1.1",
|
||||||
"ssh2": "^0.5.1",
|
"ssh2": "^0.5.1",
|
||||||
"temp": "^0.8.3"
|
"temp": "^0.8.3",
|
||||||
|
"hashids" : "^1.1.1",
|
||||||
|
"mime-types" : "^2.1.12"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.2.0"
|
"node": ">=4.2.0"
|
||||||
|
|
Loading…
Reference in New Issue