mirror of
https://github.com/NuSkooler/enigma-bbs.git
synced 2025-08-04 08:51:51 +02:00
+ Initial source checkin
This commit is contained in:
parent
9804c93f2e
commit
9a7e90b9b2
31 changed files with 4361 additions and 0 deletions
285
core/ansi_escape_parser.js
Normal file
285
core/ansi_escape_parser.js
Normal file
|
@ -0,0 +1,285 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var events = require('events');
|
||||
var util = require('util');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
|
||||
exports.ANSIEscapeParser = ANSIEscapeParser;
|
||||
|
||||
|
||||
function ANSIEscapeParser(options) {
|
||||
var self = this;
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
this.column = 1;
|
||||
this.row = 1;
|
||||
this.flags = 0x00;
|
||||
this.scrollBack = 0;
|
||||
|
||||
|
||||
|
||||
options = miscUtil.valueWithDefault(options, {
|
||||
mciReplaceChar : '',
|
||||
termHeight : 25,
|
||||
termWidth : 80,
|
||||
});
|
||||
|
||||
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
||||
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
||||
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
||||
|
||||
function getArgArray(array) {
|
||||
var i = array.length;
|
||||
while(i--) {
|
||||
array[i] = parseInt(array[i], 10);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
self.moveCursor = function(cols, rows) {
|
||||
self.column += cols;
|
||||
self.row += rows;
|
||||
|
||||
self.column = Math.max(self.column, 1);
|
||||
self.column = Math.min(self.column, self.termWidth);
|
||||
self.row = Math.max(self.row, 1);
|
||||
self.row = Math.min(self.row, self.termHeight);
|
||||
|
||||
self.emit('move cursor', self.column, self.row);
|
||||
self.rowUpdated();
|
||||
};
|
||||
|
||||
self.saveCursorPosition = function() {
|
||||
self.savedPosition = {
|
||||
row : self.row,
|
||||
column : self.column
|
||||
};
|
||||
};
|
||||
|
||||
self.restoreCursorPosition = function() {
|
||||
self.row = self.savedPosition.row;
|
||||
self.column = self.savedPosition.column;
|
||||
delete self.savedPosition;
|
||||
self.rowUpdated();
|
||||
};
|
||||
|
||||
self.clearScreen = function() {
|
||||
// :TODO: should be doing something with row/column?
|
||||
self.emit('clear screen');
|
||||
};
|
||||
|
||||
self.resetColor = function() {
|
||||
self.fgColor = 7;
|
||||
self.bgColor = 0;
|
||||
};
|
||||
|
||||
self.rowUpdated = function() {
|
||||
self.emit('row update', self.row + self.scrollBack);
|
||||
};
|
||||
|
||||
function literal(text) {
|
||||
var CR = 0x0d;
|
||||
var LF = 0x0a;
|
||||
var charCode;
|
||||
|
||||
var len = text.length;
|
||||
for(var i = 0; i < len; i++) {
|
||||
charCode = text.charCodeAt(i) & 0xff; // ensure 8 bit
|
||||
switch(charCode) {
|
||||
case CR :
|
||||
self.column = 1;
|
||||
break;
|
||||
|
||||
case LF :
|
||||
self.row++;
|
||||
self.rowUpdated();
|
||||
break;
|
||||
|
||||
default :
|
||||
// wrap
|
||||
if(self.column === self.termWidth) {
|
||||
self.column = 1;
|
||||
self.row++;
|
||||
self.rowUpdated();
|
||||
} else {
|
||||
self.column++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(self.row === 26) {
|
||||
self.scrollBack++;
|
||||
self.row--;
|
||||
self.rowUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
self.emit('chunk', text);
|
||||
}
|
||||
|
||||
function mci(mciCode, args) {
|
||||
console.log(mciCode, args);
|
||||
}
|
||||
|
||||
function getProcessedMCI(mci) {
|
||||
if(self.mciReplaceChar.length > 0) {
|
||||
return new Array(mci.length + 1).join(self.mciReplaceChar);
|
||||
} else {
|
||||
return mci;
|
||||
}
|
||||
}
|
||||
|
||||
function parseMCI(buffer) {
|
||||
var mciRe = /\%([A-Z]{2}[0-9]{1,2})(?:\(([0-9A-Z,]+)\))*/g;
|
||||
var pos = 0;
|
||||
var match;
|
||||
var mciCode;
|
||||
var args;
|
||||
|
||||
do {
|
||||
pos = mciRe.lastIndex;
|
||||
match = mciRe.exec(buffer);
|
||||
|
||||
if(null !== match) {
|
||||
if(match.index > pos) {
|
||||
literal(buffer.slice(pos, match.index));
|
||||
}
|
||||
|
||||
mciCode = match[1];
|
||||
|
||||
if(match[2]) {
|
||||
args = match[2].split(',');
|
||||
} else {
|
||||
args = [];
|
||||
}
|
||||
|
||||
|
||||
self.emit('mci', mciCode, args);
|
||||
|
||||
self.emit('chunk', getProcessedMCI(match[0]));
|
||||
}
|
||||
|
||||
} while(0 !== mciRe.lastIndex);
|
||||
|
||||
if(pos < buffer.length) {
|
||||
literal(buffer.slice(pos));
|
||||
}
|
||||
}
|
||||
|
||||
self.parse = function(buffer, savedRe) {
|
||||
// :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc.
|
||||
var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g;
|
||||
var pos = 0;
|
||||
var match;
|
||||
var opCode;
|
||||
var args;
|
||||
|
||||
// ignore anything past EOF marker, if any
|
||||
buffer = buffer.split(String.fromCharCode(0x1a), 1)[0];
|
||||
|
||||
do {
|
||||
pos = re.lastIndex;
|
||||
match = re.exec(buffer);
|
||||
|
||||
if(null !== match) {
|
||||
if(match.index > pos) {
|
||||
parseMCI(buffer.slice(pos, match.index));
|
||||
}
|
||||
|
||||
opCode = match[2];
|
||||
args = getArgArray(match[1].split(';'));
|
||||
|
||||
escape(opCode, args);
|
||||
|
||||
self.emit('chunk', match[0]);
|
||||
}
|
||||
|
||||
} while(0 !== re.lastIndex);
|
||||
|
||||
if(pos < buffer.length) {
|
||||
parseMCI(buffer.slice(pos));
|
||||
}
|
||||
|
||||
self.emit('complete');
|
||||
};
|
||||
|
||||
function escape(opCode, args) {
|
||||
var arg;
|
||||
var i;
|
||||
var len;
|
||||
|
||||
switch(opCode) {
|
||||
// cursor up
|
||||
case 'A' :
|
||||
arg = args[0] || 1;
|
||||
self.moveCursor(0, -arg);
|
||||
break;
|
||||
|
||||
// cursor down
|
||||
case 'B' :
|
||||
arg = args[0] || 1;
|
||||
self.moveCursor(0, arg);
|
||||
break;
|
||||
|
||||
// cursor forward/right
|
||||
case 'C' :
|
||||
arg = args[0] || 1;
|
||||
self.moveCursor(arg, 0);
|
||||
break;
|
||||
|
||||
// cursor back/left
|
||||
case 'D' :
|
||||
arg = args[0] || 1;
|
||||
self.moveCursor(-arg, 0);
|
||||
break;
|
||||
|
||||
case 'f' : // horiz & vertical
|
||||
case 'H' : // cursor position
|
||||
self.row = args[0] || 1;
|
||||
self.column = args[1] || 1;
|
||||
self.rowUpdated();
|
||||
break;
|
||||
|
||||
// save position
|
||||
case 's' :
|
||||
self.saveCursorPosition();
|
||||
break;
|
||||
|
||||
// restore position
|
||||
case 'u' :
|
||||
self.restoreCursorPosition();
|
||||
break;
|
||||
|
||||
// set graphic rendition
|
||||
case 'm' :
|
||||
for(i = 0, len = args.length; i < len; ++i) {
|
||||
arg = args[i];
|
||||
if(0x00 === arg) {
|
||||
self.flags = 0x00;
|
||||
self.resetColor();
|
||||
} else {
|
||||
switch(Math.floor(arg / 10)) {
|
||||
case 0 : self.flags |= arg; break;
|
||||
case 3 : self.fgColor = arg; break;
|
||||
case 4 : self.bgColor = arg; break;
|
||||
//case 3 : self.fgColor = arg - 30; break;
|
||||
//case 4 : self.bgColor = arg - 40; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// erase display/screen
|
||||
case 'J' :
|
||||
// :TODO: Handle others
|
||||
if(2 === args[0]) {
|
||||
self.clearScreen();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
util.inherits(ANSIEscapeParser, events.EventEmitter);
|
338
core/ansi_term.js
Normal file
338
core/ansi_term.js
Normal file
|
@ -0,0 +1,338 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
//
|
||||
// ANSI Terminal Support
|
||||
//
|
||||
// Resources:
|
||||
// * http://ansi-bbs.org/
|
||||
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt
|
||||
// * http://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
//
|
||||
|
||||
var assert = require('assert');
|
||||
var binary = require('binary');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
|
||||
exports.sgr = sgr;
|
||||
exports.clearScreen = clearScreen;
|
||||
exports.clearScreenGoHome = clearScreenGoHome;
|
||||
exports.normal = normal;
|
||||
exports.goHome = goHome;
|
||||
exports.disableVT100LineWrapping = disableVT100LineWrapping;
|
||||
exports.setSyncTermFont = setSyncTermFont;
|
||||
exports.fromPipeCode = fromPipeCode;
|
||||
exports.forEachControlCode = forEachControlCode;
|
||||
|
||||
|
||||
//
|
||||
// See also
|
||||
// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js
|
||||
|
||||
var ESC_CSI = '\u001b[';
|
||||
|
||||
var CONTROL = {
|
||||
up : 'A',
|
||||
down : 'B',
|
||||
forward : 'C',
|
||||
back : 'D',
|
||||
nextLine : 'E',
|
||||
prevLine : 'F',
|
||||
horizAbsolute : 'G',
|
||||
eraseData : 'J',
|
||||
scrollUp : 'S',
|
||||
scrollDown : 'T',
|
||||
savePos : 's',
|
||||
restorePos : 'u',
|
||||
queryPos : '6n',
|
||||
goto : 'H', // row Pr, column Pc -- same as f
|
||||
gotoAlt : 'f' // same as H
|
||||
};
|
||||
|
||||
/*
|
||||
DECTERM stuff. Probably never need
|
||||
hide : '?25l',
|
||||
show : '?25h',*/
|
||||
|
||||
//
|
||||
// Select Graphics Rendition
|
||||
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
||||
//
|
||||
var SGR = {
|
||||
reset : 0,
|
||||
bold : 1,
|
||||
dim : 2,
|
||||
blink : 5,
|
||||
fastBlink : 6,
|
||||
negative : 7,
|
||||
hidden : 8,
|
||||
|
||||
normal : 22,
|
||||
steady : 25,
|
||||
positive : 27,
|
||||
|
||||
black : 30,
|
||||
red : 31,
|
||||
green : 32,
|
||||
yellow : 33,
|
||||
blue : 34,
|
||||
magenta : 35,
|
||||
cyan : 36,
|
||||
white : 37,
|
||||
|
||||
blackBG : 40,
|
||||
redBG : 41,
|
||||
greenBG : 42,
|
||||
yellowBG : 43,
|
||||
blueBG : 44,
|
||||
magentaBG : 45,
|
||||
cyanBG : 47,
|
||||
whiteBG : 47,
|
||||
};
|
||||
|
||||
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
|
||||
// :TODO: document
|
||||
var SYNC_TERM_FONTS = [
|
||||
'cp437',
|
||||
'cp1251',
|
||||
'koi8_r',
|
||||
'iso8859_2',
|
||||
'iso8859_4',
|
||||
'cp866',
|
||||
'iso8859_9',
|
||||
'haik8',
|
||||
'iso8859_8',
|
||||
'koi8_u',
|
||||
'iso8859_15',
|
||||
'iso8859_4',
|
||||
'koi8_r_b',
|
||||
'iso8859_4',
|
||||
'iso8859_5',
|
||||
'ARMSCII_8',
|
||||
'iso8859_15',
|
||||
'cp850',
|
||||
'cp850',
|
||||
'cp885',
|
||||
'cp1251',
|
||||
'iso8859_7',
|
||||
'koi8-r_c',
|
||||
'iso8859_4',
|
||||
'iso8859_1',
|
||||
'cp866',
|
||||
'cp437',
|
||||
'cp866',
|
||||
'cp885',
|
||||
'cp866_u',
|
||||
'iso8859_1',
|
||||
'cp1131',
|
||||
'c64_upper',
|
||||
'c64_lower',
|
||||
'c128_upper',
|
||||
'c128_lower',
|
||||
'atari',
|
||||
'pot_noodle',
|
||||
'mo_soul',
|
||||
'microknight',
|
||||
'topaz'
|
||||
];
|
||||
|
||||
// Create methods such as up(), nextLine(),...
|
||||
Object.keys(CONTROL).forEach(function onControlName(name) {
|
||||
var code = CONTROL[name];
|
||||
|
||||
exports[name] = function() {
|
||||
var c = code;
|
||||
if(arguments.length > 0) {
|
||||
// arguments are array like -- we want an array
|
||||
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
|
||||
}
|
||||
return ESC_CSI + c;
|
||||
};
|
||||
});
|
||||
|
||||
// Create a reverse map of CONTROL values to their key/names
|
||||
|
||||
/*
|
||||
var CONTROL_REVERSE_MAP = {};
|
||||
Object.keys(CONTROL).forEach(function onControlName(name) {
|
||||
var code = CONTROL[name];
|
||||
|
||||
CONTROL_REVERSE_MAP[code] = name;
|
||||
});
|
||||
*/
|
||||
|
||||
var CONTROL_RESPONSE = {
|
||||
'R' : 'position',
|
||||
};
|
||||
|
||||
// :TODO: move this to misc utils or such -- use here & parser
|
||||
function getIntArgArray(array) {
|
||||
var i = array.length;
|
||||
while(i--) {
|
||||
array[i] = parseInt(array[i], 10);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
// :TODO: rename this
|
||||
function forEachControlCode(data, cb) {
|
||||
//var re = /\u001b\[([0-9\;])*[R]/g;
|
||||
|
||||
var len = data.length;
|
||||
var pos = 0;
|
||||
|
||||
while(pos < len) {
|
||||
if(0x1b !== data[pos++] || 0x5b !== data[pos++]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var params = '';
|
||||
|
||||
while(pos < len) {
|
||||
var c = data[pos++];
|
||||
|
||||
if(((c > 64) && (c < 91)) || ((c > 96) && (c < 123))) {
|
||||
c = String.fromCharCode(c);
|
||||
var name = CONTROL_RESPONSE[c];
|
||||
if(name) {
|
||||
params = getIntArgArray(params.split(';'));
|
||||
cb(name, params);
|
||||
}
|
||||
}
|
||||
|
||||
params += String.fromCharCode(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create various color methods such as white(), yellowBG(), reset(), ...
|
||||
Object.keys(SGR).forEach(function onSgrName(name) {
|
||||
var code = SGR[name];
|
||||
|
||||
exports[name] = function() {
|
||||
return ESC_CSI + code + 'm';
|
||||
};
|
||||
});
|
||||
|
||||
function sgr() {
|
||||
//
|
||||
// - Allow an single array or variable number of arguments
|
||||
// - Each element can be either a integer or string found in SGR
|
||||
// which in turn maps to a integer
|
||||
//
|
||||
if(arguments.length <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var result = '';
|
||||
|
||||
// :TODO: this method needs a lot of cleanup!
|
||||
|
||||
var args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
|
||||
for(var i = 0; i < args.length; i++) {
|
||||
if(typeof args[i] === 'string') {
|
||||
if(args[i] in SGR) {
|
||||
if(result.length > 0) {
|
||||
result += ';';
|
||||
}
|
||||
result += SGR[args[i]];
|
||||
}
|
||||
} else if(typeof args[i] === 'number') {
|
||||
if(result.length > 0) {
|
||||
result += ';';
|
||||
}
|
||||
result += args[i];
|
||||
}
|
||||
}
|
||||
return ESC_CSI + result + 'm';
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Shortcuts for common functions
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function clearScreen() {
|
||||
return exports.eraseData(2);
|
||||
}
|
||||
|
||||
function clearScreenGoHome() {
|
||||
return exports.goto(1,1) + exports.eraseData(2);
|
||||
}
|
||||
|
||||
function normal() {
|
||||
return sgr(['normal', 'reset']);
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
return exports.goto(); // no params = home = 1,1
|
||||
}
|
||||
|
||||
//
|
||||
// See http://www.termsys.demon.co.uk/vtANSI_BBS.htm
|
||||
//
|
||||
function disableVT100LineWrapping() {
|
||||
return ESC_CSI + '7l';
|
||||
}
|
||||
|
||||
function setSyncTermFont(name, fontPage) {
|
||||
fontPage = miscUtil.valueWithDefault(fontPage, 0);
|
||||
|
||||
assert(fontPage === 0 || fontPage === 1); // see spec
|
||||
|
||||
var i = SYNC_TERM_FONTS.indexOf(name);
|
||||
if(-1 != i) {
|
||||
return ESC_CSI + fontPage + ';' + i + ' D';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Also add:
|
||||
// * fromRenegade(): |<0-23>
|
||||
// * fromCelerity(): |<case sensitive letter>
|
||||
// * fromPCBoard(): (@X<bg><fg>@)
|
||||
// * fromWildcat(): (@<bg><fg>@ (same as PCBoard without 'X' prefix)
|
||||
// * fromWWIV(): <ctrl-c><0-7>
|
||||
// * fromSyncronet(): <ctrl-a><colorCode>
|
||||
// See http://wiki.synchro.net/custom:colors
|
||||
function fromPipeCode(s) {
|
||||
if(-1 == s.indexOf('|')) {
|
||||
return s; // no pipe codes present
|
||||
}
|
||||
|
||||
var result = '';
|
||||
var re = /\|(\d{2,3}|\|)/g;
|
||||
var m;
|
||||
var lastIndex = 0;
|
||||
while((m = re.exec(s))) {
|
||||
var val = m[1];
|
||||
|
||||
if('|' == val) {
|
||||
result += '|';
|
||||
continue;
|
||||
}
|
||||
|
||||
// convert to number
|
||||
val = parseInt(val, 10);
|
||||
if(isNaN(val)) {
|
||||
val = 0;
|
||||
}
|
||||
|
||||
assert(val >= 0 && val <= 256);
|
||||
|
||||
var attr = '';
|
||||
if(7 == val) {
|
||||
attr = sgr('normal');
|
||||
} else if (val < 7 || val >= 16) {
|
||||
attr = sgr(['normal', val]);
|
||||
} else if (val <= 15) {
|
||||
attr = sgr(['normal', val - 8, 'bold']);
|
||||
}
|
||||
|
||||
result += s.substr(lastIndex, m.index - lastIndex) + attr;
|
||||
lastIndex = re.lastIndex;
|
||||
}
|
||||
|
||||
result = (0 === result.length ? s : result + s.substr(lastIndex));
|
||||
|
||||
return result;
|
||||
}
|
672
core/art.js
Normal file
672
core/art.js
Normal file
|
@ -0,0 +1,672 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var paths = require('path');
|
||||
var assert = require('assert');
|
||||
var iconv = require('iconv-lite');
|
||||
var conf = require('./config.js');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
var binary = require('binary');
|
||||
var events = require('events');
|
||||
var util = require('util');
|
||||
var ansi = require('./ansi_term.js');
|
||||
var aep = require('./ansi_escape_parser.js');
|
||||
|
||||
exports.getArt = getArt;
|
||||
exports.getArtFromPath = getArtFromPath;
|
||||
exports.display = display;
|
||||
exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
|
||||
exports.ArtDisplayer = ArtDisplayer;
|
||||
|
||||
|
||||
var SAUCE_SIZE = 128;
|
||||
var SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE'
|
||||
var COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT'
|
||||
|
||||
// :TODO: Return MCI code information
|
||||
// :TODO: process SAUCE comments
|
||||
// :TODO: return font + font mapped information from SAUCE
|
||||
|
||||
var SUPPORTED_ART_TYPES = {
|
||||
// :TODO: the defualt encoding are really useless if they are all the same ...
|
||||
// perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
|
||||
'.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a },
|
||||
'.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a },
|
||||
'.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a },
|
||||
'.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a },
|
||||
'.txt' : { name : 'Text', defaultEncoding : 'cp437', eof : 0x1a }, // :TODO: think about this more...
|
||||
// :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
|
||||
// :TODO: extension for atari
|
||||
// :TODO: extension for topaz ansi/ascii.
|
||||
};
|
||||
|
||||
//
|
||||
// See
|
||||
// http://www.acid.org/info/sauce/sauce.htm
|
||||
//
|
||||
// :TODO: Move all SAUCE stuff to sauce.js
|
||||
function readSAUCE(data, cb) {
|
||||
if(data.length < SAUCE_SIZE) {
|
||||
cb(new Error('No SAUCE record present'));
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = data.length - SAUCE_SIZE;
|
||||
var sauceRec = data.slice(offset);
|
||||
|
||||
binary.parse(sauceRec)
|
||||
.buffer('id', 5)
|
||||
.buffer('version', 2)
|
||||
.buffer('title', 35)
|
||||
.buffer('author', 20)
|
||||
.buffer('group', 20)
|
||||
.buffer('date', 8)
|
||||
.word32lu('fileSize')
|
||||
.word8('dataType')
|
||||
.word8('fileType')
|
||||
.word16lu('tinfo1')
|
||||
.word16lu('tinfo2')
|
||||
.word16lu('tinfo3')
|
||||
.word16lu('tinfo4')
|
||||
.word8('numComments')
|
||||
.word8('flags')
|
||||
.buffer('tinfos', 22) // SAUCE 00.5
|
||||
.tap(function onVars(vars) {
|
||||
|
||||
if(!SAUCE_ID.equals(vars.id)) {
|
||||
cb(new Error('No SAUCE record present'));
|
||||
return;
|
||||
}
|
||||
|
||||
var ver = vars.version.toString('cp437');
|
||||
|
||||
if('00' !== ver) {
|
||||
cb(new Error('Unsupported SAUCE version: ' + ver));
|
||||
return;
|
||||
}
|
||||
|
||||
var sauce = {
|
||||
id : vars.id.toString('cp437'),
|
||||
version : vars.version.toString('cp437'),
|
||||
title : vars.title.toString('cp437').trim(),
|
||||
author : vars.author.toString('cp437').trim(),
|
||||
group : vars.group.toString('cp437').trim(),
|
||||
date : vars.date.toString('cp437').trim(),
|
||||
fileSize : vars.fileSize,
|
||||
dataType : vars.dataType,
|
||||
fileType : vars.fileType,
|
||||
tinfo1 : vars.tinfo1,
|
||||
tinfo2 : vars.tinfo2,
|
||||
tinfo3 : vars.tinfo3,
|
||||
tinfo4 : vars.tinfo4,
|
||||
numComments : vars.numComments,
|
||||
flags : vars.flags,
|
||||
tinfos : vars.tinfos,
|
||||
};
|
||||
|
||||
var dt = SAUCE_DATA_TYPES[sauce.dataType];
|
||||
if(dt && dt.parser) {
|
||||
sauce[dt.name] = dt.parser(sauce);
|
||||
}
|
||||
|
||||
cb(null, sauce);
|
||||
});
|
||||
}
|
||||
|
||||
// :TODO: These need completed:
|
||||
var SAUCE_DATA_TYPES = {};
|
||||
SAUCE_DATA_TYPES[0] = { name : 'None' };
|
||||
SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE };
|
||||
SAUCE_DATA_TYPES[2] = 'Bitmap';
|
||||
SAUCE_DATA_TYPES[3] = 'Vector';
|
||||
SAUCE_DATA_TYPES[4] = 'Audio';
|
||||
SAUCE_DATA_TYPES[5] = 'BinaryText';
|
||||
SAUCE_DATA_TYPES[6] = 'XBin';
|
||||
SAUCE_DATA_TYPES[7] = 'Archive';
|
||||
SAUCE_DATA_TYPES[8] = 'Executable';
|
||||
|
||||
var SAUCE_CHARACTER_FILE_TYPES = {};
|
||||
SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII';
|
||||
SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi';
|
||||
SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation';
|
||||
SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script';
|
||||
SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard';
|
||||
SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar';
|
||||
SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML';
|
||||
SAUCE_CHARACTER_FILE_TYPES[7] = 'Source';
|
||||
SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw';
|
||||
|
||||
//
|
||||
// Map of SAUCE font -> encoding hint
|
||||
//
|
||||
// Note that this is the same mapping that x84 uses. Be compatible!
|
||||
//
|
||||
var SAUCE_FONT_TO_ENCODING_HINT = {
|
||||
'Amiga MicroKnight' : 'amiga',
|
||||
'Amiga MicroKnight+' : 'amiga',
|
||||
'Amiga mOsOul' : 'amiga',
|
||||
'Amiga P0T-NOoDLE' : 'amiga',
|
||||
'Amiga Topaz 1' : 'amiga',
|
||||
'Amiga Topaz 1+' : 'amiga',
|
||||
'Amiga Topaz 2' : 'amiga',
|
||||
'Amiga Topaz 2+' : 'amiga',
|
||||
'Atari ATASCII' : 'atari',
|
||||
'IBM EGA43' : 'cp437',
|
||||
'IBM EGA' : 'cp437',
|
||||
'IBM VGA25G' : 'cp437',
|
||||
'IBM VGA50' : 'cp437',
|
||||
'IBM VGA' : 'cp437',
|
||||
};
|
||||
|
||||
['437', '720', '737', '775', '819', '850', '852', '855', '857', '858',
|
||||
'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) {
|
||||
var codec = 'cp' + page;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec;
|
||||
SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec;
|
||||
});
|
||||
|
||||
function parseCharacterSAUCE(sauce) {
|
||||
var result = {};
|
||||
|
||||
result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown';
|
||||
|
||||
if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) {
|
||||
var i = 0;
|
||||
while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) {
|
||||
++i;
|
||||
}
|
||||
var fontName = sauce.tinfos.slice(0, i).toString('cp437');
|
||||
if(fontName.length > 0) {
|
||||
result.fontName = fontName;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function sliceAtEOF(data, eofMarker) {
|
||||
var eof = data.length;
|
||||
// :TODO: max scan back or other beter way of doing this?!
|
||||
for(var i = data.length - 1; i > 0; i--) {
|
||||
if(data[i] === eofMarker) {
|
||||
eof = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data.slice(0, eof);
|
||||
}
|
||||
|
||||
function getArtFromPath(path, options, cb) {
|
||||
fs.readFile(path, function onData(err, data) {
|
||||
if(err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Convert from encodedAs -> j
|
||||
//
|
||||
var ext = paths.extname(path).toLowerCase();
|
||||
var encoding = options.encodedAs || defaultEncodingFromExtension(ext);
|
||||
|
||||
// :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?
|
||||
|
||||
function sliceOfData() {
|
||||
if(options.fullFile === true) {
|
||||
return iconv.decode(data, encoding);
|
||||
} else {
|
||||
var eofMarker = defaultEofFromExtension(ext);
|
||||
return iconv.decode(sliceAtEOF(data, eofMarker), encoding);
|
||||
}
|
||||
}
|
||||
|
||||
function getResult(sauce) {
|
||||
var result = {
|
||||
data : sliceOfData(),
|
||||
fromPath : path,
|
||||
};
|
||||
|
||||
if(sauce) {
|
||||
result.sauce = sauce;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if(options.readSauce === true) {
|
||||
readSAUCE(data, function onSauce(err, sauce) {
|
||||
if(err) {
|
||||
cb(null, getResult());
|
||||
} else {
|
||||
//
|
||||
// If a encoding was not provided & we have a mapping from
|
||||
// the information provided by SAUCE, use that.
|
||||
//
|
||||
if(!options.encodedAs) {
|
||||
if(sauce.Character && sauce.Character.fontName) {
|
||||
var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
|
||||
if(enc) {
|
||||
encoding = enc;
|
||||
}
|
||||
}
|
||||
}
|
||||
cb(null, getResult(sauce));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cb(null, getResult());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getArt(name, options, cb) {
|
||||
var ext = paths.extname(name);
|
||||
|
||||
options.basePath = miscUtil.valueWithDefault(options.basePath, conf.config.paths.art);
|
||||
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
|
||||
|
||||
// :TODO: make use of asAnsi option and convert from supported -> ansi
|
||||
|
||||
if('' !== ext) {
|
||||
options.types = [ ext.toLowerCase() ];
|
||||
} else {
|
||||
if(typeof options.types === 'undefined') {
|
||||
options.types = Object.keys(SUPPORTED_ART_TYPES);
|
||||
} else if(typeof options.types === 'string') {
|
||||
options.types = [ options.types.toLowerCase() ];
|
||||
}
|
||||
}
|
||||
|
||||
// If an extension is provided, just read the file now
|
||||
if('' !== ext) {
|
||||
var directPath = paths.join(options.basePath, name);
|
||||
getArtFromPath(directPath, options, cb);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readdir(options.basePath, function onFiles(err, files) {
|
||||
if(err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var filtered = files.filter(function onFile(file) {
|
||||
//
|
||||
// Ignore anything not allowed in |options.types|
|
||||
//
|
||||
var fext = paths.extname(file);
|
||||
if(options.types.indexOf(fext.toLowerCase()) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var bn = paths.basename(file, fext).toLowerCase();
|
||||
if(options.random) {
|
||||
var suppliedBn = paths.basename(name, fext).toLowerCase();
|
||||
//
|
||||
// Random selection enabled. We'll allow for
|
||||
// basename1.ext, basename2.ext, ...
|
||||
//
|
||||
if(bn.indexOf(suppliedBn) !== 0) {
|
||||
return false;
|
||||
}
|
||||
var num = bn.substr(suppliedBn.length);
|
||||
if(num.length > 0) {
|
||||
if(isNaN(parseInt(num, 10))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// We've already validated the extension (above). Must be an exact
|
||||
// match to basename here
|
||||
//
|
||||
if(bn != paths.basename(name, fext).toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if(filtered.length > 0) {
|
||||
//
|
||||
// We should now have:
|
||||
// - Exactly (1) item in |filtered| if non-random
|
||||
// - 1:n items in |filtered| to choose from if random
|
||||
//
|
||||
var readPath;
|
||||
if(options.random) {
|
||||
readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]);
|
||||
} else {
|
||||
assert(1 === filtered.length);
|
||||
readPath = paths.join(options.basePath, filtered[0]);
|
||||
}
|
||||
|
||||
getArtFromPath(readPath, options, cb);
|
||||
} else {
|
||||
cb(new Error('No matching art for supplied criteria'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// :TODO: need a showArt()
|
||||
// - center (if term width > 81)
|
||||
// - interruptable
|
||||
// - pausable: by user key and/or by page size (e..g term height)
|
||||
|
||||
|
||||
function defaultEncodingFromExtension(ext) {
|
||||
return SUPPORTED_ART_TYPES[ext.toLowerCase()].defaultEncoding;
|
||||
}
|
||||
|
||||
function defaultEofFromExtension(ext) {
|
||||
return SUPPORTED_ART_TYPES[ext.toLowerCase()].eof;
|
||||
}
|
||||
|
||||
function ArtDisplayer(client) {
|
||||
if(!(this instanceof ArtDisplayer)) {
|
||||
return new ArtDisplayer(client);
|
||||
}
|
||||
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
util.inherits(ArtDisplayer, events.EventEmitter);
|
||||
|
||||
// :TODO: change to display(art, options, cb)
|
||||
// cb(err, mci)
|
||||
|
||||
function display(art, options, cb) {
|
||||
if(!art || 0 === art.length) {
|
||||
cb(new Error('Missing or empty art'));
|
||||
return;
|
||||
}
|
||||
|
||||
if('undefined' === typeof options) {
|
||||
cb(new Error('Missing options'));
|
||||
return;
|
||||
}
|
||||
|
||||
if('undefined' === typeof options.client) {
|
||||
cb(new Error('Missing client in options'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cancelKeys = miscUtil.valueWithDefault(options.cancelKeys, []);
|
||||
var pauseKeys = miscUtil.valueWithDefault(options.pauseKeys, []);
|
||||
var pauseAtTermHeight = miscUtil.valueWithDefault(options.pauseAtTermHeight, false);
|
||||
var mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
||||
|
||||
// :TODO: support pause/cancel & pause @ termHeight
|
||||
var canceled = false;
|
||||
|
||||
var parser = new aep.ANSIEscapeParser({
|
||||
mciReplaceChar : mciReplaceChar,
|
||||
termHeight : options.client.term.termHeight,
|
||||
termWidth : options.client.term.termWidth,
|
||||
});
|
||||
|
||||
var mci = {};
|
||||
var mciPosQueue = [];
|
||||
var emitter = null;
|
||||
var parseComplete = false;
|
||||
|
||||
parser.on('mci', function onMCI(mciCode, args) {
|
||||
if(mci[mciCode]) {
|
||||
mci[mciCode].altColor = {
|
||||
fg : parser.fgColor,
|
||||
bg : parser.bgColor,
|
||||
flags : parser.flags,
|
||||
};
|
||||
} else {
|
||||
mci[mciCode] = {
|
||||
args : args,
|
||||
color : {
|
||||
fg : parser.fgColor,
|
||||
bg : parser.bgColor,
|
||||
flags : parser.flags,
|
||||
}
|
||||
};
|
||||
|
||||
mciPosQueue.push(mciCode);
|
||||
|
||||
if(!emitter) {
|
||||
emitter = options.client.on('onPosition', function onPosition(pos) {
|
||||
if(mciPosQueue.length > 0) {
|
||||
var forMciCode = mciPosQueue.shift();
|
||||
mci[forMciCode].position = pos;
|
||||
|
||||
if(parseComplete && 0 === mciPosQueue.length) {
|
||||
cb(null, mci);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
options.client.term.write(ansi.queryPos());
|
||||
}
|
||||
});
|
||||
|
||||
parser.on('chunk', function onChunk(chunk) {
|
||||
options.client.term.write(chunk);
|
||||
});
|
||||
|
||||
parser.on('complete', function onComplete() {
|
||||
parseComplete = true;
|
||||
|
||||
if(0 === mciPosQueue.length) {
|
||||
cb(null, mci);
|
||||
}
|
||||
});
|
||||
|
||||
parser.parse(art);
|
||||
}
|
||||
|
||||
ArtDisplayer.prototype.display = function(art, options) {
|
||||
var client = this.client;
|
||||
var self = this;
|
||||
|
||||
var cancelKeys = miscUtil.valueWithDefault(options.cancelKeys, []);
|
||||
var pauseKeys = miscUtil.valueWithDefault(options.pauseKeys, []);
|
||||
var pauseAtTermHeight = miscUtil.valueWithDefault(options.pauseAtTermHeight, false);
|
||||
|
||||
var canceled = false;
|
||||
if(cancelKeys.length > 0 || pauseKeys.length > 0) {
|
||||
var onDataKeyCheck = function(data) {
|
||||
var key = String.fromCharCode(data[0]);
|
||||
if(-1 !== cancelKeys.indexOf(key)) {
|
||||
canceled = true;
|
||||
removeDataListener();
|
||||
}
|
||||
};
|
||||
client.on('data', onDataKeyCheck);
|
||||
}
|
||||
|
||||
function removeDataListener() {
|
||||
client.removeListener('data', onDataKeyCheck);
|
||||
}
|
||||
|
||||
//
|
||||
// Try to split lines supporting various linebreaks we may encounter:
|
||||
// - DOS \r\n
|
||||
// - *nix \n
|
||||
// - Old Apple \r
|
||||
// - Unicode PARAGRAPH SEPARATOR (U+2029) and LINE SEPARATOR (U+2028)
|
||||
//
|
||||
// See also http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line
|
||||
//
|
||||
var lines = art.split(/\r?\n|\r|[\u2028\u2029]/);
|
||||
var i = 0;
|
||||
var count = lines.length;
|
||||
if(0 === count) {
|
||||
return;
|
||||
}
|
||||
|
||||
var termHeight = client.term.termHeight;
|
||||
|
||||
var aep = require('./ansi_escape_parser.js');
|
||||
var p = new aep.ANSIEscapeParser();
|
||||
|
||||
var currentRow = 0;
|
||||
var lastRow = 0;
|
||||
p.on('row update', function onRowUpdated(row) {
|
||||
currentRow = row;
|
||||
});
|
||||
|
||||
//--------
|
||||
var mci = {};
|
||||
var mciPosQueue = [];
|
||||
var parseComplete = false;
|
||||
|
||||
var emitter = null;
|
||||
|
||||
p.on('mci', function onMCI(mciCode, args) {
|
||||
if(mci[mciCode]) {
|
||||
mci[mciCode].fgColorAlt = p.fgColor;
|
||||
mci[mciCode].bgColorAlt = p.bgColor;
|
||||
mci[mciCode].flagsAlt = p.flags;
|
||||
} else {
|
||||
mci[mciCode] = {
|
||||
args : args,
|
||||
fgColor : p.fgColor,
|
||||
bgColor : p.bgColor,
|
||||
flags : p.flags,
|
||||
};
|
||||
|
||||
mciPosQueue.push(mciCode);
|
||||
|
||||
if(!emitter) {
|
||||
emitter = client.on('onPosition', function onPosition(pos) {
|
||||
if(mciPosQueue.length > 0) {
|
||||
var mc = mciPosQueue.shift();
|
||||
console.log('position @ ' + mc + ': ' + pos);
|
||||
mci[mc].pos = pos;
|
||||
|
||||
if(parseComplete && 0 === mciPosQueue.length) {
|
||||
//console.log(mci);
|
||||
var p1 = mci['LV1'].pos;
|
||||
client.term.write(ansi.sgr(['red']));
|
||||
var g = ansi.goto(p1);
|
||||
console.log(g);
|
||||
client.term.write(ansi.goto(p1[0], p1[1]));
|
||||
client.term.write('Hello, World');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
p.on('chunk', function onChunk(chunk) {
|
||||
client.term.write(chunk);
|
||||
});
|
||||
|
||||
p.on('complete', function onComplete() {
|
||||
//console.log(mci);
|
||||
parseComplete = true;
|
||||
if(0 === mciPosQueue.length) {
|
||||
console.log('mci from complete');
|
||||
console.log(mci);
|
||||
}
|
||||
});
|
||||
|
||||
p.parse(art);
|
||||
|
||||
|
||||
//-----------
|
||||
/*
|
||||
var line;
|
||||
(function nextLine() {
|
||||
if(i === count) {
|
||||
self.emit('complete');
|
||||
removeDataListener();
|
||||
return;
|
||||
}
|
||||
|
||||
if(canceled) {
|
||||
self.emit('canceled');
|
||||
removeDataListener();
|
||||
return;
|
||||
}
|
||||
|
||||
line = lines[i];
|
||||
client.term.write(line + '\n');
|
||||
p.parse(line + '\r\n');
|
||||
i++;
|
||||
|
||||
if(pauseAtTermHeight && currentRow !== lastRow && (0 === currentRow % termHeight)) {
|
||||
lastRow = currentRow;
|
||||
client.getch(function onKey(k) {
|
||||
nextLine();
|
||||
});
|
||||
} else {
|
||||
setTimeout(nextLine, 20);
|
||||
}
|
||||
})();
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
(function nextLine() {
|
||||
if(i === count) {
|
||||
client.emit('complete', true);
|
||||
removeDataListener();
|
||||
return;
|
||||
}
|
||||
|
||||
if(canceled) {
|
||||
console.log('canceled');
|
||||
client.emit('canceled');
|
||||
removeDataListener();
|
||||
return;
|
||||
}
|
||||
|
||||
client.term.write(lines[i] + '\n');
|
||||
|
||||
//
|
||||
// :TODO: support pauseAtTermHeight:
|
||||
//
|
||||
// - All cursor movement should be recorded for pauseAtTermHeight support &
|
||||
// handling > termWidth scenarios
|
||||
// - MCI codes should be processed
|
||||
// - All other ANSI/CSI ignored
|
||||
// - Count normal chars
|
||||
//
|
||||
|
||||
//setTimeout(nextLine, 20);
|
||||
//i++;
|
||||
|
||||
if(pauseAtTermHeight && i > 0 && (0 === i % termHeight)) {
|
||||
console.log('pausing @ ' + i);
|
||||
client.getch(function onKey() {
|
||||
i++;
|
||||
nextLine();
|
||||
});
|
||||
} else {
|
||||
i++;
|
||||
// :TODO: If local, use setTimeout(nextLine, 20) or so -- allow to pause/cancel
|
||||
//process.nextTick(nextLine);
|
||||
setTimeout(nextLine, 20);
|
||||
}
|
||||
|
||||
})();
|
||||
*/
|
||||
};
|
||||
|
||||
//
|
||||
// ANSI parser for quick scanning & handling
|
||||
// of basic ANSI sequences that can be used for output to clients:
|
||||
//
|
||||
function ANSIOutputParser(ansi) {
|
||||
//
|
||||
// cb's
|
||||
// - onMCI
|
||||
// - onTermHeight
|
||||
// -
|
||||
}
|
155
core/bbs.js
Normal file
155
core/bbs.js
Normal file
|
@ -0,0 +1,155 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
var conf = require('./config.js');
|
||||
var modules = require('./modules.js');
|
||||
var logger = require('./logger.js');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
|
||||
var iconv = require('iconv-lite');
|
||||
var paths = require('path');
|
||||
|
||||
exports.bbsMain = function() {
|
||||
var mainArgs = parseArgs();
|
||||
|
||||
var configPathSupplied = false;
|
||||
var configPath = conf.defaultPath();
|
||||
|
||||
if(mainArgs.indexOf('--help') > 0) {
|
||||
// :TODO: display help
|
||||
} else {
|
||||
var argCount = mainArgs.length;
|
||||
for(var i = 0; i < argCount; ++i) {
|
||||
var arg = mainArgs[i];
|
||||
if('--config' == arg) {
|
||||
configPathSupplied = true;
|
||||
configPath = mainArgs[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
conf.initFromFile(configPath);
|
||||
} catch(e) {
|
||||
//
|
||||
// If the user supplied a config and we can't read, parse, whatever
|
||||
// then output a error and bail.
|
||||
//
|
||||
if(configPathSupplied) {
|
||||
if(e.code === 'ENOENT') {
|
||||
console.error('Configuration file does not exist: ' + configPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('No configuration file found, creating defaults.');
|
||||
conf.createDefault();
|
||||
}
|
||||
|
||||
logger.init();
|
||||
|
||||
preServingInit();
|
||||
|
||||
startListening();
|
||||
|
||||
process.on('SIGINT', function onSigInt() {
|
||||
// :TODO: for any client in |clientConnections|, if 'ready', send a "Server Disconnecting" + semi-gracefull hangup
|
||||
// e.g. client.disconnectNow()
|
||||
|
||||
logger.log.info('Process interrupted, shutting down');
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
|
||||
function parseArgs() {
|
||||
var args = [];
|
||||
process.argv.slice(2).forEach(function(val, index, array) {
|
||||
args.push(val);
|
||||
});
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function preServingInit() {
|
||||
iconv.extendNodeEncodings();
|
||||
}
|
||||
|
||||
var clientConnections = [];
|
||||
|
||||
function startListening() {
|
||||
if(!conf.config.servers) {
|
||||
// :TODO: Log error ... output to stderr as well. We can do it all with the logger
|
||||
return [];
|
||||
}
|
||||
|
||||
modules.loadModulesForCategory('servers', function onServerModule(err, module) {
|
||||
if(err) {
|
||||
logger.log.info(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var port = parseInt(module.runtime.config.port);
|
||||
if(isNaN(port)) {
|
||||
logger.log.error({ port : module.runtime.config.port, server : module.moduleInfo.name }, 'Cannot load server (Invalid port)');
|
||||
return;
|
||||
}
|
||||
|
||||
var server = module.createServer();
|
||||
|
||||
// :TODO: handle maxConnections, e.g. conf.maxConnections
|
||||
|
||||
server.on('client', function onClient(client) {
|
||||
//
|
||||
// Start tracking the client. We'll assign it an ID which is
|
||||
// just the index in our connections array.
|
||||
//
|
||||
if(typeof client.runtime === 'undefined') {
|
||||
client.runtime = {};
|
||||
}
|
||||
|
||||
addNewClient(client);
|
||||
//client.runtime.id = clientConnections.push(client) - 1;
|
||||
|
||||
//logger.log.info({ clientId : client.runtime.id, from : client.address(), server : module.moduleInfo.name }, 'Client connected');
|
||||
|
||||
client.on('ready', function onClientReady() {
|
||||
// Go to module -- use default error handler
|
||||
modules.goto(conf.config.entryMod, client);
|
||||
});
|
||||
|
||||
client.on('end', function onClientEnd() {
|
||||
logger.log.info({ clientId : client.runtime.id }, 'Client disconnected');
|
||||
|
||||
removeClient(client);
|
||||
});
|
||||
|
||||
client.on('error', function onClientError(err) {
|
||||
logger.log.info({ clientId : client.runtime.id }, 'Connection error: %s' % err.message);
|
||||
});
|
||||
|
||||
client.on('close', function onClientClose(hadError) {
|
||||
var l = hadError ? logger.log.info : logger.log.debug;
|
||||
l({ clientId : client.runtime.id }, 'Connection closed');
|
||||
removeClient(client);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
logger.log.info({ server : module.moduleInfo.name, port : port }, 'Listening for connections');
|
||||
});
|
||||
}
|
||||
|
||||
function addNewClient(client) {
|
||||
var id = client.runtime.id = clientConnections.push(client) - 1;
|
||||
logger.log.debug('Connection count is now %d', clientConnections.length);
|
||||
return id;
|
||||
}
|
||||
|
||||
function removeClient(client) {
|
||||
var i = clientConnections.indexOf(client);
|
||||
if(i > -1) {
|
||||
clientConnections.splice(i, 1);
|
||||
logger.log.debug('Connection count is now %d', clientConnections.length);
|
||||
}
|
||||
}
|
89
core/client.js
Normal file
89
core/client.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var stream = require('stream');
|
||||
var term = require('./client_term.js');
|
||||
var assert = require('assert');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
var ansi = require('./ansi_term.js');
|
||||
var logger = require('./logger.js');
|
||||
|
||||
exports.Client = Client;
|
||||
|
||||
function Client(input, output) {
|
||||
stream.call(this);
|
||||
|
||||
var self = this;
|
||||
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
this.term = new term.ClientTerminal(this.output);
|
||||
|
||||
self.on('data', function onData(data) {
|
||||
console.log('data: ' + data.length);
|
||||
handleANSIControlResponse(data);
|
||||
});
|
||||
|
||||
function handleANSIControlResponse(data) {
|
||||
console.log(data);
|
||||
ansi.forEachControlCode(data, function onControlResponse(name, params) {
|
||||
var eventName = 'on' + name[0].toUpperCase() + name.substr(1);
|
||||
console.log(eventName + ': ' + params);
|
||||
self.emit(eventName, params);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
require('util').inherits(Client, stream);
|
||||
|
||||
Client.prototype.end = function () {
|
||||
return this.output.end.apply(this.output, arguments);
|
||||
};
|
||||
|
||||
Client.prototype.destroy = function () {
|
||||
return this.output.destroy.apply(this.output, arguments);
|
||||
};
|
||||
|
||||
Client.prototype.destroySoon = function () {
|
||||
return this.output.destroySoon.apply(this.output, arguments);
|
||||
};
|
||||
|
||||
Client.prototype.getch = function(cb) {
|
||||
this.input.once('data', function onData(data) {
|
||||
// :TODO: needs work. What about F keys and the like?
|
||||
assert(data.length === 1);
|
||||
cb(data);
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.address = function() {
|
||||
return this.input.address();
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Default error handlers
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Client.prototype.defaultHandlerMissingMod = function(err) {
|
||||
var self = this;
|
||||
|
||||
function handler(err) {
|
||||
logger.log.error(err);
|
||||
|
||||
self.term.write('An unrecoverable error has been encountered!\n');
|
||||
self.term.write('This has been logged for your SysOp to review.\n');
|
||||
self.term.write('\nGoodbye!\n');
|
||||
|
||||
|
||||
//self.term.write(err);
|
||||
|
||||
//if(miscUtil.isDevelopment() && err.stack) {
|
||||
// self.term.write('\n' + err.stack + '\n');
|
||||
//}
|
||||
|
||||
self.end();
|
||||
}
|
||||
|
||||
return handler;
|
||||
};
|
||||
|
101
core/client_term.js
Normal file
101
core/client_term.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
var logger = require('./logger.js');
|
||||
|
||||
var iconv = require('iconv-lite');
|
||||
var assert = require('assert');
|
||||
|
||||
iconv.extendNodeEncodings();
|
||||
|
||||
exports.ClientTerminal = ClientTerminal;
|
||||
|
||||
function ClientTerminal(output) {
|
||||
this.output = output;
|
||||
|
||||
var self = this;
|
||||
|
||||
var outputEncoding = 'cp437';
|
||||
assert(iconv.encodingExists(outputEncoding));
|
||||
|
||||
// convert line feeds such as \n -> \r\n
|
||||
this.convertLF = true;
|
||||
|
||||
//
|
||||
// Some terminal we handle specially
|
||||
// They can also be found in this.env{}
|
||||
//
|
||||
var termType = 'unknown';
|
||||
var termHeight = 0;
|
||||
var termWidth = 0;
|
||||
|
||||
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
|
||||
this.env = {};
|
||||
|
||||
Object.defineProperty(this, 'outputEncoding', {
|
||||
get : function() {
|
||||
return outputEncoding;
|
||||
},
|
||||
set : function(enc) {
|
||||
if(iconv.encodingExists(enc)) {
|
||||
outputEncoding = enc;
|
||||
} else {
|
||||
logger.log.warn({ encoding : enc }, 'Unknown encoding');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'termType', {
|
||||
get : function() {
|
||||
return termType;
|
||||
},
|
||||
set : function(ttype) {
|
||||
termType = ttype.toLowerCase();
|
||||
|
||||
//
|
||||
// ANSI terminals should be encoded to CP437
|
||||
//
|
||||
if('ansi' == termType) {
|
||||
this.outputEncoding = 'cp437';
|
||||
} else {
|
||||
// :TODO: See how x84 does this -- only set if local/remote are binary
|
||||
this.outputEncoding = 'utf8';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'termWidth', {
|
||||
get : function() {
|
||||
return termWidth;
|
||||
},
|
||||
set : function(width) {
|
||||
if(width > 0) {
|
||||
termWidth = width;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'termHeight', {
|
||||
get : function() {
|
||||
return termHeight;
|
||||
},
|
||||
set : function(height) {
|
||||
if(height > 0) {
|
||||
termHeight = height;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientTerminal.prototype.isANSI = function() {
|
||||
return 'ansi' === this.termType;
|
||||
};
|
||||
|
||||
ClientTerminal.prototype.write = function(s) {
|
||||
if(this.convertLF && typeof s === 'string') {
|
||||
s = s.replace(/\n/g, '\r\n');
|
||||
}
|
||||
|
||||
this.output.write(iconv.encode(s, this.outputEncoding));
|
||||
};
|
50
core/config.js
Normal file
50
core/config.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var paths = require('path');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
|
||||
module.exports = {
|
||||
config : undefined,
|
||||
|
||||
defaultPath : function() {
|
||||
var base = miscUtil.resolvePath('~/');
|
||||
if(base) {
|
||||
return paths.join(base, '.enigmabbs', 'config.json');
|
||||
}
|
||||
},
|
||||
|
||||
initFromFile : function(path, cb) {
|
||||
var data = fs.readFileSync(path, 'utf8');
|
||||
this.config = JSON.parse(data);
|
||||
},
|
||||
|
||||
createDefault : function() {
|
||||
this.config = {
|
||||
bbsName : 'Another Fine ENiGMA½ BBS',
|
||||
|
||||
entryMod : 'connect',
|
||||
|
||||
paths : {
|
||||
mods : paths.join(__dirname, './../mods/'),
|
||||
servers : paths.join(__dirname, './servers/'),
|
||||
art : paths.join(__dirname, './../mods/art/'),
|
||||
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such
|
||||
},
|
||||
|
||||
servers : {
|
||||
telnet : {
|
||||
port : 8888,
|
||||
enabled : true,
|
||||
},
|
||||
ssh : {
|
||||
port : 8889,
|
||||
enabled : false,
|
||||
rsaPrivateKey : paths.join(__dirname, './../misc/default_key.rsa'),
|
||||
dsaPrivateKey : paths.join(__dirname, './../misc/default_key.dsa'),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
64
core/line_editor.js
Normal file
64
core/line_editor.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
"use strict";
|
||||
|
||||
var assert = require('assert');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
|
||||
exports.LineEditor = LineEditor;
|
||||
|
||||
var STANDARD_KEYSET = {
|
||||
refresh : [ 12 ],
|
||||
backspace : [ 8, 127 ],
|
||||
backword : [ 23 ],
|
||||
enter : [ 10 ],
|
||||
exit : [ 27 ],
|
||||
};
|
||||
|
||||
// :TODO: Rename to TextEdit
|
||||
// :TODO: TextEdit should be single or multi line
|
||||
|
||||
|
||||
function LineEditor(client, options) {
|
||||
var self = this;
|
||||
|
||||
self.client = client;
|
||||
self.valueText = '';
|
||||
|
||||
if(typeof options !== 'undefined') {
|
||||
self.options.keyset = miscUtil.valueWithDefault(options.keyset, STANDARD_KEYSET);
|
||||
} else {
|
||||
self.options = {
|
||||
keyset : STANDARD_KEYSET,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
this.client.on('data', function onData(data) {
|
||||
assert(1 === data.length);
|
||||
self.onCh(data);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
LineEditor.prototype.isKey = function(setName, ch) {
|
||||
return this.options.keyset[setName].indexOf(ch) > -1;
|
||||
}
|
||||
|
||||
LineEditor.prototype.onCh = function(ch) {
|
||||
if(this.isKey('refresh', ch)) {
|
||||
|
||||
} else if(this.isKey('backspace', ch)) {
|
||||
|
||||
} else if(this.isKey('backword', ch)) {
|
||||
|
||||
} else if(this.isKey('enter', ch)) {
|
||||
|
||||
} else if(this.isKey('exit', ch)) {
|
||||
|
||||
} else {
|
||||
|
||||
// :TODO: filter out chars
|
||||
// :TODO: check max width
|
||||
this.valueText += ch;
|
||||
this.client.term.write(ch);
|
||||
}
|
||||
};
|
39
core/logger.js
Normal file
39
core/logger.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
var bunyan = require('bunyan');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
var paths = require('path');
|
||||
var conf = require('./config.js');
|
||||
|
||||
module.exports = {
|
||||
log : undefined,
|
||||
|
||||
init : function() {
|
||||
//var ringBufferLimit = miscUtil.valueWithDefault(config.logRingBufferLimit, 100);
|
||||
var logPath = miscUtil.valueWithDefault(conf.config.paths.logs);
|
||||
var logFile = paths.join(logPath, 'enigma-bbs.log');
|
||||
|
||||
// :TODO: make this configurable --
|
||||
// user should be able to configure rotations, levels to file vs ringBuffer,
|
||||
// completely disable logging, etc.
|
||||
|
||||
this.log = bunyan.createLogger({
|
||||
name : 'ENiGMA½ BBS',
|
||||
streams : [
|
||||
{
|
||||
type : 'rotating-file',
|
||||
path : logFile,
|
||||
period : '1d',
|
||||
count : 3,
|
||||
level : 'trace'
|
||||
}
|
||||
/*,
|
||||
{
|
||||
type : 'raw',
|
||||
stream : ringBuffer,
|
||||
level : 'trace'
|
||||
}*/
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
28
core/misc_util.js
Normal file
28
core/misc_util.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
"use strict";
|
||||
|
||||
var paths = require('path');
|
||||
|
||||
exports.isProduction = isProduction;
|
||||
exports.isDevelopment = isDevelopment;
|
||||
exports.valueWithDefault = valueWithDefault;
|
||||
exports.resolvePath = resolvePath;
|
||||
|
||||
function isProduction() {
|
||||
var env = process.env.NODE_ENV || 'dev';
|
||||
return 'production' === env;
|
||||
};
|
||||
|
||||
function isDevelopment() {
|
||||
return (!(isProduction()));
|
||||
};
|
||||
|
||||
function valueWithDefault(val, defVal) {
|
||||
return (typeof val !== 'undefined' ? val : defVal);
|
||||
};
|
||||
|
||||
function resolvePath(path) {
|
||||
if(path.substr(0, 2) === '~/') {
|
||||
path = (process.env.HOME || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1);
|
||||
}
|
||||
return paths.resolve(path);
|
||||
};
|
85
core/modules.js
Normal file
85
core/modules.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var paths = require('path');
|
||||
var conf = require('./config.js');
|
||||
var miscUtil = require('./misc_util.js');
|
||||
var logger = require('./logger.js');
|
||||
|
||||
// exports
|
||||
exports.loadModule = loadModule;
|
||||
exports.loadModulesForCategory = loadModulesForCategory;
|
||||
|
||||
exports.goto = goto;
|
||||
|
||||
function loadModule(name, category, cb) {
|
||||
var config = conf.config;
|
||||
var path = config.paths[category];
|
||||
|
||||
if(!path) {
|
||||
cb(new Error('not sure where to look for "' + name + '" of category "' + category + '"'));
|
||||
return;
|
||||
}
|
||||
|
||||
// update conf to point at this module's section, if any
|
||||
config = config[category] ? config[category][name] : null;
|
||||
|
||||
if(config && false === config.enabled) {
|
||||
cb(new Error('module "' + name + '" is disabled'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var mod = require(paths.join(path, name + '.js'));
|
||||
|
||||
if(!mod.moduleInfo) {
|
||||
cb(new Error('module is missing \'moduleInfo\' section'));
|
||||
return;
|
||||
}
|
||||
|
||||
mod.runtime = {
|
||||
config : config
|
||||
};
|
||||
|
||||
cb(null, mod);
|
||||
} catch(e) {
|
||||
cb(e);
|
||||
}
|
||||
};
|
||||
|
||||
function loadModulesForCategory(category, cb) {
|
||||
var path = conf.config.paths[category];
|
||||
|
||||
fs.readdir(path, function onFiles(err, files) {
|
||||
if(err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var filtered = files.filter(function onFilter(file) { return '.js' === paths.extname(file); });
|
||||
filtered.forEach(function onFile(file) {
|
||||
var modName = paths.basename(file, '.js');
|
||||
loadModule(paths.basename(file, '.js'), category, cb);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function goto(name, client, cb) {
|
||||
// Assign a default missing module handler callback if none was provided
|
||||
cb = miscUtil.valueWithDefault(cb, client.defaultHandlerMissingMod());
|
||||
|
||||
loadModule(name, 'mods', function onMod(err, mod) {
|
||||
if(err) {
|
||||
cb(err);
|
||||
} else {
|
||||
try {
|
||||
logger.log.debug({ moduleName : name }, 'Goto module');
|
||||
mod.entryPoint(client);
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
30
core/servers/ssh.js
Normal file
30
core/servers/ssh.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
"use strict";
|
||||
|
||||
var libssh = require('ssh');
|
||||
var conf = require('../config.js');
|
||||
|
||||
/*
|
||||
Notes on getting libssh to work. This will ultimately require some contribs back
|
||||
* Can't install without --nodedir= as had to upgrade node on the box for other reasons
|
||||
* From ssh dir, node-gyp --nodedir=... configure build
|
||||
* nan is out of date and doesn't work with existing node. Had to update. ( was "~0.6.0") (npm update after this)
|
||||
*
|
||||
*/
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'SSH',
|
||||
desc : 'SSH Server',
|
||||
author : 'NuSkooler'
|
||||
};
|
||||
|
||||
function createServer() {
|
||||
var server = libssh.createServer(
|
||||
conf.config.servers.ssh.rsaPrivateKey,
|
||||
conf.config.servers.ssh.dsaPrivateKey);
|
||||
|
||||
server.on('connection', function onConnection(session) {
|
||||
console.log('ermergerd')
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
729
core/servers/telnet.js
Normal file
729
core/servers/telnet.js
Normal file
|
@ -0,0 +1,729 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
var baseClient = require('../client.js');
|
||||
var logger = require('../logger.js');
|
||||
|
||||
var net = require('net');
|
||||
var buffers = require('buffers');
|
||||
var binary = require('binary');
|
||||
var stream = require('stream');
|
||||
var assert = require('assert');
|
||||
|
||||
//var debug = require('debug')('telnet');
|
||||
|
||||
exports.moduleInfo = {
|
||||
name : 'Telnet',
|
||||
desc : 'Telnet Server',
|
||||
author : 'NuSkooler'
|
||||
};
|
||||
|
||||
exports.createServer = createServer;
|
||||
|
||||
//
|
||||
// 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!!
|
||||
|
||||
|
||||
*/
|
||||
|
||||
var 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
|
||||
//
|
||||
var SB_COMMANDS = {
|
||||
IS : 0,
|
||||
SEND : 1,
|
||||
INFO : 2,
|
||||
};
|
||||
|
||||
//
|
||||
// Telnet Options
|
||||
//
|
||||
// Resources
|
||||
// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html
|
||||
//
|
||||
var 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]
|
||||
var NEW_ENVIRONMENT_COMMANDS = {
|
||||
VAR : 0,
|
||||
VALUE : 1,
|
||||
ESC : 2,
|
||||
USERVAR : 3,
|
||||
};
|
||||
|
||||
var IAC_BUF = new Buffer([ COMMANDS.IAC ]);
|
||||
var SB_BUF = new Buffer([ COMMANDS.SB ]);
|
||||
var SE_BUF = new Buffer([ COMMANDS.SE ]);
|
||||
var IAC_SE_BUF = new Buffer([ COMMANDS.IAC, COMMANDS.SE ]);
|
||||
|
||||
var COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) {
|
||||
names[COMMANDS[name]] = name.toLowerCase();
|
||||
return names;
|
||||
}, {});
|
||||
|
||||
var COMMAND_IMPLS = {};
|
||||
['do', 'dont', 'will', 'wont', 'sb'].forEach(function(command) {
|
||||
var 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
|
||||
var OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) {
|
||||
names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' ');
|
||||
return names;
|
||||
}, {});
|
||||
|
||||
var 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.TERMINAL_TYPE] =
|
||||
OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] =
|
||||
OPTION_IMPLS[OPTIONS.X_DISPLAY_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;
|
||||
}
|
||||
|
||||
var end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes
|
||||
if(-1 === end) {
|
||||
return MORE_DATA_REQUIRED;
|
||||
}
|
||||
|
||||
// eat up and process the header
|
||||
var 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). Client such as Netrunner do this.
|
||||
//
|
||||
var 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]
|
||||
var 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 + payload + IAC SE
|
||||
if(bufs.length < 7) {
|
||||
return MORE_DATA_REQUIRED;
|
||||
}
|
||||
|
||||
var end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes
|
||||
if(-1 === end) {
|
||||
return MORE_DATA_REQUIRED;
|
||||
}
|
||||
|
||||
// eat up and process the header
|
||||
var 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) {
|
||||
logger.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
|
||||
var params = [];
|
||||
var p = 0;
|
||||
for(var 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));
|
||||
}
|
||||
|
||||
var varName;
|
||||
event.envVars = {};
|
||||
// :TODO: handle cases where a variable was present in a previous exchange, but missing here...e.g removed
|
||||
for(var j = 0; j < params.length; ++j) {
|
||||
if(params[j].length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var 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;
|
||||
};
|
||||
|
||||
var 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) {
|
||||
var command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same
|
||||
event.commandCode = command;
|
||||
event.command = COMMAND_NAMES[command];
|
||||
|
||||
var handler = COMMAND_IMPLS[command];
|
||||
if(handler) {
|
||||
//return COMMAND_IMPLS[command](bufs, i + 1, event);
|
||||
return handler(bufs, i + 1, event);
|
||||
} else {
|
||||
assert(2 == bufs.length); // IAC + COMMAND
|
||||
event.buf = bufs.splice(0, 2).toBuffer();
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
function parseOption(bufs, i, event) {
|
||||
var 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);
|
||||
|
||||
var self = this;
|
||||
|
||||
var bufs = buffers();
|
||||
this.bufs = bufs;
|
||||
|
||||
var readyFired = false;
|
||||
var encodingSet = false;
|
||||
|
||||
this.negotiationsComplete = false; // are we in the 'negotiation' phase?
|
||||
this.didReady = false; // have we emit the 'ready' event?
|
||||
|
||||
this.input.on('data', function onData(b) {
|
||||
bufs.push(b);
|
||||
|
||||
var i;
|
||||
while((i = bufs.indexOf(IAC_BUF)) >= 0) {
|
||||
// :TODO: Android client Irssi ConnectBot asserts here:
|
||||
assert(bufs.length > (i + 1), 'bufs.length=' + bufs.length + ' i=' + i + ' bufs=' + bufs);
|
||||
|
||||
if(i > 0) {
|
||||
self.emit('data', bufs.splice(0, i).toBuffer());
|
||||
}
|
||||
|
||||
i = parseBufs(bufs);
|
||||
|
||||
if(MORE_DATA_REQUIRED === i) {
|
||||
break;
|
||||
} else {
|
||||
//self.emit('event', i); // generic event
|
||||
//self.emit(i.command, i); // "will", "wont", ...
|
||||
|
||||
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', function() {
|
||||
self.emit('end');
|
||||
});
|
||||
}
|
||||
|
||||
require('util').inherits(TelnetClient, baseClient.Client);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Telnet Command/Option handling
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
TelnetClient.prototype.handleTelnetEvent = function(evt) {
|
||||
// handler name e.g. 'handleWontCommand'
|
||||
var 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('environment variables' === evt.option) {
|
||||
//
|
||||
// See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html
|
||||
//
|
||||
this.requestEnvironmentVariables();
|
||||
} else {
|
||||
// :TODO: temporary:
|
||||
console.log('will ' + JSON.stringify(evt));
|
||||
}
|
||||
};
|
||||
|
||||
TelnetClient.prototype.handleWontCommand = function(evt) {
|
||||
console.log('wont ' + JSON.stringify(evt));
|
||||
};
|
||||
|
||||
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:
|
||||
console.log('do ' + JSON.stringify(evt));
|
||||
}
|
||||
};
|
||||
|
||||
TelnetClient.prototype.handleDontCommand = function(evt) {
|
||||
console.log('dont ' + JSON.stringify(evt));
|
||||
};
|
||||
|
||||
TelnetClient.prototype.setTermType = function(ttype) {
|
||||
this.term.env['TERM'] = ttype;
|
||||
this.term.termType = ttype;
|
||||
|
||||
logger.log.debug({ termType : ttype }, 'Set terminal type');
|
||||
}
|
||||
|
||||
TelnetClient.prototype.handleSbCommand = function(evt) {
|
||||
var 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');
|
||||
}
|
||||
} 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]);
|
||||
} else if('ROWS' === name && 0 === self.term.termHeight) {
|
||||
self.term.termHeight = parseInt(evt.envVars[name]);
|
||||
} else {
|
||||
if(name in self.term.env) {
|
||||
assert(evt.type === SB_COMMANDS.INFO);
|
||||
|
||||
logger.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];
|
||||
}
|
||||
}
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
|
||||
logger.log.debug({ termWidth : evt.width , termHeight : evt.height }, 'Window size updated');
|
||||
} else {
|
||||
console.log('unhandled SB: ' + JSON.stringify(evt));
|
||||
}
|
||||
};
|
||||
|
||||
var 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)
|
||||
logger.log.debug('Interrupt Process (IP) - Ending');
|
||||
this.input.end();
|
||||
} else if('ayt' === evt.command) {
|
||||
this.output.write('\b');
|
||||
logger.log.debug('Are You There (AYT) - Replied "\\b"');
|
||||
} else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) {
|
||||
logger.log.debug({ evt : evt }, 'Ignoring command');
|
||||
} else {
|
||||
logger.log.warn({ evt : evt }, 'Unknown command');
|
||||
}
|
||||
};
|
||||
|
||||
TelnetClient.prototype.requestTerminalType = function() {
|
||||
var buf = Buffer([ COMMANDS.IAC, COMMANDS.SB, OPTIONS.TERMINAL_TYPE, SB_COMMANDS.SEND, COMMANDS.IAC, COMMANDS.SE ]);
|
||||
/*
|
||||
var buf = Buffer(6);
|
||||
buf[0] = COMMANDS.IAC;
|
||||
buf[1] = COMMANDS.SB;
|
||||
buf[2] = OPTIONS.TERMINAL_TYPE;
|
||||
buf[3] = SB_COMMANDS.SEND;
|
||||
buf[4] = COMMANDS.IAC;
|
||||
buf[5] = COMMANDS.SE;
|
||||
*/
|
||||
|
||||
return this.output.write(buf);
|
||||
};
|
||||
|
||||
var WANTED_ENVIRONMENT_VARIABLES = [ 'LINES', 'COLUMNS', 'TERM' ];
|
||||
|
||||
TelnetClient.prototype.requestEnvironmentVariables = function() {
|
||||
var bufs = buffers();
|
||||
|
||||
bufs.push(new Buffer([ COMMANDS.IAC, COMMANDS.SB, OPTIONS.NEW_ENVIRONMENT, SB_COMMANDS.SEND ]));
|
||||
|
||||
for(var i = 0; i < WANTED_ENVIRONMENT_VARIABLES.length; ++i) {
|
||||
bufs.push(new Buffer( [ ENVIRONMENT_VARIABLES_COMMANDS.VAR ]));
|
||||
bufs.push(new Buffer(WANTED_ENVIRONMENT_VARIABLES[i])); // :TODO: encoding here?! UTF-8 will work, but shoudl be more explicit
|
||||
}
|
||||
|
||||
bufs.push(new Buffer([ COMMANDS.IAC, COMMANDS.SE ]));
|
||||
|
||||
return this.output.write(bufs.toBuffer());
|
||||
};
|
||||
|
||||
TelnetClient.prototype.banner = function() {
|
||||
|
||||
// :TODO: See x84 implementation here.
|
||||
// First, we should probably buffer then send.
|
||||
|
||||
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) {
|
||||
var code = OPTIONS[name];
|
||||
|
||||
Command.prototype[name.toLowerCase()] = function() {
|
||||
var buf = 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) {
|
||||
function get() {
|
||||
return new Command(command, this);
|
||||
}
|
||||
|
||||
Object.defineProperty(TelnetClient.prototype, command, {
|
||||
get : get,
|
||||
enumerable : true,
|
||||
configurable : true
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function createServer() {
|
||||
var server = net.createServer(function onConnection(sock) {
|
||||
var self = this;
|
||||
var client = new TelnetClient(sock, sock);
|
||||
|
||||
client.banner();
|
||||
|
||||
self.emit('client', client);
|
||||
});
|
||||
|
||||
return server;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue