From 34474bc610014676d7a303de043eb5923f255f62 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Sep 2016 21:08:59 -0600 Subject: [PATCH 1/7] minor cleanup --- core/message.js | 63 ++++++++++++++++++++++---------------------- core/message_area.js | 4 +-- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/core/message.js b/core/message.js index ff13a84a..65fe80f9 100644 --- a/core/message.js +++ b/core/message.js @@ -351,13 +351,13 @@ Message.prototype.persist = function(cb) { return cb(new Error('Cannot persist invalid message!')); } - let self = this; + const self = this; async.series( [ function beginTransaction(callback) { Message.startTransaction(err => { - callback(err); + return callback(err); }); }, function storeMessage(callback) { @@ -375,53 +375,52 @@ Message.prototype.persist = function(cb) { `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, self.getMessageTimestampString(msgTimestamp) ], - function inserted(err) { // use for this scope + function inserted(err) { // use non-arrow function for 'this' scope if(!err) { self.messageId = this.lastID; } - callback(err); + return callback(err); } ); }, function storeMeta(callback) { if(!self.meta) { - callback(null); - } else { - /* - Example of self.meta: - - meta: { - System: { - local_to_user_id: 1234, - }, - FtnProperty: { - ftn_seen_by: [ "1/102 103", "2/42 52 65" ] - } - } - */ - async.each(Object.keys(self.meta), (category, nextCat) => { - async.each(Object.keys(self.meta[category]), (name, nextName) => { - self.persistMetaValue(category, name, self.meta[category][name], err => { - nextName(err); - }); - }, err => { - nextCat(err); - }); - - }, err => { - callback(err); - }); + return callback(null); } + /* + Example of self.meta: + + meta: { + System: { + local_to_user_id: 1234, + }, + FtnProperty: { + ftn_seen_by: [ "1/102 103", "2/42 52 65" ] + } + } + */ + async.each(Object.keys(self.meta), (category, nextCat) => { + async.each(Object.keys(self.meta[category]), (name, nextName) => { + self.persistMetaValue(category, name, self.meta[category][name], err => { + nextName(err); + }); + }, err => { + nextCat(err); + }); + + }, err => { + callback(err); + }); }, function storeHashTags(callback) { // :TODO: hash tag support - callback(null); + return callback(null); } ], err => { Message.endTransaction(err, transErr => { - cb(err ? err : transErr, self.messageId); + return cb(err ? err : transErr, self.messageId); }); } ); diff --git a/core/message_area.js b/core/message_area.js index 1a272a58..2f74357b 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -560,10 +560,10 @@ function persistMessage(message, cb) { async.series( [ function persistMessageToDisc(callback) { - message.persist(callback); + return message.persist(callback); }, function recordToMessageNetworks(callback) { - msgNetRecord(message, callback); + return msgNetRecord(message, callback); } ], cb From caa91371620dc72b504940929f51e447f31c6612 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Sep 2016 21:28:21 -0600 Subject: [PATCH 2/7] * Move login servers to core/servers/login * A bit of minor changes related to upcoming file areas --- will likely branch before anything major * Clean up database.js a bit --- core/bbs.js | 7 +- core/config.js | 82 +++-- core/database.js | 440 ++++++++++++----------- core/servers/ssh.js | 265 -------------- core/servers/telnet.js | 788 ----------------------------------------- 5 files changed, 293 insertions(+), 1289 deletions(-) delete mode 100644 core/servers/ssh.js delete mode 100644 core/servers/telnet.js diff --git a/core/bbs.js b/core/bbs.js index 816dfee6..36243227 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -223,15 +223,14 @@ function initialize(cb) { } function startListening(cb) { - if(!conf.config.servers) { + if(!conf.config.loginServers) { // :TODO: Log error ... output to stderr as well. We can do it all with the logger - //logger.log.error('No servers configured'); - return cb(new Error('No servers configured')); + return cb(new Error('No login servers configured')); } const moduleUtil = require('./module_util.js'); // late load so we get Config - moduleUtil.loadModulesForCategory('servers', (err, module) => { + moduleUtil.loadModulesForCategory('loginServers', (err, module) => { if(err) { if('EENIGMODDISABLED' === err.code) { logger.log.debug(err.message); diff --git a/core/config.js b/core/config.js index b6f92ded..c53f37f0 100644 --- a/core/config.js +++ b/core/config.js @@ -14,29 +14,29 @@ exports.init = init; exports.getDefaultPath = getDefaultPath; function hasMessageConferenceAndArea(config) { - assert(_.isObject(config.messageConferences)); // we create one ourself! - - const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => { - return 'system_internal' !== confTag; - }); - - if(0 === nonInternalConfs.length) { - return false; - } - - // :TODO: there is likely a better/cleaner way of doing this - - var result = false; - _.forEach(nonInternalConfs, confTag => { - if(_.has(config.messageConferences[confTag], 'areas') && - Object.keys(config.messageConferences[confTag].areas) > 0) - { - result = true; - return false; // stop iteration - } - }); - - return result; + assert(_.isObject(config.messageConferences)); // we create one ourself! + + const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => { + return 'system_internal' !== confTag; + }); + + if(0 === nonInternalConfs.length) { + return false; + } + + // :TODO: there is likely a better/cleaner way of doing this + + let result = false; + _.forEach(nonInternalConfs, confTag => { + if(_.has(config.messageConferences[confTag], 'areas') && + Object.keys(config.messageConferences[confTag].areas) > 0) + { + result = true; + return false; // stop iteration + } + }); + + return result; } function init(configPath, cb) { @@ -75,18 +75,18 @@ function init(configPath, cb) { // // Various sections must now exist in config // - if(hasMessageConferenceAndArea(mergedConfig)) { - var msgAreasErr = new Error('Please create at least one message conference and area!'); + if(hasMessageConferenceAndArea(mergedConfig)) { + var msgAreasErr = new Error('Please create at least one message conference and area!'); msgAreasErr.code = 'EBADCONFIG'; - callback(msgAreasErr); - } else { - callback(null, mergedConfig); - } + return callback(msgAreasErr); + } else { + return callback(null, mergedConfig); + } } ], function complete(err, mergedConfig) { exports.config = mergedConfig; - cb(err); + return cb(err); } ); } @@ -171,7 +171,8 @@ function getDefaultConfig() { paths : { mods : paths.join(__dirname, './../mods/'), - servers : paths.join(__dirname, './servers/'), + loginServers : paths.join(__dirname, './servers/login/'), + contentServers : paths.join(__dirname, './servers/content/'), scannerTossers : paths.join(__dirname, './scanner_tossers/'), mailers : paths.join(__dirname, './mailers/') , @@ -185,7 +186,7 @@ function getDefaultConfig() { misc : paths.join(__dirname, './../misc/'), }, - servers : { + loginServers : { telnet : { port : 8888, enabled : true, @@ -266,6 +267,23 @@ function getDefaultConfig() { bundleTargetByteSize : 2048000, // 2M, before creating another archive } }, + + fileBase: { + // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: + areaStoragePrefix : paths.join(__dirname, './../file_base/'), + + fileNamePatterns: { + shortDesc : [ '^FILE_ID\.DIZ$', '^DESC\.SDI$' ], + longDesc : [ '^.*\.NFO$', '^README\.1ST$', '^README\.TXT$' ], + }, + + areas: { + message_attachment : { + name : 'Message attachments', + desc : 'File attachments to messages', + } + } + }, eventScheduler : { diff --git a/core/database.js b/core/database.js index b6437cf5..e5e74571 100644 --- a/core/database.js +++ b/core/database.js @@ -47,233 +47,273 @@ function getModDatabasePath(moduleInfo, suffix) { } function initializeDatabases(cb) { - async.series( - [ - function systemDb(callback) { - dbs.system = new sqlite3.Database(getDatabasePath('system'), err => { - if(err) { - return callback(err); - } - - dbs.system.serialize( () => { - createSystemTables(); - }); - - return callback(null); - }); - }, - function userDb(callback) { - dbs.user = new sqlite3.Database(getDatabasePath('user'), err => { - if(err) { - return callback(err); - } - - dbs.user.serialize( () => { - createUserTables(); - createInitialUserValues(); - }); - - return callback(null); - }); - }, - function messageDb(callback) { - dbs.message = new sqlite3.Database(getDatabasePath('message'), err => { - if(err) { - return callback(err); - } - - - dbs.message.serialize(function serialized() { - createMessageBaseTables(); - createInitialMessageValues(); - }); - - return callback(null); - }); + async.each( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { + dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => { + if(err) { + return cb(err); } - ], - cb - ); + + dbs[dbName].serialize( () => { + DB_INIT_TABLE[dbName](); + + return next(null); + }); + }); + }, err => { + return cb(err); + }); } -function createSystemTables() { +const DB_INIT_TABLE = { + system : () => { + dbs.system.run('PRAGMA foreign_keys = ON;'); - dbs.system.run('PRAGMA foreign_keys = ON;'); + // Various stat/event logging - see stat_log.js + dbs.system.run( + `CREATE TABLE IF NOT EXISTS system_stat ( + stat_name VARCHAR PRIMARY KEY NOT NULL, + stat_value VARCHAR NOT NULL + );` + ); - // Various stat/event logging - see stat_log.js - dbs.system.run( - `CREATE TABLE IF NOT EXISTS system_stat ( - stat_name VARCHAR PRIMARY KEY NOT NULL, - stat_value VARCHAR NOT NULL - );` - ); + dbs.system.run( + `CREATE TABLE IF NOT EXISTS system_event_log ( + id INTEGER PRIMARY KEY, + timestamp DATETIME NOT NULL, + log_name VARCHAR NOT NULL, + log_value VARCHAR NOT NULL, - dbs.system.run( - `CREATE TABLE IF NOT EXISTS system_event_log ( - id INTEGER PRIMARY KEY, - timestamp DATETIME NOT NULL, - log_name VARCHAR NOT NULL, - log_value VARCHAR NOT NULL, + UNIQUE(timestamp, log_name) + );` + ); - UNIQUE(timestamp, log_name) - );` - ); + dbs.system.run( + `CREATE TABLE IF NOT EXISTS user_event_log ( + id INTEGER PRIMARY KEY, + timestamp DATETIME NOT NULL, + user_id INTEGER NOT NULL, + log_name VARCHAR NOT NULL, + log_value VARCHAR NOT NULL, - dbs.system.run( - `CREATE TABLE IF NOT EXISTS user_event_log ( - id INTEGER PRIMARY KEY, - timestamp DATETIME NOT NULL, - user_id INTEGER NOT NULL, - log_name VARCHAR NOT NULL, - log_value VARCHAR NOT NULL, + UNIQUE(timestamp, user_id, log_name) + );` + ); + }, - UNIQUE(timestamp, user_id, log_name) - );` - ); -} + user : () => { + dbs.user.run('PRAGMA foreign_keys = ON;'); -function createUserTables() { - dbs.user.run('PRAGMA foreign_keys = ON;'); + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY, + user_name VARCHAR NOT NULL, + UNIQUE(user_name) + );` + ); - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user ( - id INTEGER PRIMARY KEY, - user_name VARCHAR NOT NULL, - UNIQUE(user_name) - );` - ); + // :TODO: create FK on delete/etc. - // :TODO: create FK on delete/etc. + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_property ( + user_id INTEGER NOT NULL, + prop_name VARCHAR NOT NULL, + prop_value VARCHAR, + UNIQUE(user_id, prop_name), + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE + );` + ); - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_property ( - user_id INTEGER NOT NULL, - prop_name VARCHAR NOT NULL, - prop_value VARCHAR, - UNIQUE(user_id, prop_name), - FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE - );` - ); + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_group_member ( + group_name VARCHAR NOT NULL, + user_id INTEGER NOT NULL, + UNIQUE(group_name, user_id) + );` + ); - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_group_member ( - group_name VARCHAR NOT NULL, - user_id INTEGER NOT NULL, - UNIQUE(group_name, user_id) - );` - ); + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_login_history ( + user_id INTEGER NOT NULL, + user_name VARCHAR NOT NULL, + timestamp DATETIME NOT NULL + );` + ); + }, - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_login_history ( - user_id INTEGER NOT NULL, - user_name VARCHAR NOT NULL, - timestamp DATETIME NOT NULL - );` - ); -} + message : () => { + dbs.message.run('PRAGMA foreign_keys = ON;'); -function createMessageBaseTables() { + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message ( + message_id INTEGER PRIMARY KEY, + area_tag VARCHAR NOT NULL, + message_uuid VARCHAR(36) NOT NULL, + reply_to_message_id INTEGER, + to_user_name VARCHAR NOT NULL, + from_user_name VARCHAR NOT NULL, + subject, /* FTS @ message_fts */ + message, /* FTS @ message_fts */ + modified_timestamp DATETIME NOT NULL, + view_count INTEGER NOT NULL DEFAULT 0, + UNIQUE(message_uuid) + );` + ); - dbs.message.run('PRAGMA foreign_keys = ON;'); + dbs.message.run( + `CREATE INDEX IF NOT EXISTS message_by_area_tag_index + ON message (area_tag);` + ); - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message ( - message_id INTEGER PRIMARY KEY, - area_tag VARCHAR NOT NULL, - message_uuid VARCHAR(36) NOT NULL, - reply_to_message_id INTEGER, - to_user_name VARCHAR NOT NULL, - from_user_name VARCHAR NOT NULL, - subject, /* FTS @ message_fts */ - message, /* FTS @ message_fts */ - modified_timestamp DATETIME NOT NULL, - view_count INTEGER NOT NULL DEFAULT 0, - UNIQUE(message_uuid) - );` - ); + dbs.message.run( + `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 ( + content="message", + subject, + message + );` + ); - dbs.message.run( - `CREATE INDEX IF NOT EXISTS message_by_area_tag_index - ON message (area_tag);` - ); + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN + DELETE FROM message_fts WHERE docid=old.rowid; + END; + + CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN + DELETE FROM message_fts WHERE docid=old.rowid; + END; - dbs.message.run( - `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 ( - content="message", - subject, - message - );` - ); + CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN + INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); + END; - dbs.message.run( - `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN - DELETE FROM message_fts WHERE docid=old.rowid; - END; + CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN + INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); + END;` + ); + + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message_meta ( + message_id INTEGER NOT NULL, + meta_category INTEGER NOT NULL, + meta_name VARCHAR NOT NULL, + meta_value VARCHAR NOT NULL, + UNIQUE(message_id, meta_category, meta_name, meta_value), + FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE + );` + ); + + // :TODO: need SQL to ensure cleaned up if delete from message? + /* + dbs.message.run( + `CREATE TABLE IF NOT EXISTS hash_tag ( + hash_tag_id INTEGER PRIMARY KEY, + hash_tag_name VARCHAR NOT NULL, + UNIQUE(hash_tag_name) + );` + ); + + // :TODO: need SQL to ensure cleaned up if delete from message? + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message_hash_tag ( + hash_tag_id INTEGER NOT NULL, + message_id INTEGER NOT NULL, + );` + ); + */ + + dbs.message.run( + `CREATE TABLE IF NOT EXISTS user_message_area_last_read ( + user_id INTEGER NOT NULL, + area_tag VARCHAR NOT NULL, + message_id INTEGER NOT NULL, + UNIQUE(user_id, area_tag) + );` + ); - CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN - DELETE FROM message_fts WHERE docid=old.rowid; - END; + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message_area_last_scan ( + scan_toss VARCHAR NOT NULL, + area_tag VARCHAR NOT NULL, + message_id INTEGER NOT NULL, + UNIQUE(scan_toss, area_tag) + );` + ); + }, - CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN - INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); - END; + file : () => { + dbs.file.run('PRAGMA foreign_keys = ON;'); - CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN - INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); - END;` - ); + dbs.file.run( + // :TODO: should any of this be unique?? + `CREATE TABLE IF NOT EXISTS file ( + file_id INTEGER PRIMARY KEY, + area_tag VARCHAR NOT NULL, + file_sha1 VARCHAR NOT NULL, + file_name, /* FTS @ file_fts */ + desc, /* FTS @ file_fts */ + desc_long, /* FTS @ file_fts */ + upload_by_username VARCHAR NOT NULL, + upload_timestamp DATETIME NOT NULL + );` + ); - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message_meta ( - message_id INTEGER NOT NULL, - meta_category INTEGER NOT NULL, - meta_name VARCHAR NOT NULL, - meta_value VARCHAR NOT NULL, - UNIQUE(message_id, meta_category, meta_name, meta_value), - FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE - );` - ); + dbs.file.run( + `CREATE INDEX IF NOT EXISTS file_by_area_tag_index + ON file (area_tag);` + ); - // :TODO: need SQL to ensure cleaned up if delete from message? - /* - dbs.message.run( - `CREATE TABLE IF NOT EXISTS hash_tag ( - hash_tag_id INTEGER PRIMARY KEY, - hash_tag_name VARCHAR NOT NULL, - UNIQUE(hash_tag_name) - );` - ); + dbs.file.run( + `CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 ( + content="file", + file_name, + desc, + desc_long + );` + ); - // :TODO: need SQL to ensure cleaned up if delete from message? - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message_hash_tag ( - hash_tag_id INTEGER NOT NULL, - message_id INTEGER NOT NULL, - );` - ); - */ + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN + DELETE FROM file_fts WHERE docid=old.rowid; + END; + + CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN + DELETE FROM file_fts WHERE docid=old.rowid; + END; - dbs.message.run( - `CREATE TABLE IF NOT EXISTS user_message_area_last_read ( - user_id INTEGER NOT NULL, - area_tag VARCHAR NOT NULL, - message_id INTEGER NOT NULL, - UNIQUE(user_id, area_tag) - );` - ); - - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message_area_last_scan ( - scan_toss VARCHAR NOT NULL, - area_tag VARCHAR NOT NULL, - message_id INTEGER NOT NULL, - UNIQUE(scan_toss, area_tag) - );` - ); -} + CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN + INSERT INTO file_fts(docid, file_name, desc, long_desc) VALUES(new.rowid, new.file_name, new.desc, new.long_desc); + END; -function createInitialMessageValues() { -} + CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN + INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.long_desc); + END;` + ); -function createInitialUserValues() { -} \ No newline at end of file + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_meta ( + file_id INTEGER NOT NULL, + meta_name VARCHAR NOT NULL, + meta_value VARCHAR NOT NULL, + UNIQUE(file_id, meta_name, meta_value), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE + );` + ); + + dbs.file.run( + `CREATE TABLE IF NOT EXISTS hash_tag ( + hash_tag_id INTEGER PRIMARY KEY, + hash_tag VARCHAR NOT NULL, + + UNIQUE(hash_tag) + );` + ); + + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_hash_tag ( + hash_tag_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + + UNIQUE(hash_tag_id, file_id) + );` + ); + } +}; \ No newline at end of file diff --git a/core/servers/ssh.js b/core/servers/ssh.js deleted file mode 100644 index d60d821e..00000000 --- a/core/servers/ssh.js +++ /dev/null @@ -1,265 +0,0 @@ -/* jslint node: true */ -'use strict'; - -// ENiGMA½ -const Config = require('../config.js').config; -const baseClient = require('../client.js'); -const Log = require('../logger.js').log; -const ServerModule = require('../server_module.js').ServerModule; -const userLogin = require('../user_login.js').userLogin; -const enigVersion = require('../../package.json').version; -const theme = require('../theme.js'); -const stringFormat = require('../string_format.js'); - -// deps -const ssh2 = require('ssh2'); -const fs = require('fs'); -const util = require('util'); -const _ = require('lodash'); -const assert = require('assert'); - -exports.moduleInfo = { - name : 'SSH', - desc : 'SSH Server', - author : 'NuSkooler', - isSecure : true, -}; - -exports.getModule = SSHServerModule; - -function SSHClient(clientConn) { - baseClient.Client.apply(this, arguments); - - // - // WARNING: Until we have emit 'ready', self.input, and self.output and - // not yet defined! - // - - const self = this; - - let loginAttempts = 0; - - clientConn.on('authentication', function authAttempt(ctx) { - const username = ctx.username || ''; - const password = ctx.password || ''; - - self.isNewUser = (Config.users.newUserNames || []).indexOf(username) > -1; - - self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); - - function terminateConnection() { - ctx.reject(); - clientConn.end(); - } - - // - // If the system is open and |isNewUser| is true, the login - // sequence is hijacked in order to start the applicaiton process. - // - if(false === Config.general.closedSystem && self.isNewUser) { - return ctx.accept(); - } - - if(username.length > 0 && password.length > 0) { - loginAttempts += 1; - - userLogin(self, ctx.username, ctx.password, function authResult(err) { - if(err) { - if(err.existingConn) { - // :TODO: Can we display somthing here? - terminateConnection(); - return; - } else { - return ctx.reject(SSHClient.ValidAuthMethods); - } - } else { - ctx.accept(); - } - }); - } else { - if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { - return ctx.reject(SSHClient.ValidAuthMethods); - } - - if(0 === username.length) { - // :TODO: can we display something here? - return ctx.reject(); - } - - let interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; - - ctx.prompt(interactivePrompt, function retryPrompt(answers) { - loginAttempts += 1; - - userLogin(self, username, (answers[0] || ''), err => { - if(err) { - if(err.existingConn) { - // :TODO: can we display something here? - terminateConnection(); - } else { - if(loginAttempts >= Config.general.loginAttempts) { - terminateConnection(); - } else { - const artOpts = { - client : self, - name : 'SSHPMPT.ASC', - readSauce : false, - }; - - theme.getThemeArt(artOpts, (err, artInfo) => { - if(err) { - interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; - } else { - const newUserNameList = _.has(Config, 'users.newUserNames') && Config.users.newUserNames.length > 0 ? - Config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : - '(No new user names enabled!)'; - - interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; - } - return ctx.prompt(interactivePrompt, retryPrompt); - }); - } - } - } else { - ctx.accept(); - } - }); - }); - } - }); - - this.updateTermInfo = function(info) { - // - // From ssh2 docs: - // "rows and cols override width and height when rows and cols are non-zero." - // - let termHeight; - let termWidth; - - if(info.rows > 0 && info.cols > 0) { - termHeight = info.rows; - termWidth = info.cols; - } else if(info.width > 0 && info.height > 0) { - termHeight = info.height; - termWidth = info.width; - } - - assert(_.isObject(self.term)); - - // - // Note that if we fail here, connect.js attempts some non-standard - // queries/etc., and ultimately will default to 80x24 if all else fails - // - if(termHeight > 0 && termWidth > 0) { - self.term.termHeight = termHeight; - self.term.termWidth = termWidth; - - self.clearMciCache(); // term size changes = invalidate cache - } - - if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { - self.setTermType(info.term); - } - }; - - clientConn.once('ready', function clientReady() { - self.log.info('SSH authentication success'); - - clientConn.on('session', accept => { - - const session = accept(); - - session.on('pty', function pty(accept, reject, info) { - self.log.debug(info, 'SSH pty event'); - - if(_.isFunction(accept)) { - accept(); - } - - if(self.input) { // do we have I/O? - self.updateTermInfo(info); - } else { - self.cachedPtyInfo = info; - } - }); - - session.on('shell', accept => { - self.log.debug('SSH shell event'); - - const channel = accept(); - - self.setInputOutput(channel.stdin, channel.stdout); - - channel.stdin.on('data', data => { - self.emit('data', data); - }); - - if(self.cachedPtyInfo) { - self.updateTermInfo(self.cachedPtyInfo); - delete self.cachedPtyInfo; - } - - // we're ready! - const firstMenu = self.isNewUser ? Config.servers.ssh.firstMenuNewUser : Config.servers.ssh.firstMenu; - self.emit('ready', { firstMenu : firstMenu } ); - }); - - session.on('window-change', (accept, reject, info) => { - self.log.debug(info, 'SSH window-change event'); - - self.updateTermInfo(info); - }); - - }); - }); - - clientConn.on('end', () => { - self.emit('end'); // remove client connection/tracking - }); - - clientConn.on('error', err => { - self.log.warn( { error : err.message, code : err.code }, 'SSH connection error'); - }); -} - -util.inherits(SSHClient, baseClient.Client); - -SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ]; - -function SSHServerModule() { - ServerModule.call(this); -} - -util.inherits(SSHServerModule, ServerModule); - -SSHServerModule.prototype.createServer = function() { - SSHServerModule.super_.prototype.createServer.call(this); - - const serverConf = { - hostKeys : [ - { - key : fs.readFileSync(Config.servers.ssh.privateKeyPem), - passphrase : Config.servers.ssh.privateKeyPass, - } - ], - ident : 'enigma-bbs-' + enigVersion + '-srv', - - // Note that sending 'banner' breaks at least EtherTerm! - debug : (sshDebugLine) => { - if(true === Config.servers.ssh.traceConnections) { - Log.trace(`SSH: ${sshDebugLine}`); - } - }, - }; - - const server = ssh2.Server(serverConf); - server.on('connection', function onConnection(conn, info) { - Log.info(info, 'New SSH connection'); - - const client = new SSHClient(conn); - - this.emit('client', client, conn._sock); - }); - - return server; -}; diff --git a/core/servers/telnet.js b/core/servers/telnet.js deleted file mode 100644 index 0e174a57..00000000 --- a/core/servers/telnet.js +++ /dev/null @@ -1,788 +0,0 @@ -/* jslint node: true */ -'use strict'; - -// ENiGMA½ -const baseClient = require('../client.js'); -const Log = require('../logger.js').log; -const ServerModule = require('../server_module.js').ServerModule; -const Config = require('../config.js').config; - -// deps -const net = require('net'); -const buffers = require('buffers'); -const binary = require('binary'); -const assert = require('assert'); -const util = require('util'); - -//var debug = require('debug')('telnet'); - -exports.moduleInfo = { - name : 'Telnet', - desc : 'Telnet Server', - author : 'NuSkooler', - isSecure : false, -}; - -exports.getModule = TelnetServerModule; - - -// -// Telnet Protocol Resources -// * http://pcmicro.com/netfoss/telnet.html -// * http://mud-dev.wikidot.com/telnet:negotiation -// - -/* - TODO: - * Document COMMANDS -- add any missing - * Document OPTIONS -- add any missing - * Internally handle OPTIONS: - * Some should be emitted generically - * Some shoudl be handled internally -- denied, handled, etc. - * - - * Allow term (ttype) to be set by environ sub negotiation - - * Process terms in loop.... research needed - - * Handle will/won't - * Handle do's, .. - * Some won't should close connection - - * Options/Commands we don't understand shouldn't crash the server!! - - -*/ - -const COMMANDS = { - SE : 240, // End of Sub-Negotation Parameters - NOP : 241, // No Operation - DM : 242, // Data Mark - BRK : 243, // Break - IP : 244, // Interrupt Process - AO : 245, // Abort Output - AYT : 246, // Are You There? - EC : 247, // Erase Character - EL : 248, // Erase Line - GA : 249, // Go Ahead - SB : 250, // Start Sub-Negotiation Parameters - WILL : 251, // - WONT : 252, - DO : 253, - DONT : 254, - IAC : 255, // (Data Byte) -}; - -// -// Resources: -// * http://www.faqs.org/rfcs/rfc1572.html -// -const SB_COMMANDS = { - IS : 0, - SEND : 1, - INFO : 2, -}; - -// -// Telnet Options -// -// Resources -// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html -// -const OPTIONS = { - TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 - ECHO : 1, // http://tools.ietf.org/html/rfc857 - // RECONNECTION : 2 - SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 - //APPROX_MESSAGE_SIZE : 4 - STATUS : 5, // http://tools.ietf.org/html/rfc859 - TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 - //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt - //OUPUT_LINE_WIDTH : 8, - //OUTPUT_PAGE_SIZE : 9, // - //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 - //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 - //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 - //OUTPUT_FORMFEED_DISP : 13, // RFC 655 - //OUTPUT_VERT_TABSTOPS : 14, // RFC 656 - //OUTPUT_VERT_TAB_DISP : 15, // RFC 657 - //OUTPUT_LF_DISP : 16, // RFC 658 - //EXTENDED_ASCII : 17, // RFC 659 - //LOGOUT : 18, // RFC 727 - //BYTE_MACRO : 19, // RFC 753 - //DATA_ENTRY_TERMINAL : 20, // RFC 1043 - //SUPDUP : 21, // RFC 736 - //SUPDUP_OUTPUT : 22, // RFC 749 - SEND_LOCATION : 23, // RFC 779 - TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 - //END_OF_RECORD : 25, // RFC 885 - //TACACS_USER_ID : 26, // RFC 927 - //OUTPUT_MARKING : 27, // RFC 933 - //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 - //TELNET_3270_REGIME : 29, // RFC 1041 - WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 - TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 - REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 - LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 - X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 - NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) - AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 - ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 - NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) - //TN3270E : 40, // RFC 2355 - //XAUTH : 41, - //CHARSET : 42, // RFC 2066 - //REMOTE_SERIAL_PORT : 43, - //COM_PORT_CONTROL : 44, // RFC 2217 - //SUPRESS_LOCAL_ECHO : 45, - //START_TLS : 46, - //KERMIT : 47, // RFC 2840 - //SEND_URL : 48, - //FORWARD_X : 49, - - //PRAGMA_LOGON : 138, - //SSPI_LOGON : 139, - //PRAGMA_HEARTBEAT : 140 - - EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) -}; - -// Commands used within NEW_ENVIRONMENT[_DEP] -const NEW_ENVIRONMENT_COMMANDS = { - VAR : 0, - VALUE : 1, - ESC : 2, - USERVAR : 3, -}; - -const IAC_BUF = new Buffer([ COMMANDS.IAC ]); -const IAC_SE_BUF = new Buffer([ COMMANDS.IAC, COMMANDS.SE ]); - -const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { - names[COMMANDS[name]] = name.toLowerCase(); - return names; -}, {}); - -const COMMAND_IMPLS = {}; -[ 'do', 'dont', 'will', 'wont', 'sb' ].forEach(function(command) { - const code = COMMANDS[command.toUpperCase()]; - COMMAND_IMPLS[code] = function(bufs, i, event) { - if(bufs.length < (i + 1)) { - return MORE_DATA_REQUIRED; - } - return parseOption(bufs, i, event); - }; -}); - -// :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode - -// Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY -const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) { - names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' '); - return names; -}, {}); - -const OPTION_IMPLS = {}; -// :TODO: fill in the rest... -OPTION_IMPLS.NO_ARGS = -OPTION_IMPLS[OPTIONS.ECHO] = -OPTION_IMPLS[OPTIONS.STATUS] = -OPTION_IMPLS[OPTIONS.LINEMODE] = -OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = -OPTION_IMPLS[OPTIONS.AUTHENTICATION] = -OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] = -OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] = -OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = -OPTION_IMPLS[OPTIONS.SEND_LOCATION] = - -OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { - event.buf = bufs.splice(0, i).toBuffer(); - return event; -}; - -OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // We need 4 bytes header + data + IAC SE - if(bufs.length < 7) { - return MORE_DATA_REQUIRED; - } - - let end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } - - // eat up and process the header - let buf = bufs.splice(0, 4).toBuffer(); - binary.parse(buf) - .word8('iac1') - .word8('sb') - .word8('ttype') - .word8('is') - .tap(function(vars) { - assert(vars.iac1 === COMMANDS.IAC); - assert(vars.sb === COMMANDS.SB); - assert(vars.ttype === OPTIONS.TERMINAL_TYPE); - assert(vars.is === SB_COMMANDS.IS); - }); - - // eat up the rest - end -= 4; - buf = bufs.splice(0, end).toBuffer(); - - // - // From this point -> |end| is our ttype - // - // Look for trailing NULL(s). Clients such as NetRunner do this. - // If none is found, we take the entire buffer - // - let trimAt = 0; - for(; trimAt < buf.length; ++trimAt) { - if(0x00 === buf[trimAt]) { - break; - } - } - - event.ttype = buf.toString('ascii', 0, trimAt); - - // pop off the terminating IAC SE - bufs.splice(0, 2); - } - - return event; -}; - -OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // we need 9 bytes - if(bufs.length < 9) { - return MORE_DATA_REQUIRED; - } - - event.buf = bufs.splice(0, 9).toBuffer(); - binary.parse(event.buf) - .word8('iac1') - .word8('sb') - .word8('naws') - .word16bu('width') - .word16bu('height') - .word8('iac2') - .word8('se') - .tap(function(vars) { - assert(vars.iac1 == COMMANDS.IAC); - assert(vars.sb == COMMANDS.SB); - assert(vars.naws == OPTIONS.WINDOW_SIZE); - assert(vars.iac2 == COMMANDS.IAC); - assert(vars.se == COMMANDS.SE); - - event.cols = event.columns = event.width = vars.width; - event.rows = event.height = vars.height; - }); - } - return event; -}; - -// Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] -const NEW_ENVIRONMENT_DELIMITERS = []; -Object.keys(NEW_ENVIRONMENT_COMMANDS).forEach(function onKey(k) { - NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]); -}); - -// Handle the deprecated RFC 1408 & the updated RFC 1572: -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // - // We need 4 bytes header + + IAC SE - // Many terminals send a empty list: - // IAC SB NEW-ENVIRON IS IAC SE - // - if(bufs.length < 6) { - return MORE_DATA_REQUIRED; - } - - let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } - - // eat up and process the header - let buf = bufs.splice(0, 4).toBuffer(); - binary.parse(buf) - .word8('iac1') - .word8('sb') - .word8('newEnv') - .word8('isOrInfo') // initial=IS, updates=INFO - .tap(function(vars) { - assert(vars.iac1 === COMMANDS.IAC); - assert(vars.sb === COMMANDS.SB); - assert(vars.newEnv === OPTIONS.NEW_ENVIRONMENT || vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP); - assert(vars.isOrInfo === SB_COMMANDS.IS || vars.isOrInfo === SB_COMMANDS.INFO); - - event.type = vars.isOrInfo; - - if(vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP) { - // :TODO: bring all this into Telnet class - Log.log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); - } - }); - - // eat up the rest - end -= 4; - buf = bufs.splice(0, end).toBuffer(); - - // - // This part can become messy. The basic spec is: - // IAC SB NEW-ENVIRON IS type ... [ VALUE ... ] [ type ... [ VALUE ... ] [ ... ] ] IAC SE - // - // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html - // - // Start by splitting up the remaining buffer. Keep the delimiters - // as prefixes we can use for processing. - // - // :TODO: Currently not supporting ESCaped values (ESC + ). Probably not really in the wild, but we should be compliant - // :TODO: Could probably just convert this to use a regex & handle delims + escaped values... in any case, this is sloppy... - const params = []; - let p = 0; - let j; - let l; - for(j = 0, l = buf.length; j < l; ++j) { - if(NEW_ENVIRONMENT_DELIMITERS.indexOf(buf[j]) === -1) { - continue; - } - - params.push(buf.slice(p, j)); - p = j; - } - - // remainder - if(p < l) { - params.push(buf.slice(p, l)); - } - - let varName; - event.envVars = {}; - // :TODO: handle cases where a variable was present in a previous exchange, but missing here...e.g removed - for(j = 0; j < params.length; ++j) { - if(params[j].length < 2) { - continue; - } - - let cmd = params[j].readUInt8(); - if(cmd === NEW_ENVIRONMENT_COMMANDS.VAR || cmd === NEW_ENVIRONMENT_COMMANDS.USERVAR) { - varName = params[j].slice(1).toString('utf8'); // :TODO: what encoding should this really be? - } else { - event.envVars[varName] = params[j].slice(1).toString('utf8'); // :TODO: again, what encoding? - } - } - - // pop off remaining IAC SE - bufs.splice(0, 2); - } - - return event; -}; - -const MORE_DATA_REQUIRED = 0xfeedface; - -function parseBufs(bufs) { - assert(bufs.length >= 2); - assert(bufs.get(0) === COMMANDS.IAC); - return parseCommand(bufs, 1, {}); -} - -function parseCommand(bufs, i, event) { - const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.commandCode = command; - event.command = COMMAND_NAMES[command]; - - const handler = COMMAND_IMPLS[command]; - if(handler) { - return handler(bufs, i + 1, event); - } else { - if(2 !== bufs.length) { - Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND - } - - event.buf = bufs.splice(0, 2).toBuffer(); - return event; - } -} - -function parseOption(bufs, i, event) { - const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.optionCode = option; - event.option = OPTION_NAMES[option]; - return OPTION_IMPLS[option](bufs, i + 1, event); -} - - -function TelnetClient(input, output) { - baseClient.Client.apply(this, arguments); - - const self = this; - - let bufs = buffers(); - this.bufs = bufs; - - this.setInputOutput(input, output); - - this.negotiationsComplete = false; // are we in the 'negotiation' phase? - this.didReady = false; // have we emit the 'ready' event? - - this.subNegotiationState = { - newEnvironRequested : false, - }; - - this.input.on('data', b => { - bufs.push(b); - - let i; - while((i = bufs.indexOf(IAC_BUF)) >= 0) { - - // - // Some clients will send even IAC separate from data - // - if(bufs.length <= (i + 1)) { - i = MORE_DATA_REQUIRED; - break; - } - - assert(bufs.length > (i + 1)); - - if(i > 0) { - self.emit('data', bufs.splice(0, i).toBuffer()); - } - - i = parseBufs(bufs); - - if(MORE_DATA_REQUIRED === i) { - break; - } else { - if(i.option) { - self.emit(i.option, i); // "transmit binary", "echo", ... - } - - self.handleTelnetEvent(i); - - if(i.data) { - self.emit('data', i.data); - } - } - } - - if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { - // - // Standard data payload. This can still be "non-user" data - // such as ANSI control, but we don't handle that here. - // - self.emit('data', bufs.splice(0).toBuffer()); - } - - }); - - this.input.on('end', () => { - self.emit('end'); - }); - - this.input.on('error', err => { - self.log.debug( { err : err }, 'Socket error'); - self.emit('end'); - }); - - this.connectionDebug = (info, msg) => { - if(Config.servers.telnet.traceConnections) { - self.log.trace(info, 'Telnet: ' + msg); - } - }; -} - -util.inherits(TelnetClient, baseClient.Client); - -/////////////////////////////////////////////////////////////////////////////// -// Telnet Command/Option handling -/////////////////////////////////////////////////////////////////////////////// -TelnetClient.prototype.handleTelnetEvent = function(evt) { - // handler name e.g. 'handleWontCommand' - const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; - - if(this[handlerName]) { - // specialized - this[handlerName](evt); - } else { - // generic-ish - this.handleMiscCommand(evt); - } -}; - -TelnetClient.prototype.handleWillCommand = function(evt) { - if('terminal type' === evt.option) { - // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // - this.requestTerminalType(); - } else if('new environment' === evt.option) { - // - // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html - // - this.requestNewEnvironment(); - } else { - // :TODO: temporary: - this.connectionDebug(evt, 'WILL'); - } -}; - -TelnetClient.prototype.handleWontCommand = function(evt) { - if('new environment' === evt.option) { - this.dont.new_environment(); - } else { - this.connectionDebug(evt, 'WONT'); - } -}; - -TelnetClient.prototype.handleDoCommand = function(evt) { - // :TODO: handle the rest, e.g. echo nd the like - - if('linemode' === evt.option) { - // - // Client wants to enable linemode editing. Denied. - // - this.wont.linemode(); - } else if('encrypt' === evt.option) { - // - // Client wants to enable encryption. Denied. - // - this.wont.encrypt(); - } else { - // :TODO: temporary: - this.connectionDebug(evt, 'DO'); - } -}; - -TelnetClient.prototype.handleDontCommand = function(evt) { - this.connectionDebug(evt, 'DONT'); -}; - -TelnetClient.prototype.handleSbCommand = function(evt) { - const self = this; - - if('terminal type' === evt.option) { - // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // - // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // We should keep asking until we see a repeat. From there, determine the best type/etc. - self.setTermType(evt.ttype); - - self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout - - if(!self.didReady) { - self.didReady = true; - self.emit('ready', { firstMenu : Config.servers.telnet.firstMenu } ); - } - } else if('new environment' === evt.option) { - // - // Handling is as follows: - // * Map 'TERM' -> 'termType' and only update if ours is 'unknown' - // * Map COLUMNS -> 'termWidth' and only update if ours is 0 - // * Map ROWS -> 'termHeight' and only update if ours is 0 - // * Add any new variables, ignore any existing - // - Object.keys(evt.envVars || {} ).forEach(function onEnv(name) { - if('TERM' === name && 'unknown' === self.term.termType) { - self.setTermType(evt.envVars[name]); - } else if('COLUMNS' === name && 0 === self.term.termWidth) { - self.term.termWidth = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache - self.log.debug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); - } else if('ROWS' === name && 0 === self.term.termHeight) { - self.term.termHeight = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache - self.log.debug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); - } else { - if(name in self.term.env) { - assert( - SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type, - 'Unexpected type: ' + evt.type); - - self.log.warn( - { varName : name, value : evt.envVars[name], existingValue : self.term.env[name] }, - 'Environment variable already exists'); - } else { - self.term.env[name] = evt.envVars[name]; - self.log.debug( - { varName : name, value : evt.envVars[name] }, 'New environment variable'); - } - } - }); - - } else if('window size' === evt.option) { - // - // Update termWidth & termHeight. - // Set LINES and COLUMNS environment variables as well. - // - self.term.termWidth = evt.width; - self.term.termHeight = evt.height; - - if(evt.width > 0) { - self.term.env.COLUMNS = evt.height; - } - - if(evt.height > 0) { - self.term.env.ROWS = evt.height; - } - - self.clearMciCache(); // term size changes = invalidate cache - - self.log.debug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); - } else { - self.log(evt, 'SB'); - } -}; - -const IGNORED_COMMANDS = []; -[ COMMANDS.EL, COMMANDS.GA, COMMANDS.NOP, COMMANDS.DM, COMMANDS.BRK ].forEach(function onCommandCode(cc) { - IGNORED_COMMANDS.push(cc); -}); - - -TelnetClient.prototype.handleMiscCommand = function(evt) { - assert(evt.command !== 'undefined' && evt.command.length > 0); - - // - // See: - // * RFC 854 @ http://tools.ietf.org/html/rfc854 - // - if('ip' === evt.command) { - // Interrupt Process (IP) - this.log.debug('Interrupt Process (IP) - Ending'); - - this.input.end(); - } else if('ayt' === evt.command) { - this.output.write('\b'); - - this.log.debug('Are You There (AYT) - Replied "\\b"'); - } else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) { - this.log.debug({ evt : evt }, 'Ignoring command'); - } else { - this.log.warn({ evt : evt }, 'Unknown command'); - } -}; - -TelnetClient.prototype.requestTerminalType = function() { - const buf = new Buffer( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.TERMINAL_TYPE, - SB_COMMANDS.SEND, - COMMANDS.IAC, - COMMANDS.SE ]); - this.output.write(buf); -}; - -const WANTED_ENVIRONMENT_VAR_BUFS = [ - new Buffer( 'LINES' ), - new Buffer( 'COLUMNS' ), - new Buffer( 'TERM' ), - new Buffer( 'TERM_PROGRAM' ) -]; - -TelnetClient.prototype.requestNewEnvironment = function() { - - if(this.subNegotiationState.newEnvironRequested) { - this.log.debug('New environment already requested'); - return; - } - - const self = this; - - const bufs = buffers(); - bufs.push(new Buffer( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.NEW_ENVIRONMENT, - SB_COMMANDS.SEND ] - )); - - for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { - bufs.push(new Buffer( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); - } - - bufs.push(new Buffer([ NEW_ENVIRONMENT_COMMANDS.USERVAR, COMMANDS.IAC, COMMANDS.SE ])); - - self.output.write(bufs.toBuffer()); - - this.subNegotiationState.newEnvironRequested = true; -}; - -TelnetClient.prototype.banner = function() { - this.will.echo(); - - this.will.suppress_go_ahead(); - this.do.suppress_go_ahead(); - - this.do.transmit_binary(); - this.will.transmit_binary(); - - this.do.terminal_type(); - - this.do.window_size(); - this.do.new_environment(); -}; - -function Command(command, client) { - this.command = COMMANDS[command.toUpperCase()]; - this.client = client; -} - -// Create Command objects with echo, transmit_binary, ... -Object.keys(OPTIONS).forEach(function(name) { - const code = OPTIONS[name]; - - Command.prototype[name.toLowerCase()] = function() { - const buf = new Buffer(3); - buf[0] = COMMANDS.IAC; - buf[1] = this.command; - buf[2] = code; - return this.client.output.write(buf); - }; -}); - -// Create do, dont, etc. methods on Client -['do', 'dont', 'will', 'wont'].forEach(function(command) { - const get = function() { - return new Command(command, this); - }; - - Object.defineProperty(TelnetClient.prototype, command, { - get : get, - enumerable : true, - configurable : true - }); -}); - -function TelnetServerModule() { - ServerModule.call(this); -} - -util.inherits(TelnetServerModule, ServerModule); - -TelnetServerModule.prototype.createServer = function() { - TelnetServerModule.super_.prototype.createServer.call(this); - - const server = net.createServer( (sock) => { - const client = new TelnetClient(sock, sock); - - client.banner(); - - server.emit('client', client, sock); - }); - - return server; -}; From 9f22117e49442fb61bedf2801876967f7cd8d258 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Sep 2016 21:28:50 -0600 Subject: [PATCH 3/7] * Move login servers to core/servers/login --- core/servers/login/ssh.js | 265 ++++++++++++ core/servers/login/telnet.js | 788 +++++++++++++++++++++++++++++++++++ 2 files changed, 1053 insertions(+) create mode 100644 core/servers/login/ssh.js create mode 100644 core/servers/login/telnet.js diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js new file mode 100644 index 00000000..1abd1e84 --- /dev/null +++ b/core/servers/login/ssh.js @@ -0,0 +1,265 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Config = require('../../config.js').config; +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const ServerModule = require('../../server_module.js').ServerModule; +const userLogin = require('../../user_login.js').userLogin; +const enigVersion = require('../../../package.json').version; +const theme = require('../../theme.js'); +const stringFormat = require('../../string_format.js'); + +// deps +const ssh2 = require('ssh2'); +const fs = require('fs'); +const util = require('util'); +const _ = require('lodash'); +const assert = require('assert'); + +exports.moduleInfo = { + name : 'SSH', + desc : 'SSH Server', + author : 'NuSkooler', + isSecure : true, +}; + +exports.getModule = SSHServerModule; + +function SSHClient(clientConn) { + baseClient.Client.apply(this, arguments); + + // + // WARNING: Until we have emit 'ready', self.input, and self.output and + // not yet defined! + // + + const self = this; + + let loginAttempts = 0; + + clientConn.on('authentication', function authAttempt(ctx) { + const username = ctx.username || ''; + const password = ctx.password || ''; + + self.isNewUser = (Config.users.newUserNames || []).indexOf(username) > -1; + + self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); + + function terminateConnection() { + ctx.reject(); + clientConn.end(); + } + + // + // If the system is open and |isNewUser| is true, the login + // sequence is hijacked in order to start the applicaiton process. + // + if(false === Config.general.closedSystem && self.isNewUser) { + return ctx.accept(); + } + + if(username.length > 0 && password.length > 0) { + loginAttempts += 1; + + userLogin(self, ctx.username, ctx.password, function authResult(err) { + if(err) { + if(err.existingConn) { + // :TODO: Can we display somthing here? + terminateConnection(); + return; + } else { + return ctx.reject(SSHClient.ValidAuthMethods); + } + } else { + ctx.accept(); + } + }); + } else { + if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { + return ctx.reject(SSHClient.ValidAuthMethods); + } + + if(0 === username.length) { + // :TODO: can we display something here? + return ctx.reject(); + } + + let interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; + + ctx.prompt(interactivePrompt, function retryPrompt(answers) { + loginAttempts += 1; + + userLogin(self, username, (answers[0] || ''), err => { + if(err) { + if(err.existingConn) { + // :TODO: can we display something here? + terminateConnection(); + } else { + if(loginAttempts >= Config.general.loginAttempts) { + terminateConnection(); + } else { + const artOpts = { + client : self, + name : 'SSHPMPT.ASC', + readSauce : false, + }; + + theme.getThemeArt(artOpts, (err, artInfo) => { + if(err) { + interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; + } else { + const newUserNameList = _.has(Config, 'users.newUserNames') && Config.users.newUserNames.length > 0 ? + Config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : + '(No new user names enabled!)'; + + interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; + } + return ctx.prompt(interactivePrompt, retryPrompt); + }); + } + } + } else { + ctx.accept(); + } + }); + }); + } + }); + + this.updateTermInfo = function(info) { + // + // From ssh2 docs: + // "rows and cols override width and height when rows and cols are non-zero." + // + let termHeight; + let termWidth; + + if(info.rows > 0 && info.cols > 0) { + termHeight = info.rows; + termWidth = info.cols; + } else if(info.width > 0 && info.height > 0) { + termHeight = info.height; + termWidth = info.width; + } + + assert(_.isObject(self.term)); + + // + // Note that if we fail here, connect.js attempts some non-standard + // queries/etc., and ultimately will default to 80x24 if all else fails + // + if(termHeight > 0 && termWidth > 0) { + self.term.termHeight = termHeight; + self.term.termWidth = termWidth; + + self.clearMciCache(); // term size changes = invalidate cache + } + + if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { + self.setTermType(info.term); + } + }; + + clientConn.once('ready', function clientReady() { + self.log.info('SSH authentication success'); + + clientConn.on('session', accept => { + + const session = accept(); + + session.on('pty', function pty(accept, reject, info) { + self.log.debug(info, 'SSH pty event'); + + if(_.isFunction(accept)) { + accept(); + } + + if(self.input) { // do we have I/O? + self.updateTermInfo(info); + } else { + self.cachedPtyInfo = info; + } + }); + + session.on('shell', accept => { + self.log.debug('SSH shell event'); + + const channel = accept(); + + self.setInputOutput(channel.stdin, channel.stdout); + + channel.stdin.on('data', data => { + self.emit('data', data); + }); + + if(self.cachedPtyInfo) { + self.updateTermInfo(self.cachedPtyInfo); + delete self.cachedPtyInfo; + } + + // we're ready! + const firstMenu = self.isNewUser ? Config.loginServers.ssh.firstMenuNewUser : Config.loginServers.ssh.firstMenu; + self.emit('ready', { firstMenu : firstMenu } ); + }); + + session.on('window-change', (accept, reject, info) => { + self.log.debug(info, 'SSH window-change event'); + + self.updateTermInfo(info); + }); + + }); + }); + + clientConn.on('end', () => { + self.emit('end'); // remove client connection/tracking + }); + + clientConn.on('error', err => { + self.log.warn( { error : err.message, code : err.code }, 'SSH connection error'); + }); +} + +util.inherits(SSHClient, baseClient.Client); + +SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ]; + +function SSHServerModule() { + ServerModule.call(this); +} + +util.inherits(SSHServerModule, ServerModule); + +SSHServerModule.prototype.createServer = function() { + SSHServerModule.super_.prototype.createServer.call(this); + + 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}`); + } + }, + }; + + const server = ssh2.Server(serverConf); + server.on('connection', function onConnection(conn, info) { + Log.info(info, 'New SSH connection'); + + const client = new SSHClient(conn); + + this.emit('client', client, conn._sock); + }); + + return server; +}; diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js new file mode 100644 index 00000000..7573e62e --- /dev/null +++ b/core/servers/login/telnet.js @@ -0,0 +1,788 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const ServerModule = require('../../server_module.js').ServerModule; +const Config = require('../../config.js').config; + +// deps +const net = require('net'); +const buffers = require('buffers'); +const binary = require('binary'); +const assert = require('assert'); +const util = require('util'); + +//var debug = require('debug')('telnet'); + +exports.moduleInfo = { + name : 'Telnet', + desc : 'Telnet Server', + author : 'NuSkooler', + isSecure : false, +}; + +exports.getModule = TelnetServerModule; + + +// +// Telnet Protocol Resources +// * http://pcmicro.com/netfoss/telnet.html +// * http://mud-dev.wikidot.com/telnet:negotiation +// + +/* + TODO: + * Document COMMANDS -- add any missing + * Document OPTIONS -- add any missing + * Internally handle OPTIONS: + * Some should be emitted generically + * Some shoudl be handled internally -- denied, handled, etc. + * + + * Allow term (ttype) to be set by environ sub negotiation + + * Process terms in loop.... research needed + + * Handle will/won't + * Handle do's, .. + * Some won't should close connection + + * Options/Commands we don't understand shouldn't crash the server!! + + +*/ + +const COMMANDS = { + SE : 240, // End of Sub-Negotation Parameters + NOP : 241, // No Operation + DM : 242, // Data Mark + BRK : 243, // Break + IP : 244, // Interrupt Process + AO : 245, // Abort Output + AYT : 246, // Are You There? + EC : 247, // Erase Character + EL : 248, // Erase Line + GA : 249, // Go Ahead + SB : 250, // Start Sub-Negotiation Parameters + WILL : 251, // + WONT : 252, + DO : 253, + DONT : 254, + IAC : 255, // (Data Byte) +}; + +// +// Resources: +// * http://www.faqs.org/rfcs/rfc1572.html +// +const SB_COMMANDS = { + IS : 0, + SEND : 1, + INFO : 2, +}; + +// +// Telnet Options +// +// Resources +// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html +// +const OPTIONS = { + TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 + ECHO : 1, // http://tools.ietf.org/html/rfc857 + // RECONNECTION : 2 + SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 + //APPROX_MESSAGE_SIZE : 4 + STATUS : 5, // http://tools.ietf.org/html/rfc859 + TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 + //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt + //OUPUT_LINE_WIDTH : 8, + //OUTPUT_PAGE_SIZE : 9, // + //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 + //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 + //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 + //OUTPUT_FORMFEED_DISP : 13, // RFC 655 + //OUTPUT_VERT_TABSTOPS : 14, // RFC 656 + //OUTPUT_VERT_TAB_DISP : 15, // RFC 657 + //OUTPUT_LF_DISP : 16, // RFC 658 + //EXTENDED_ASCII : 17, // RFC 659 + //LOGOUT : 18, // RFC 727 + //BYTE_MACRO : 19, // RFC 753 + //DATA_ENTRY_TERMINAL : 20, // RFC 1043 + //SUPDUP : 21, // RFC 736 + //SUPDUP_OUTPUT : 22, // RFC 749 + SEND_LOCATION : 23, // RFC 779 + TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 + //END_OF_RECORD : 25, // RFC 885 + //TACACS_USER_ID : 26, // RFC 927 + //OUTPUT_MARKING : 27, // RFC 933 + //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 + //TELNET_3270_REGIME : 29, // RFC 1041 + WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 + TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 + REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 + LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 + X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 + NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) + AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 + ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 + NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) + //TN3270E : 40, // RFC 2355 + //XAUTH : 41, + //CHARSET : 42, // RFC 2066 + //REMOTE_SERIAL_PORT : 43, + //COM_PORT_CONTROL : 44, // RFC 2217 + //SUPRESS_LOCAL_ECHO : 45, + //START_TLS : 46, + //KERMIT : 47, // RFC 2840 + //SEND_URL : 48, + //FORWARD_X : 49, + + //PRAGMA_LOGON : 138, + //SSPI_LOGON : 139, + //PRAGMA_HEARTBEAT : 140 + + EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) +}; + +// Commands used within NEW_ENVIRONMENT[_DEP] +const NEW_ENVIRONMENT_COMMANDS = { + VAR : 0, + VALUE : 1, + ESC : 2, + USERVAR : 3, +}; + +const IAC_BUF = new Buffer([ COMMANDS.IAC ]); +const IAC_SE_BUF = new Buffer([ COMMANDS.IAC, COMMANDS.SE ]); + +const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { + names[COMMANDS[name]] = name.toLowerCase(); + return names; +}, {}); + +const COMMAND_IMPLS = {}; +[ 'do', 'dont', 'will', 'wont', 'sb' ].forEach(function(command) { + const code = COMMANDS[command.toUpperCase()]; + COMMAND_IMPLS[code] = function(bufs, i, event) { + if(bufs.length < (i + 1)) { + return MORE_DATA_REQUIRED; + } + return parseOption(bufs, i, event); + }; +}); + +// :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode + +// Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY +const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) { + names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' '); + return names; +}, {}); + +const OPTION_IMPLS = {}; +// :TODO: fill in the rest... +OPTION_IMPLS.NO_ARGS = +OPTION_IMPLS[OPTIONS.ECHO] = +OPTION_IMPLS[OPTIONS.STATUS] = +OPTION_IMPLS[OPTIONS.LINEMODE] = +OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = +OPTION_IMPLS[OPTIONS.AUTHENTICATION] = +OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] = +OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] = +OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = +OPTION_IMPLS[OPTIONS.SEND_LOCATION] = + +OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { + event.buf = bufs.splice(0, i).toBuffer(); + return event; +}; + +OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { + if(event.commandCode !== COMMANDS.SB) { + OPTION_IMPLS.NO_ARGS(bufs, i, event); + } else { + // We need 4 bytes header + data + IAC SE + if(bufs.length < 7) { + return MORE_DATA_REQUIRED; + } + + let end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes + if(-1 === end) { + return MORE_DATA_REQUIRED; + } + + // eat up and process the header + let buf = bufs.splice(0, 4).toBuffer(); + binary.parse(buf) + .word8('iac1') + .word8('sb') + .word8('ttype') + .word8('is') + .tap(function(vars) { + assert(vars.iac1 === COMMANDS.IAC); + assert(vars.sb === COMMANDS.SB); + assert(vars.ttype === OPTIONS.TERMINAL_TYPE); + assert(vars.is === SB_COMMANDS.IS); + }); + + // eat up the rest + end -= 4; + buf = bufs.splice(0, end).toBuffer(); + + // + // From this point -> |end| is our ttype + // + // Look for trailing NULL(s). Clients such as NetRunner do this. + // If none is found, we take the entire buffer + // + let trimAt = 0; + for(; trimAt < buf.length; ++trimAt) { + if(0x00 === buf[trimAt]) { + break; + } + } + + event.ttype = buf.toString('ascii', 0, trimAt); + + // pop off the terminating IAC SE + bufs.splice(0, 2); + } + + return event; +}; + +OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { + if(event.commandCode !== COMMANDS.SB) { + OPTION_IMPLS.NO_ARGS(bufs, i, event); + } else { + // we need 9 bytes + if(bufs.length < 9) { + return MORE_DATA_REQUIRED; + } + + event.buf = bufs.splice(0, 9).toBuffer(); + binary.parse(event.buf) + .word8('iac1') + .word8('sb') + .word8('naws') + .word16bu('width') + .word16bu('height') + .word8('iac2') + .word8('se') + .tap(function(vars) { + assert(vars.iac1 == COMMANDS.IAC); + assert(vars.sb == COMMANDS.SB); + assert(vars.naws == OPTIONS.WINDOW_SIZE); + assert(vars.iac2 == COMMANDS.IAC); + assert(vars.se == COMMANDS.SE); + + event.cols = event.columns = event.width = vars.width; + event.rows = event.height = vars.height; + }); + } + return event; +}; + +// Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] +const NEW_ENVIRONMENT_DELIMITERS = []; +Object.keys(NEW_ENVIRONMENT_COMMANDS).forEach(function onKey(k) { + NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]); +}); + +// Handle the deprecated RFC 1408 & the updated RFC 1572: +OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = +OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { + if(event.commandCode !== COMMANDS.SB) { + OPTION_IMPLS.NO_ARGS(bufs, i, event); + } else { + // + // We need 4 bytes header + + IAC SE + // Many terminals send a empty list: + // IAC SB NEW-ENVIRON IS IAC SE + // + if(bufs.length < 6) { + return MORE_DATA_REQUIRED; + } + + let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes + if(-1 === end) { + return MORE_DATA_REQUIRED; + } + + // eat up and process the header + let buf = bufs.splice(0, 4).toBuffer(); + binary.parse(buf) + .word8('iac1') + .word8('sb') + .word8('newEnv') + .word8('isOrInfo') // initial=IS, updates=INFO + .tap(function(vars) { + assert(vars.iac1 === COMMANDS.IAC); + assert(vars.sb === COMMANDS.SB); + assert(vars.newEnv === OPTIONS.NEW_ENVIRONMENT || vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP); + assert(vars.isOrInfo === SB_COMMANDS.IS || vars.isOrInfo === SB_COMMANDS.INFO); + + event.type = vars.isOrInfo; + + if(vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP) { + // :TODO: bring all this into Telnet class + Log.log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); + } + }); + + // eat up the rest + end -= 4; + buf = bufs.splice(0, end).toBuffer(); + + // + // This part can become messy. The basic spec is: + // IAC SB NEW-ENVIRON IS type ... [ VALUE ... ] [ type ... [ VALUE ... ] [ ... ] ] IAC SE + // + // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html + // + // Start by splitting up the remaining buffer. Keep the delimiters + // as prefixes we can use for processing. + // + // :TODO: Currently not supporting ESCaped values (ESC + ). Probably not really in the wild, but we should be compliant + // :TODO: Could probably just convert this to use a regex & handle delims + escaped values... in any case, this is sloppy... + const params = []; + let p = 0; + let j; + let l; + for(j = 0, l = buf.length; j < l; ++j) { + if(NEW_ENVIRONMENT_DELIMITERS.indexOf(buf[j]) === -1) { + continue; + } + + params.push(buf.slice(p, j)); + p = j; + } + + // remainder + if(p < l) { + params.push(buf.slice(p, l)); + } + + let varName; + event.envVars = {}; + // :TODO: handle cases where a variable was present in a previous exchange, but missing here...e.g removed + for(j = 0; j < params.length; ++j) { + if(params[j].length < 2) { + continue; + } + + let cmd = params[j].readUInt8(); + if(cmd === NEW_ENVIRONMENT_COMMANDS.VAR || cmd === NEW_ENVIRONMENT_COMMANDS.USERVAR) { + varName = params[j].slice(1).toString('utf8'); // :TODO: what encoding should this really be? + } else { + event.envVars[varName] = params[j].slice(1).toString('utf8'); // :TODO: again, what encoding? + } + } + + // pop off remaining IAC SE + bufs.splice(0, 2); + } + + return event; +}; + +const MORE_DATA_REQUIRED = 0xfeedface; + +function parseBufs(bufs) { + assert(bufs.length >= 2); + assert(bufs.get(0) === COMMANDS.IAC); + return parseCommand(bufs, 1, {}); +} + +function parseCommand(bufs, i, event) { + const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same + event.commandCode = command; + event.command = COMMAND_NAMES[command]; + + const handler = COMMAND_IMPLS[command]; + if(handler) { + return handler(bufs, i + 1, event); + } else { + if(2 !== bufs.length) { + Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND + } + + event.buf = bufs.splice(0, 2).toBuffer(); + return event; + } +} + +function parseOption(bufs, i, event) { + const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same + event.optionCode = option; + event.option = OPTION_NAMES[option]; + return OPTION_IMPLS[option](bufs, i + 1, event); +} + + +function TelnetClient(input, output) { + baseClient.Client.apply(this, arguments); + + const self = this; + + let bufs = buffers(); + this.bufs = bufs; + + this.setInputOutput(input, output); + + this.negotiationsComplete = false; // are we in the 'negotiation' phase? + this.didReady = false; // have we emit the 'ready' event? + + this.subNegotiationState = { + newEnvironRequested : false, + }; + + this.input.on('data', b => { + bufs.push(b); + + let i; + while((i = bufs.indexOf(IAC_BUF)) >= 0) { + + // + // Some clients will send even IAC separate from data + // + if(bufs.length <= (i + 1)) { + i = MORE_DATA_REQUIRED; + break; + } + + assert(bufs.length > (i + 1)); + + if(i > 0) { + self.emit('data', bufs.splice(0, i).toBuffer()); + } + + i = parseBufs(bufs); + + if(MORE_DATA_REQUIRED === i) { + break; + } else { + if(i.option) { + self.emit(i.option, i); // "transmit binary", "echo", ... + } + + self.handleTelnetEvent(i); + + if(i.data) { + self.emit('data', i.data); + } + } + } + + if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { + // + // Standard data payload. This can still be "non-user" data + // such as ANSI control, but we don't handle that here. + // + self.emit('data', bufs.splice(0).toBuffer()); + } + + }); + + this.input.on('end', () => { + self.emit('end'); + }); + + this.input.on('error', err => { + self.log.debug( { err : err }, 'Socket error'); + self.emit('end'); + }); + + this.connectionDebug = (info, msg) => { + if(Config.loginServers.telnet.traceConnections) { + self.log.trace(info, 'Telnet: ' + msg); + } + }; +} + +util.inherits(TelnetClient, baseClient.Client); + +/////////////////////////////////////////////////////////////////////////////// +// Telnet Command/Option handling +/////////////////////////////////////////////////////////////////////////////// +TelnetClient.prototype.handleTelnetEvent = function(evt) { + // handler name e.g. 'handleWontCommand' + const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; + + if(this[handlerName]) { + // specialized + this[handlerName](evt); + } else { + // generic-ish + this.handleMiscCommand(evt); + } +}; + +TelnetClient.prototype.handleWillCommand = function(evt) { + if('terminal type' === evt.option) { + // + // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // + this.requestTerminalType(); + } else if('new environment' === evt.option) { + // + // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html + // + this.requestNewEnvironment(); + } else { + // :TODO: temporary: + this.connectionDebug(evt, 'WILL'); + } +}; + +TelnetClient.prototype.handleWontCommand = function(evt) { + if('new environment' === evt.option) { + this.dont.new_environment(); + } else { + this.connectionDebug(evt, 'WONT'); + } +}; + +TelnetClient.prototype.handleDoCommand = function(evt) { + // :TODO: handle the rest, e.g. echo nd the like + + if('linemode' === evt.option) { + // + // Client wants to enable linemode editing. Denied. + // + this.wont.linemode(); + } else if('encrypt' === evt.option) { + // + // Client wants to enable encryption. Denied. + // + this.wont.encrypt(); + } else { + // :TODO: temporary: + this.connectionDebug(evt, 'DO'); + } +}; + +TelnetClient.prototype.handleDontCommand = function(evt) { + this.connectionDebug(evt, 'DONT'); +}; + +TelnetClient.prototype.handleSbCommand = function(evt) { + const self = this; + + if('terminal type' === evt.option) { + // + // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // + // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // We should keep asking until we see a repeat. From there, determine the best type/etc. + self.setTermType(evt.ttype); + + self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout + + if(!self.didReady) { + self.didReady = true; + self.emit('ready', { firstMenu : Config.loginServers.telnet.firstMenu } ); + } + } else if('new environment' === evt.option) { + // + // Handling is as follows: + // * Map 'TERM' -> 'termType' and only update if ours is 'unknown' + // * Map COLUMNS -> 'termWidth' and only update if ours is 0 + // * Map ROWS -> 'termHeight' and only update if ours is 0 + // * Add any new variables, ignore any existing + // + Object.keys(evt.envVars || {} ).forEach(function onEnv(name) { + if('TERM' === name && 'unknown' === self.term.termType) { + self.setTermType(evt.envVars[name]); + } else if('COLUMNS' === name && 0 === self.term.termWidth) { + self.term.termWidth = parseInt(evt.envVars[name]); + self.clearMciCache(); // term size changes = invalidate cache + self.log.debug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); + } else if('ROWS' === name && 0 === self.term.termHeight) { + self.term.termHeight = parseInt(evt.envVars[name]); + self.clearMciCache(); // term size changes = invalidate cache + self.log.debug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); + } else { + if(name in self.term.env) { + assert( + SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type, + 'Unexpected type: ' + evt.type); + + self.log.warn( + { varName : name, value : evt.envVars[name], existingValue : self.term.env[name] }, + 'Environment variable already exists'); + } else { + self.term.env[name] = evt.envVars[name]; + self.log.debug( + { varName : name, value : evt.envVars[name] }, 'New environment variable'); + } + } + }); + + } else if('window size' === evt.option) { + // + // Update termWidth & termHeight. + // Set LINES and COLUMNS environment variables as well. + // + self.term.termWidth = evt.width; + self.term.termHeight = evt.height; + + if(evt.width > 0) { + self.term.env.COLUMNS = evt.height; + } + + if(evt.height > 0) { + self.term.env.ROWS = evt.height; + } + + self.clearMciCache(); // term size changes = invalidate cache + + self.log.debug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); + } else { + self.log(evt, 'SB'); + } +}; + +const IGNORED_COMMANDS = []; +[ COMMANDS.EL, COMMANDS.GA, COMMANDS.NOP, COMMANDS.DM, COMMANDS.BRK ].forEach(function onCommandCode(cc) { + IGNORED_COMMANDS.push(cc); +}); + + +TelnetClient.prototype.handleMiscCommand = function(evt) { + assert(evt.command !== 'undefined' && evt.command.length > 0); + + // + // See: + // * RFC 854 @ http://tools.ietf.org/html/rfc854 + // + if('ip' === evt.command) { + // Interrupt Process (IP) + this.log.debug('Interrupt Process (IP) - Ending'); + + this.input.end(); + } else if('ayt' === evt.command) { + this.output.write('\b'); + + this.log.debug('Are You There (AYT) - Replied "\\b"'); + } else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) { + this.log.debug({ evt : evt }, 'Ignoring command'); + } else { + this.log.warn({ evt : evt }, 'Unknown command'); + } +}; + +TelnetClient.prototype.requestTerminalType = function() { + const buf = new Buffer( [ + COMMANDS.IAC, + COMMANDS.SB, + OPTIONS.TERMINAL_TYPE, + SB_COMMANDS.SEND, + COMMANDS.IAC, + COMMANDS.SE ]); + this.output.write(buf); +}; + +const WANTED_ENVIRONMENT_VAR_BUFS = [ + new Buffer( 'LINES' ), + new Buffer( 'COLUMNS' ), + new Buffer( 'TERM' ), + new Buffer( 'TERM_PROGRAM' ) +]; + +TelnetClient.prototype.requestNewEnvironment = function() { + + if(this.subNegotiationState.newEnvironRequested) { + this.log.debug('New environment already requested'); + return; + } + + const self = this; + + const bufs = buffers(); + bufs.push(new Buffer( [ + COMMANDS.IAC, + COMMANDS.SB, + OPTIONS.NEW_ENVIRONMENT, + SB_COMMANDS.SEND ] + )); + + for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { + bufs.push(new Buffer( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); + } + + bufs.push(new Buffer([ NEW_ENVIRONMENT_COMMANDS.USERVAR, COMMANDS.IAC, COMMANDS.SE ])); + + self.output.write(bufs.toBuffer()); + + this.subNegotiationState.newEnvironRequested = true; +}; + +TelnetClient.prototype.banner = function() { + this.will.echo(); + + this.will.suppress_go_ahead(); + this.do.suppress_go_ahead(); + + this.do.transmit_binary(); + this.will.transmit_binary(); + + this.do.terminal_type(); + + this.do.window_size(); + this.do.new_environment(); +}; + +function Command(command, client) { + this.command = COMMANDS[command.toUpperCase()]; + this.client = client; +} + +// Create Command objects with echo, transmit_binary, ... +Object.keys(OPTIONS).forEach(function(name) { + const code = OPTIONS[name]; + + Command.prototype[name.toLowerCase()] = function() { + const buf = new Buffer(3); + buf[0] = COMMANDS.IAC; + buf[1] = this.command; + buf[2] = code; + return this.client.output.write(buf); + }; +}); + +// Create do, dont, etc. methods on Client +['do', 'dont', 'will', 'wont'].forEach(function(command) { + const get = function() { + return new Command(command, this); + }; + + Object.defineProperty(TelnetClient.prototype, command, { + get : get, + enumerable : true, + configurable : true + }); +}); + +function TelnetServerModule() { + ServerModule.call(this); +} + +util.inherits(TelnetServerModule, ServerModule); + +TelnetServerModule.prototype.createServer = function() { + TelnetServerModule.super_.prototype.createServer.call(this); + + const server = net.createServer( (sock) => { + const client = new TelnetClient(sock, sock); + + client.banner(); + + server.emit('client', client, sock); + }); + + return server; +}; From 6a28b3ff35e00c57a065f65adb46fd54638327d8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Sep 2016 21:29:46 -0600 Subject: [PATCH 4/7] Dump schedule info @ load --- core/scanner_tossers/ftn_bso.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 30cb0933..b5e65227 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1216,8 +1216,9 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { if(exportSchedule) { Log.debug( { - schedule : this.moduleConfig.schedule.export, + schedule : this.moduleConfig.schedule.export, schedOK : -1 === exportSchedule.sched.error, + next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), immediate : exportSchedule.immediate ? true : false, }, 'Export schedule loaded' @@ -1245,6 +1246,7 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { { schedule : this.moduleConfig.schedule.import, schedOK : -1 === importSchedule.sched.error, + next : moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), watchFile : _.isString(importSchedule.watchFile) ? importSchedule.watchFile : 'None', }, 'Import schedule loaded' From 7da0abdc399f28f5f5b8e3679f3948a92ee74de4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Sep 2016 21:30:26 -0600 Subject: [PATCH 5/7] Work on EnigError and usage as experiment; This will go to many other areas of the code --- core/enig_error.js | 15 +++++++++++++-- core/menu_stack.js | 11 ++++++----- core/message.js | 2 +- core/view_controller.js | 6 +++++- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/core/enig_error.js b/core/enig_error.js index 69722a85..adf5eef6 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -2,11 +2,14 @@ 'use strict'; class EnigError extends Error { - constructor(message) { + constructor(message, code, reason, reasonCode) { super(message); this.name = this.constructor.name; this.message = message; + this.code = code; + this.reason = reason; + this.reasonCode = reasonCode; if(typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor); @@ -16,4 +19,12 @@ class EnigError extends Error { } } -exports.EnigError = EnigError; \ No newline at end of file +class EnigMenuError extends EnigError { } + +exports.EnigError = EnigError; +exports.EnigMenuError = EnigMenuError; + +exports.Errors = { + General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), + MenuStack : (reason, reasonCode) => new EnigMenuError('Menu stack error', -33001, reason, reasonCode), +}; diff --git a/core/menu_stack.js b/core/menu_stack.js index 98a171b9..84e6a6e0 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -3,6 +3,7 @@ // ENiGMA½ const loadMenu = require('./menu_util.js').loadMenu; +const Errors = require('./enig_error.js').Errors; // deps const _ = require('lodash'); @@ -57,16 +58,16 @@ module.exports = class MenuStack { if(_.isArray(menuConfig.next)) { nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); if(!nextMenu) { - return cb(new Error('No matching condition for \'next\'!')); + return cb(Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH')); } } else if(_.isString(menuConfig.next)) { nextMenu = menuConfig.next; } else { - return cb(new Error('Invalid or missing \'next\' member in menu config!')); + return cb(Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT')); } if(nextMenu === currentModuleInfo.name) { - return cb(new Error('Menu config \'next\' specifies current menu!')); + return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE')); } this.goto(nextMenu, { }, cb); @@ -90,7 +91,7 @@ module.exports = class MenuStack { return this.goto(previousModuleInfo.name, opts, cb); } - return cb(new Error('No previous menu available!')); + return cb(Errors.MenuStack('No previous menu available', 'NOPREV')); } goto(name, options, cb) { @@ -104,7 +105,7 @@ module.exports = class MenuStack { if(currentModuleInfo && name === currentModuleInfo.name) { if(cb) { - cb(new Error('Already at supplied menu!')); + cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); } return; } diff --git a/core/message.js b/core/message.js index 65fe80f9..e1d3e4fd 100644 --- a/core/message.js +++ b/core/message.js @@ -139,7 +139,7 @@ Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); return uuid.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); -} +}; Message.getMessageIdByUuid = function(uuid, cb) { msgDb.get( diff --git a/core/view_controller.js b/core/view_controller.js index dcc25d31..f2fbb366 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -49,7 +49,11 @@ function ViewController(options) { menuUtil.handleAction(self.client, formData, actionBlock, (err) => { if(err) { // :TODO: What can we really do here? - self.client.log.warn( { err : err }, 'Error during handleAction()'); + if('ALREADYTHERE' === err.reasonCode) { + self.client.log.trace( err.reason ); + } else { + self.client.log.warn( { err : err }, 'Error during handleAction()'); + } } self.waitActionCompletion = false; From f8c49906462d3776ffc3d2c879cf13127068595c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 1 Oct 2016 13:22:34 -0600 Subject: [PATCH 6/7] Add 'pcansi' support for ZOC terminal --- core/client_term.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/client_term.js b/core/client_term.js index 8ef4753d..c9a9e66f 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -131,15 +131,16 @@ ClientTerminal.prototype.isANSI = function() { // ansi-bbs: // * fTelnet // + // pcansi: + // * ZOC + // // screen: // * ConnectBot (Android) // // linux: // * JuiceSSH (note: TERM=linux also) // - - // :TODO: Others?? - return [ 'ansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1; + return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].indexOf(this.termType) > -1; }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) From 0a98ce651ff40c29507b831564b27a5afe627f47 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Oct 2016 13:47:19 -0600 Subject: [PATCH 7/7] #101 set real names for message areas --- core/fse.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/fse.js b/core/fse.js index 414c10ef..0afc31c1 100644 --- a/core/fse.js +++ b/core/fse.js @@ -8,6 +8,7 @@ const ansi = require('./ansi_term.js'); const theme = require('./theme.js'); const Message = require('./message.js'); const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; +const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; const getUserIdAndName = require('./user.js').getUserIdAndName; const cleanControlCodes = require('./string_util.js').cleanControlCodes; const StatLog = require('./stat_log.js'); @@ -563,8 +564,14 @@ function FullScreenEditorModule(options) { break; case 'edit' : - self.viewControllers.header.getView(1).setText(self.client.user.username); // from - + const fromView = self.viewControllers.header.getView(1); + const area = getMessageAreaByTag(self.messageAreaTag); + if(area && area.realNames) { + fromView.setText(self.client.user.properties.real_name || self.client.user.username); + } else { + fromView.setText(self.client.user.username); + } + if(self.replyToMessage) { self.initHeaderReplyEditMode(); }