* 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
This commit is contained in:
parent
34474bc610
commit
caa9137162
|
@ -223,15 +223,14 @@ function initialize(cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListening(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
|
// :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 login servers configured'));
|
||||||
return cb(new Error('No servers configured'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleUtil = require('./module_util.js'); // late load so we get Config
|
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(err) {
|
||||||
if('EENIGMODDISABLED' === err.code) {
|
if('EENIGMODDISABLED' === err.code) {
|
||||||
logger.log.debug(err.message);
|
logger.log.debug(err.message);
|
||||||
|
|
|
@ -14,29 +14,29 @@ exports.init = init;
|
||||||
exports.getDefaultPath = getDefaultPath;
|
exports.getDefaultPath = getDefaultPath;
|
||||||
|
|
||||||
function hasMessageConferenceAndArea(config) {
|
function hasMessageConferenceAndArea(config) {
|
||||||
assert(_.isObject(config.messageConferences)); // we create one ourself!
|
assert(_.isObject(config.messageConferences)); // we create one ourself!
|
||||||
|
|
||||||
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
|
const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => {
|
||||||
return 'system_internal' !== confTag;
|
return 'system_internal' !== confTag;
|
||||||
});
|
});
|
||||||
|
|
||||||
if(0 === nonInternalConfs.length) {
|
if(0 === nonInternalConfs.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// :TODO: there is likely a better/cleaner way of doing this
|
// :TODO: there is likely a better/cleaner way of doing this
|
||||||
|
|
||||||
var result = false;
|
let result = false;
|
||||||
_.forEach(nonInternalConfs, confTag => {
|
_.forEach(nonInternalConfs, confTag => {
|
||||||
if(_.has(config.messageConferences[confTag], 'areas') &&
|
if(_.has(config.messageConferences[confTag], 'areas') &&
|
||||||
Object.keys(config.messageConferences[confTag].areas) > 0)
|
Object.keys(config.messageConferences[confTag].areas) > 0)
|
||||||
{
|
{
|
||||||
result = true;
|
result = true;
|
||||||
return false; // stop iteration
|
return false; // stop iteration
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(configPath, cb) {
|
function init(configPath, cb) {
|
||||||
|
@ -75,18 +75,18 @@ function init(configPath, cb) {
|
||||||
//
|
//
|
||||||
// Various sections must now exist in config
|
// Various sections must now exist in config
|
||||||
//
|
//
|
||||||
if(hasMessageConferenceAndArea(mergedConfig)) {
|
if(hasMessageConferenceAndArea(mergedConfig)) {
|
||||||
var msgAreasErr = new Error('Please create at least one message conference and area!');
|
var msgAreasErr = new Error('Please create at least one message conference and area!');
|
||||||
msgAreasErr.code = 'EBADCONFIG';
|
msgAreasErr.code = 'EBADCONFIG';
|
||||||
callback(msgAreasErr);
|
return callback(msgAreasErr);
|
||||||
} else {
|
} else {
|
||||||
callback(null, mergedConfig);
|
return callback(null, mergedConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
function complete(err, mergedConfig) {
|
function complete(err, mergedConfig) {
|
||||||
exports.config = mergedConfig;
|
exports.config = mergedConfig;
|
||||||
cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,8 @@ function getDefaultConfig() {
|
||||||
|
|
||||||
paths : {
|
paths : {
|
||||||
mods : paths.join(__dirname, './../mods/'),
|
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/'),
|
scannerTossers : paths.join(__dirname, './scanner_tossers/'),
|
||||||
mailers : paths.join(__dirname, './mailers/') ,
|
mailers : paths.join(__dirname, './mailers/') ,
|
||||||
|
@ -185,7 +186,7 @@ function getDefaultConfig() {
|
||||||
misc : paths.join(__dirname, './../misc/'),
|
misc : paths.join(__dirname, './../misc/'),
|
||||||
},
|
},
|
||||||
|
|
||||||
servers : {
|
loginServers : {
|
||||||
telnet : {
|
telnet : {
|
||||||
port : 8888,
|
port : 8888,
|
||||||
enabled : true,
|
enabled : true,
|
||||||
|
@ -267,6 +268,23 @@ function getDefaultConfig() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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 : {
|
eventScheduler : {
|
||||||
|
|
||||||
|
|
||||||
|
|
438
core/database.js
438
core/database.js
|
@ -47,233 +47,273 @@ function getModDatabasePath(moduleInfo, suffix) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeDatabases(cb) {
|
function initializeDatabases(cb) {
|
||||||
async.series(
|
async.each( [ 'system', 'user', 'message', 'file' ], (dbName, next) => {
|
||||||
[
|
dbs[dbName] = new sqlite3.Database(getDatabasePath(dbName), err => {
|
||||||
function systemDb(callback) {
|
if(err) {
|
||||||
dbs.system = new sqlite3.Database(getDatabasePath('system'), err => {
|
return cb(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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
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(
|
||||||
dbs.system.run(
|
`CREATE TABLE IF NOT EXISTS system_event_log (
|
||||||
`CREATE TABLE IF NOT EXISTS system_stat (
|
id INTEGER PRIMARY KEY,
|
||||||
stat_name VARCHAR PRIMARY KEY NOT NULL,
|
timestamp DATETIME NOT NULL,
|
||||||
stat_value VARCHAR NOT NULL
|
log_name VARCHAR NOT NULL,
|
||||||
);`
|
log_value VARCHAR NOT NULL,
|
||||||
);
|
|
||||||
|
|
||||||
dbs.system.run(
|
UNIQUE(timestamp, log_name)
|
||||||
`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)
|
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(
|
UNIQUE(timestamp, user_id, log_name)
|
||||||
`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)
|
user : () => {
|
||||||
);`
|
dbs.user.run('PRAGMA foreign_keys = ON;');
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createUserTables() {
|
dbs.user.run(
|
||||||
dbs.user.run('PRAGMA foreign_keys = ON;');
|
`CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_name VARCHAR NOT NULL,
|
||||||
|
UNIQUE(user_name)
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
|
||||||
dbs.user.run(
|
// :TODO: create FK on delete/etc.
|
||||||
`CREATE TABLE IF NOT EXISTS user (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_name VARCHAR NOT NULL,
|
|
||||||
UNIQUE(user_name)
|
|
||||||
);`
|
|
||||||
);
|
|
||||||
|
|
||||||
// :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(
|
dbs.user.run(
|
||||||
`CREATE TABLE IF NOT EXISTS user_property (
|
`CREATE TABLE IF NOT EXISTS user_group_member (
|
||||||
user_id INTEGER NOT NULL,
|
group_name VARCHAR NOT NULL,
|
||||||
prop_name VARCHAR NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
prop_value VARCHAR,
|
UNIQUE(group_name, user_id)
|
||||||
UNIQUE(user_id, prop_name),
|
);`
|
||||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
);
|
||||||
);`
|
|
||||||
);
|
|
||||||
|
|
||||||
dbs.user.run(
|
dbs.user.run(
|
||||||
`CREATE TABLE IF NOT EXISTS user_group_member (
|
`CREATE TABLE IF NOT EXISTS user_login_history (
|
||||||
group_name VARCHAR NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_name VARCHAR NOT NULL,
|
||||||
UNIQUE(group_name, user_id)
|
timestamp DATETIME NOT NULL
|
||||||
);`
|
);`
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
|
||||||
dbs.user.run(
|
message : () => {
|
||||||
`CREATE TABLE IF NOT EXISTS user_login_history (
|
dbs.message.run('PRAGMA foreign_keys = ON;');
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
user_name VARCHAR NOT NULL,
|
|
||||||
timestamp DATETIME NOT NULL
|
|
||||||
);`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
dbs.message.run(
|
||||||
`CREATE TABLE IF NOT EXISTS message (
|
`CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (
|
||||||
message_id INTEGER PRIMARY KEY,
|
content="message",
|
||||||
area_tag VARCHAR NOT NULL,
|
subject,
|
||||||
message_uuid VARCHAR(36) NOT NULL,
|
message
|
||||||
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(
|
dbs.message.run(
|
||||||
`CREATE INDEX IF NOT EXISTS message_by_area_tag_index
|
`CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
|
||||||
ON message (area_tag);`
|
DELETE FROM message_fts WHERE docid=old.rowid;
|
||||||
);
|
END;
|
||||||
|
|
||||||
dbs.message.run(
|
CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
|
||||||
`CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 (
|
DELETE FROM message_fts WHERE docid=old.rowid;
|
||||||
content="message",
|
END;
|
||||||
subject,
|
|
||||||
message
|
|
||||||
);`
|
|
||||||
);
|
|
||||||
|
|
||||||
dbs.message.run(
|
CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
|
||||||
`CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN
|
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
|
||||||
DELETE FROM message_fts WHERE docid=old.rowid;
|
END;
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN
|
CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
|
||||||
DELETE FROM message_fts WHERE docid=old.rowid;
|
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
|
||||||
END;
|
END;`
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN
|
dbs.message.run(
|
||||||
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
|
`CREATE TABLE IF NOT EXISTS message_meta (
|
||||||
END;
|
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
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN
|
// :TODO: need SQL to ensure cleaned up if delete from message?
|
||||||
INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message);
|
/*
|
||||||
END;`
|
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.message.run(
|
// :TODO: need SQL to ensure cleaned up if delete from message?
|
||||||
`CREATE TABLE IF NOT EXISTS message_meta (
|
dbs.message.run(
|
||||||
message_id INTEGER NOT NULL,
|
`CREATE TABLE IF NOT EXISTS message_hash_tag (
|
||||||
meta_category INTEGER NOT NULL,
|
hash_tag_id INTEGER NOT NULL,
|
||||||
meta_name VARCHAR NOT NULL,
|
message_id INTEGER 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 user_message_area_last_read (
|
||||||
dbs.message.run(
|
user_id INTEGER NOT NULL,
|
||||||
`CREATE TABLE IF NOT EXISTS hash_tag (
|
area_tag VARCHAR NOT NULL,
|
||||||
hash_tag_id INTEGER PRIMARY KEY,
|
message_id INTEGER NOT NULL,
|
||||||
hash_tag_name VARCHAR NOT NULL,
|
UNIQUE(user_id, area_tag)
|
||||||
UNIQUE(hash_tag_name)
|
);`
|
||||||
);`
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// :TODO: need SQL to ensure cleaned up if delete from message?
|
dbs.message.run(
|
||||||
dbs.message.run(
|
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
|
||||||
`CREATE TABLE IF NOT EXISTS message_hash_tag (
|
scan_toss VARCHAR NOT NULL,
|
||||||
hash_tag_id INTEGER NOT NULL,
|
area_tag VARCHAR NOT NULL,
|
||||||
message_id INTEGER NOT NULL,
|
message_id INTEGER NOT NULL,
|
||||||
);`
|
UNIQUE(scan_toss, area_tag)
|
||||||
);
|
);`
|
||||||
*/
|
);
|
||||||
|
},
|
||||||
|
|
||||||
dbs.message.run(
|
file : () => {
|
||||||
`CREATE TABLE IF NOT EXISTS user_message_area_last_read (
|
dbs.file.run('PRAGMA foreign_keys = ON;');
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
area_tag VARCHAR NOT NULL,
|
|
||||||
message_id INTEGER NOT NULL,
|
|
||||||
UNIQUE(user_id, area_tag)
|
|
||||||
);`
|
|
||||||
);
|
|
||||||
|
|
||||||
dbs.message.run(
|
dbs.file.run(
|
||||||
`CREATE TABLE IF NOT EXISTS message_area_last_scan (
|
// :TODO: should any of this be unique??
|
||||||
scan_toss VARCHAR NOT NULL,
|
`CREATE TABLE IF NOT EXISTS file (
|
||||||
area_tag VARCHAR NOT NULL,
|
file_id INTEGER PRIMARY KEY,
|
||||||
message_id INTEGER NOT NULL,
|
area_tag VARCHAR NOT NULL,
|
||||||
UNIQUE(scan_toss, area_tag)
|
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
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
|
||||||
function createInitialMessageValues() {
|
dbs.file.run(
|
||||||
}
|
`CREATE INDEX IF NOT EXISTS file_by_area_tag_index
|
||||||
|
ON file (area_tag);`
|
||||||
|
);
|
||||||
|
|
||||||
function createInitialUserValues() {
|
dbs.file.run(
|
||||||
}
|
`CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 (
|
||||||
|
content="file",
|
||||||
|
file_name,
|
||||||
|
desc,
|
||||||
|
desc_long
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;`
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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 + <optional payload> + 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 + <type>). 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;
|
|
||||||
};
|
|
Loading…
Reference in New Issue