From 75787b61079c4cfe91498d415f4d4d8d89565843 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 17 May 2020 20:22:16 -0600 Subject: [PATCH 01/25] Start work on new telnet server --- core/config.js | 5 ++++ core/servers/login/telnet2.js | 43 +++++++++++++++++++++++++++++++++++ package.json | 3 ++- yarn.lock | 9 +++++++- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 core/servers/login/telnet2.js diff --git a/core/config.js b/core/config.js index 66ffab14..76ee98f5 100644 --- a/core/config.js +++ b/core/config.js @@ -285,6 +285,11 @@ function getDefaultConfig() { }, loginServers : { + telnet2: { + port: 8810, + enabled : true, + firstMenu: 'telnetConnected', + }, telnet : { port : 8888, enabled : true, diff --git a/core/servers/login/telnet2.js b/core/servers/login/telnet2.js new file mode 100644 index 00000000..a19fe675 --- /dev/null +++ b/core/servers/login/telnet2.js @@ -0,0 +1,43 @@ +// ENiGMA½ +const LoginServerModule = require('../../login_server_module'); +const Client = require('../../client'); + +// deps +const net = require('net'); +const { TelnetSocket, TelnetSpec } = require('telnet-socket'); + +const ModuleInfo = exports.moduleInfo = { + name : 'Telnet', + desc : 'Telnet Server', + author : 'NuSkooler', + isSecure : false, + packageName : 'codes.l33t.enigma.telnet.server.v2', +}; + + + +class TelnetClient extends Client { + constructor(socket) { + super(); + + this.setInputOutput(socket, socket); + this.telnetSocket = new TelnetSocket(socket); + + // :TODO: banner + } +}; + +exports.getModule = class TelnetServerModule extends LoginServerModule { + constructor() { + super(); + } + + createServer(cb) { + this.server = net.createServer( socket => { + const client = new TelnetClient(socket); + this.handleNewClient(client, socket, ModuleInfo); + }); + + return cb(null); + } +}; diff --git a/package.json b/package.json index 0a68e584..6bb5a613 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "uuid-parse": "1.1.0", "ws": "^7.3.0", "xxhash": "^0.3.0", - "yazl": "^2.5.1" + "yazl": "^2.5.1", + "telnet-socket" : "github:NuSkooler/telnet-socket" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index 30d2470a..5dd80c0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -160,7 +160,7 @@ bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -binary-parser@^1.6.2: +binary-parser@^1.5.0, binary-parser@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.6.2.tgz#8410a82ffd9403271ec182bd91e63a09cee88cbe" integrity sha512-cYAhKB51A9T/uylDvMK7uAYaPLWLwlferNOpnQ0E0fuO73yPi7kWaWiOm22BvuKxCbggmkiFN0VkuLg6gc+KQQ== @@ -1840,6 +1840,13 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" +"telnet-socket@github:NuSkooler/telnet-socket": + version "0.1.0" + resolved "https://codeload.github.com/NuSkooler/telnet-socket/tar.gz/a29ea2edaec98bb844f2b0d27fa0977f33eaa7bf" + dependencies: + binary-parser "^1.5.0" + buffers "github:NuSkooler/node-buffers" + temptmp@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/temptmp/-/temptmp-1.1.0.tgz#bfbbff858d7f7d59c563fbf069758a7775ecd431" From 1018485e8e55e5be284502080f27698c02cb0207 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 17 May 2020 21:36:48 -0600 Subject: [PATCH 02/25] WIP --- core/config.js | 5 -- core/servers/login/telnet2.js | 126 ++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 11 deletions(-) diff --git a/core/config.js b/core/config.js index 76ee98f5..66ffab14 100644 --- a/core/config.js +++ b/core/config.js @@ -285,11 +285,6 @@ function getDefaultConfig() { }, loginServers : { - telnet2: { - port: 8810, - enabled : true, - firstMenu: 'telnetConnected', - }, telnet : { port : 8888, enabled : true, diff --git a/core/servers/login/telnet2.js b/core/servers/login/telnet2.js index a19fe675..3127dfb8 100644 --- a/core/servers/login/telnet2.js +++ b/core/servers/login/telnet2.js @@ -1,10 +1,16 @@ // ENiGMA½ const LoginServerModule = require('../../login_server_module'); -const Client = require('../../client'); +const { Client } = require('../../client'); +const Config = require('../../config').get; +const { log: Log } = require('../../logger'); // deps const net = require('net'); -const { TelnetSocket, TelnetSpec } = require('telnet-socket'); +const { + TelnetSocket, + TelnetSpec: { Options, Commands } +} = require('telnet-socket'); +const { inherits } = require('util'); const ModuleInfo = exports.moduleInfo = { name : 'Telnet', @@ -16,17 +22,104 @@ const ModuleInfo = exports.moduleInfo = { -class TelnetClient extends Client { +class TelnetClient { constructor(socket) { - super(); + Client.apply(this, socket, socket); this.setInputOutput(socket, socket); - this.telnetSocket = new TelnetSocket(socket); + this.socket = new TelnetSocket(socket); - // :TODO: banner + // + // Wait up to 3s to hear about from our terminal type request + // then go ahead and move on... + // + setTimeout(() => { + this.clientReady(); + }, 3000); + + this.socket.on('WILL', command => { + switch (command.option) { + case Options.LINEMODE : + return this.socket.dont.linemode(); + + case Options.TTYPE : + return this.socket.sb.send.ttype(); + + case Options.NEW_ENVIRON : + return this.socket.sb.send.new_environ(); + + default : + break; + } + }); + + this.socket.on('SB', command => { + switch (command.option) { + case Options.TTYPE : + this.setTermType(command.optionData.ttype); + this.clientReady(); + break; + + case Options.NEW_ENVIRON : + { + if ('unknown' === this.term.termType) { + const term = + command.optionData.vars.find(nv => nv.TERM) || + command.optionData.userVars.find(nv => nv.TERM); + if (term) { + this.setTermType(term); + } + } + + command.optionData.vars.forEach(nv => { + console.log(nv); + }); + } + break; + + case Options.NAWS : + this.term.termWidth = command.optionData.width; + this.term.termHeight = command.optionData.height; + + // :TODO: update env, see old telnet.js + + this.clearMciCache(); + + // :TODO: Log negotiation + break; + } + }); + + this.banner(); + } + + clientReady() { + if (this.clientReadyHandled) { + return; // already processed + } + + this.clientReadyHandled = true; + this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); + } + + banner() { + this.socket.do.echo(); + this.socket.will.echo(); // we'll echo back + + this.socket.will.sga(); + this.socket.do.sga(); + + this.socket.do.transmit_binary(); + this.socket.will.transmit_binary(); + + this.socket.do.ttype(); + this.socket.do.naws(); + this.socket.do.new_environ(); } }; +inherits(TelnetClient, Client); + exports.getModule = class TelnetServerModule extends LoginServerModule { constructor() { super(); @@ -38,6 +131,27 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { this.handleNewClient(client, socket, ModuleInfo); }); + this.server.on('error', err => { + Log.info( { error : err.message }, 'Telnet server error'); + }); + return cb(null); } + + listen(cb) { + const config = Config(); + //const port = parseInt(config.loginServers.telnet.port); + const port = 8810; // :TODO: Put me back ;) + if(isNaN(port)) { + Log.error( { server : ModuleInfo.name, port : config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); + return cb(Errors.Invalid(`Invalid port: ${config.loginServers.telnet.port}`)); + } + + this.server.listen(port, config.loginServers.telnet.address, err => { + if(!err) { + Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + } + return cb(err); + }); + } }; From e517e31b9405aa099b89617e6a1666e99734d18c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 17 May 2020 22:35:03 -0600 Subject: [PATCH 03/25] Working fairly well --- core/servers/login/telnet2.js | 72 +++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/core/servers/login/telnet2.js b/core/servers/login/telnet2.js index 3127dfb8..3e38d988 100644 --- a/core/servers/login/telnet2.js +++ b/core/servers/login/telnet2.js @@ -20,8 +20,6 @@ const ModuleInfo = exports.moduleInfo = { packageName : 'codes.l33t.enigma.telnet.server.v2', }; - - class TelnetClient { constructor(socket) { Client.apply(this, socket, socket); @@ -37,6 +35,33 @@ class TelnetClient { this.clientReady(); }, 3000); + this.socket.on('data', data => { + this.emit('data', data); + }); + + this.socket.on('error', err => { + // :TODO: Log me + return this.emit('end'); + }); + + this.socket.on('end', () => { + this.emit('end'); + }); + + this.socket.on('DO', command => { + switch (command.option) { + case Options.ECHO : + return this.socket.will.echo(); + + default : + return this.socket.command(Commands.WONT, command.option); + } + }); + + this.socket.on('DONT', command => { + // :TODO: Log me + }); + this.socket.on('WILL', command => { switch (command.option) { case Options.LINEMODE : @@ -53,6 +78,10 @@ class TelnetClient { } }); + this.socket.on('WONT', command => { + // :TODO: see telnet.js handling + }); + this.socket.on('SB', command => { switch (command.option) { case Options.TTYPE : @@ -78,18 +107,39 @@ class TelnetClient { break; case Options.NAWS : - this.term.termWidth = command.optionData.width; - this.term.termHeight = command.optionData.height; + { + const { width, height } = command.optionData; - // :TODO: update env, see old telnet.js + this.term.termWidth = width; + this.term.termHeight = height; - this.clearMciCache(); + if (width) { + this.term.env.COLUMNS = width; + } - // :TODO: Log negotiation + if (height) { + this.term.env.ROWS = height; + } + + this.clearMciCache(); + + // :TODO: Log negotiation + } break; } }); + this.socket.on('IP', command => { + // :TODO: Log me + return this.disconnect(); + }); + + this.socket.on('AYT', () => { + // :TODO: Log me + return this.socket.write('\b'); + }); + + // kick off negotiations this.banner(); } @@ -102,6 +152,14 @@ class TelnetClient { this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); } + disconnect() { + try { + return this.socket.rawSocket.end(); + } catch (e) { + // ignored + } + } + banner() { this.socket.do.echo(); this.socket.will.echo(); // we'll echo back From a1ac6dfc67e12fdbca7376652032c3be9fa38e4b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 May 2020 00:33:14 -0600 Subject: [PATCH 04/25] Shim in dataHandler for toggling from elsewhere --- core/servers/login/telnet2.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/core/servers/login/telnet2.js b/core/servers/login/telnet2.js index 3e38d988..7f63659e 100644 --- a/core/servers/login/telnet2.js +++ b/core/servers/login/telnet2.js @@ -35,9 +35,11 @@ class TelnetClient { this.clientReady(); }, 3000); - this.socket.on('data', data => { + this.dataHandler = function(data) { this.emit('data', data); - }); + }.bind(this); + + this.socket.on('data', this.dataHandler); this.socket.on('error', err => { // :TODO: Log me @@ -79,7 +81,14 @@ class TelnetClient { }); this.socket.on('WONT', command => { - // :TODO: see telnet.js handling + switch (command.option) { + case Options.NEW_ENVIRON : + return this.socket.dont.new_environ(); + + default : + // :TODO: Log me + break; + } }); this.socket.on('SB', command => { @@ -143,6 +152,10 @@ class TelnetClient { this.banner(); } + // dataHandler(data) { + // this.emit('data', data); + // } + clientReady() { if (this.clientReadyHandled) { return; // already processed From 6d307ec06ba7c589fb175764677c1e911a87ddc3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 May 2020 18:41:23 -0600 Subject: [PATCH 05/25] Clean up a bit. Still more to do... --- core/servers/login/telnet.js | 1028 ++++++------------------------- core/servers/login/telnet2.js | 228 ------- core/servers/login/websocket.js | 2 +- 3 files changed, 204 insertions(+), 1054 deletions(-) delete mode 100644 core/servers/login/telnet2.js diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 20ec6cd9..111167fb 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -1,841 +1,232 @@ -/* jslint node: true */ -'use strict'; - // ENiGMA½ -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const LoginServerModule = require('../../login_server_module.js'); -const Config = require('../../config.js').get; -const EnigAssert = require('../../enigma_assert.js'); -const { stringFromNullTermBuffer } = require('../../string_util.js'); -const { Errors } = require('../../enig_error.js'); +const LoginServerModule = require('../../login_server_module'); +const { Client } = require('../../client'); +const Config = require('../../config').get; +const { log: Log } = require('../../logger'); // deps -const net = require('net'); -const buffers = require('buffers'); -const { Parser } = require('binary-parser'); -const util = require('util'); +const net = require('net'); +const { + TelnetSocket, + TelnetSpec: { Options, Commands } +} = require('telnet-socket'); +const { inherits } = require('util'); const ModuleInfo = exports.moduleInfo = { name : 'Telnet', - desc : 'Telnet Server', + desc : 'Telnet Server v2', author : 'NuSkooler', isSecure : false, - packageName : 'codes.l33t.enigma.telnet.server', + packageName : 'codes.l33t.enigma.telnet.server.v2', }; -exports.TelnetClient = TelnetClient; +class TelnetClient { + constructor(socket) { + Client.apply(this, socket, socket); -// -// Telnet Protocol Resources -// * http://pcmicro.com/netfoss/telnet.html -// * http://mud-dev.wikidot.com/telnet:negotiation -// + this.setInputOutput(socket, socket); + this.socket = new TelnetSocket(socket); -/* - TODO: - * Various (much lesser used) Telnet command coverage -*/ - -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 -// * http://www.networksorcery.com/enp/protocol/telnet.htm -// -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 - - ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854 - - 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 = Buffer.from([ COMMANDS.IAC ]); -const IAC_SE_BUF = Buffer.from([ 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; -}, {}); - -function unknownOption(bufs, i, event) { - Log.warn( { bufs : bufs, i : i, event : event }, 'Unknown Telnet option'); - event.buf = bufs.splice(0, i).toBuffer(); - return event; -} - -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.ARE_YOU_THERE] = -OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { - event.buf = bufs.splice(0, i).toBuffer(); - return event; -}; - -const TermTypeCmdParser = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint8('is') - .array('ttype', { - type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC - }) - // note we read iac2 above - .uint8('se'); - -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; - } - - const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } - - let ttypeCmd; - try { - ttypeCmd = TermTypeCmdParser.parse(bufs.toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing TTYP telnet command'); - return event; - } - - EnigAssert(COMMANDS.IAC === ttypeCmd.iac1); - EnigAssert(COMMANDS.SB === ttypeCmd.sb); - EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); - EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); - EnigAssert(ttypeCmd.ttype.length > 0); - // note we found IAC_SE above - - // some terminals such as NetRunner provide a NULL-terminated buffer - // slice to remove IAC - event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); - - bufs.splice(0, end); - } - - return event; -}; - -const NawsCmdParser = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint16be('width') - .uint16be('height') - .uint8('iac2') - .uint8('se'); - -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; - } - - let nawsCmd; - try { - nawsCmd = NawsCmdParser.parse(bufs.splice(0, 9).toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing NAWS telnet command'); - return event; - } - - EnigAssert(COMMANDS.IAC === nawsCmd.iac1); - EnigAssert(COMMANDS.SB === nawsCmd.sb); - EnigAssert(OPTIONS.WINDOW_SIZE === nawsCmd.opt); - EnigAssert(COMMANDS.IAC === nawsCmd.iac2); - EnigAssert(COMMANDS.SE === nawsCmd.se); - - event.cols = event.columns = event.width = nawsCmd.width; - event.rows = event.height = nawsCmd.height; - } - return event; -}; - -// Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] -//const NEW_ENVIRONMENT_DELIMITERS = _.values(NEW_ENVIRONMENT_COMMANDS); - -const EnvCmdParser = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint8('isOrInfo') // IS=initial, INFO=updates - .array('envBlock', { - type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC - }) - // note we consume IAC above - .uint8('se'); - -// Handle the deprecated RFC 1408 & the updated RFC 1572: -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { // - // We need 4 bytes header + + IAC SE - // Many terminals send a empty list: - // IAC SB NEW-ENVIRON IS IAC SE + // Wait up to 3s to hear about from our terminal type request + // then go ahead and move on... // - if(bufs.length < 6) { - return MORE_DATA_REQUIRED; - } + setTimeout(() => { + this._clientReady(); + }, 3000); - let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } + this.dataHandler = function(data) { + this.emit('data', data); + }.bind(this); - // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. + this.socket.on('data', this.dataHandler); - let envCmd; - try { - envCmd = EnvCmdParser.parse(bufs.splice(0, bufs.length).toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing NEW-ENVIRON telnet command'); - return event; - } + this.socket.on('error', err => { + this._logDebug( { error : err.message }, 'Socket error'); + return this.emit('end'); + }); - EnigAssert(COMMANDS.IAC === envCmd.iac1); - EnigAssert(COMMANDS.SB === envCmd.sb); - EnigAssert(OPTIONS.NEW_ENVIRONMENT === envCmd.opt || OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt); - EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); + this.socket.on('end', () => { + this.emit('end'); + }); - if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { - // :TODO: we should probably support this for legacy clients? - Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); - } + this.socket.on('DO', command => { + switch (command.option) { + case Options.ECHO : + return this.socket.will.echo(); - const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC - - if(envBuf.length < 4) { // TYPE + single char name + sep + single char value - // empty env block - return event; - } - - const States = { - Name : 1, - Value : 2, - }; - - let state = States.Name; - const setVars = {}; - const delVars = []; - let varName; - // :TODO: handle ESC type!!! - while(envBuf.length) { - switch(state) { - case States.Name : - { - const type = parseInt(envBuf.splice(0, 1)); - if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { - return event; // fail :( - } - - let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); - if(-1 === nameEnd) { - nameEnd = envBuf.length; - } - - varName = envBuf.splice(0, nameEnd); - if(!varName) { - return event; // something is wrong. - } - - varName = Buffer.from(varName).toString('ascii'); - - const next = parseInt(envBuf.splice(0, 1)); - if(NEW_ENVIRONMENT_COMMANDS.VALUE === next) { - state = States.Value; - } else { - state = States.Name; - delVars.push(varName); // no value; del this var - } - } - break; - - case States.Value : - { - let valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VAR); - if(-1 === valueEnd) { - valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.USERVAR); - } - if(-1 === valueEnd) { - valueEnd = envBuf.length; - } - - let value = envBuf.splice(0, valueEnd); - if(value) { - value = Buffer.from(value).toString('ascii'); - setVars[varName] = value; - } - state = States.Name; - } - break; - } - } - - // :TODO: Handle deleting previously set vars via delVars - event.type = envCmd.isOrInfo; - event.envVars = setVars; - } - - return event; -}; - -const MORE_DATA_REQUIRED = 0xfeedface; - -function parseBufs(bufs) { - EnigAssert(bufs.length >= 2); - EnigAssert(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]; - - const handler = OPTION_IMPLS[option]; - return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event); -} - - -function TelnetClient(input, output) { - baseClient.Client.apply(this, arguments); - - const self = this; - - let bufs = buffers(); - this.bufs = bufs; - - this.sentDont = {}; // DON'T's we've already sent - - 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.dataHandler = function(b) { - if(!Buffer.isBuffer(b)) { - EnigAssert(false, `Cannot push non-buffer ${typeof b}`); - return; - } - - 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; - } - - EnigAssert(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) { - 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('data', this.dataHandler); - - this.input.on('end', () => { - self.emit('end'); - }); - - this.input.on('error', err => { - this.connectionDebug( { err : err }, 'Socket error' ); - return self.emit('end'); - }); - - this.connectionTrace = (info, msg) => { - if(Config().loginServers.telnet.traceConnections) { - const logger = self.log || Log; - return logger.trace(info, `Telnet: ${msg}`); - } - }; - - this.connectionDebug = (info, msg) => { - const logger = self.log || Log; - return logger.debug(info, `Telnet: ${msg}`); - }; - - this.connectionWarn = (info, msg) => { - const logger = self.log || Log; - return logger.warn(info, `Telnet: ${msg}`); - }; - - this.readyNow = () => { - if(!this.didReady) { - this.didReady = true; - this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); - } - }; - - this.disconnect = function() { - try { - return this.output.end.apply(this.output, arguments); - } - catch(e) { - // nothing - } - }; -} - -util.inherits(TelnetClient, baseClient.Client); - -/////////////////////////////////////////////////////////////////////////////// -// Telnet Command/Option handling -/////////////////////////////////////////////////////////////////////////////// -TelnetClient.prototype.handleTelnetEvent = function(evt) { - - if(!evt.command) { - return this.connectionWarn( { evt : evt }, 'No command for event'); - } - - // 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.connectionTrace(evt, 'WILL'); - } -}; - -TelnetClient.prototype.handleWontCommand = function(evt) { - if(this.sentDont[evt.option]) { - return this.connectionTrace(evt, 'WONT - DON\'T already sent'); - } - - this.sentDont[evt.option] = true; - - if('new environment' === evt.option) { - this.dont.new_environment(); - } else { - this.connectionTrace(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.connectionTrace(evt, 'DO'); - } -}; - -TelnetClient.prototype.handleDontCommand = function(evt) { - this.connectionTrace(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 - - self.readyNow(); - } 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.connectionDebug({ 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.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); - } else { - if(name in self.term.env) { - - EnigAssert( - SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type, - 'Unexpected type: ' + evt.type - ); - - self.connectionWarn( - { varName : name, value : evt.envVars[name], existingValue : self.term.env[name] }, - 'Environment variable already exists' - ); - } else { - self.term.env[name] = evt.envVars[name]; - self.connectionDebug( { varName : name, value : evt.envVars[name] }, 'New environment variable' ); - } + default : + return this.socket.command(Commands.WONT, command.option); } }); - } 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; + this.socket.on('DONT', command => { + this._logTrace(command, 'DONT'); + }); - if(evt.width > 0) { - self.term.env.COLUMNS = evt.height; + this.socket.on('WILL', command => { + switch (command.option) { + case Options.LINEMODE : + return this.socket.dont.linemode(); + + case Options.TTYPE : + return this.socket.sb.send.ttype(); + + case Options.NEW_ENVIRON : + return this.socket.sb.send.new_environ( + [ 'ROWS', 'COLUMNS', 'TERM', 'TERM_PROGRAM' ] + ); + + default : + break; + } + }); + + this.socket.on('WONT', command => { + switch (command.option) { + case Options.NEW_ENVIRON : + return this.socket.dont.new_environ(); + + default : + return this._logTrace(command, 'WONT'); + } + }); + + this.socket.on('SB', command => { + switch (command.option) { + case Options.TTYPE : + this.setTermType(command.optionData.ttype); + return this._clientReady(); + + case Options.NEW_ENVIRON : + { + this._logDebug( + { vars : command.optionData.vars, userVars : command.optionData.userVars }, + 'New environment received' + ); + + // get a value from vars with fallback of user vars + const getValue = (name) => { + return command.optionData.vars.find(nv => nv.name === name) || + command.optionData.userVars.find(nv => nv.name === name); + }; + + if ('unknown' === this.term.termType) { + // allow from vars or user vars + const term = getValue('TERM') || getValue('TERM_PROGRAM'); + if (term) { + this.setTermType(term.value); + } + } + + if (0 === this.term.termHeight || 0 === this.term.termWidth) { + const updateTermSize = (what) => { + const value = parseInt(getValue(what)); + if (value) { + this.term[what === 'ROWS' ? 'termHeight' : 'termWidth'] = value; + this.clearMciCache(); + this._logDebug( + { [ what ] : value, source : 'NEW-ENVIRON' }, + 'Window size updated' + ); + } + }; + + updateTermSize('ROWS'); + updateTermSize('COLUMNS'); + } + } + break; + + case Options.NAWS : + { + const { width, height } = command.optionData; + + this.term.termWidth = width; + this.term.termHeight = height; + + if (width) { + this.term.env.COLUMNS = width; + } + + if (height) { + this.term.env.ROWS = height; + } + + this.clearMciCache(); + + this._logDebug( + { width, height, source : 'NAWS' }, + 'Windows size updated' + ); + } + break; + + default : + return this._logTrace(command, 'SB'); + } + }); + + this.socket.on('IP', command => { + this._logDebug(command, 'Interrupt Process (IP) - Ending session'); + return this.disconnect(); + }); + + this.socket.on('AYT', () => { + this.socket.write('\b'); + return this._logTrace(command, 'Are You There (AYT) - Replied'); + }); + + // kick off negotiations + this._banner(); + } + + disconnect() { + try { + return this.socket.rawSocket.end(); + } catch (e) { + // ignored + } + } + + _logTrace(info, msg) { + if (Config().loginServers.telnet.traceConnections) { + const log = this.log || Log; + return log.trace(info, `Telnet: ${msg}`); + } + } + + _logDebug(info, msg) { + const log = this.log || Log; + return log.debug(info, `Telnet: ${msg}`); + } + + _clientReady() { + if (this.clientReadyHandled) { + return; // already processed } - if(evt.height > 0) { - self.term.env.ROWS = evt.height; - } + this.clientReadyHandled = true; + this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); + } - self.clearMciCache(); // term size changes = invalidate cache + _banner() { + this.socket.do.echo(); + this.socket.will.echo(); // we'll echo back - self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); - } else { - self.connectionDebug(evt, 'SB'); + this.socket.will.sga(); + this.socket.do.sga(); + + this.socket.do.transmit_binary(); + this.socket.will.transmit_binary(); + + this.socket.do.ttype(); + this.socket.do.naws(); + this.socket.do.new_environ(); } }; -const IGNORED_COMMANDS = [ - COMMANDS.EL, COMMANDS.GA, COMMANDS.NOP, COMMANDS.DM, COMMANDS.BRK -]; - - -TelnetClient.prototype.handleMiscCommand = function(evt) { - EnigAssert(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.trace({ command : evt.command, commandCode : evt.commandCode }, 'Ignoring command'); - } else { - this.log.warn({ evt : evt }, 'Unknown command'); - } -}; - -TelnetClient.prototype.requestTerminalType = function() { - const buf = Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.TERMINAL_TYPE, - SB_COMMANDS.SEND, - COMMANDS.IAC, - COMMANDS.SE ]); - this.output.write(buf); -}; - -const WANTED_ENVIRONMENT_VAR_BUFS = [ - Buffer.from( 'LINES' ), - Buffer.from( 'COLUMNS' ), - Buffer.from( 'TERM' ), - Buffer.from( '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(Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.NEW_ENVIRONMENT, - SB_COMMANDS.SEND ] - )); - - for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { - bufs.push(Buffer.from( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); - } - - bufs.push(Buffer.from([ 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 = Buffer.alloc(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 - }); -}); +inherits(TelnetClient, Client); exports.getModule = class TelnetServerModule extends LoginServerModule { constructor() { @@ -843,24 +234,9 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { } createServer(cb) { - this.server = net.createServer( sock => { - const client = new TelnetClient(sock, sock); - - client.banner(); - - this.handleNewClient(client, sock, ModuleInfo); - - // - // Set a timeout and attempt to proceed even if we don't know - // the term type yet, which is the preferred trigger - // for moving along - // - setTimeout( () => { - if(!client.didReady) { - Log.info('Proceeding after 3s without knowing term type'); - client.readyNow(); - } - }, 3000); + this.server = net.createServer( socket => { + const client = new TelnetClient(socket); + this.handleNewClient(client, socket, ModuleInfo); }); this.server.on('error', err => { @@ -886,3 +262,5 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { }); } }; + +exports.TelnetClient = TelnetClient; // WebSockets is a wrapper on top of this diff --git a/core/servers/login/telnet2.js b/core/servers/login/telnet2.js deleted file mode 100644 index 7f63659e..00000000 --- a/core/servers/login/telnet2.js +++ /dev/null @@ -1,228 +0,0 @@ -// ENiGMA½ -const LoginServerModule = require('../../login_server_module'); -const { Client } = require('../../client'); -const Config = require('../../config').get; -const { log: Log } = require('../../logger'); - -// deps -const net = require('net'); -const { - TelnetSocket, - TelnetSpec: { Options, Commands } -} = require('telnet-socket'); -const { inherits } = require('util'); - -const ModuleInfo = exports.moduleInfo = { - name : 'Telnet', - desc : 'Telnet Server', - author : 'NuSkooler', - isSecure : false, - packageName : 'codes.l33t.enigma.telnet.server.v2', -}; - -class TelnetClient { - constructor(socket) { - Client.apply(this, socket, socket); - - this.setInputOutput(socket, socket); - this.socket = new TelnetSocket(socket); - - // - // Wait up to 3s to hear about from our terminal type request - // then go ahead and move on... - // - setTimeout(() => { - this.clientReady(); - }, 3000); - - this.dataHandler = function(data) { - this.emit('data', data); - }.bind(this); - - this.socket.on('data', this.dataHandler); - - this.socket.on('error', err => { - // :TODO: Log me - return this.emit('end'); - }); - - this.socket.on('end', () => { - this.emit('end'); - }); - - this.socket.on('DO', command => { - switch (command.option) { - case Options.ECHO : - return this.socket.will.echo(); - - default : - return this.socket.command(Commands.WONT, command.option); - } - }); - - this.socket.on('DONT', command => { - // :TODO: Log me - }); - - this.socket.on('WILL', command => { - switch (command.option) { - case Options.LINEMODE : - return this.socket.dont.linemode(); - - case Options.TTYPE : - return this.socket.sb.send.ttype(); - - case Options.NEW_ENVIRON : - return this.socket.sb.send.new_environ(); - - default : - break; - } - }); - - this.socket.on('WONT', command => { - switch (command.option) { - case Options.NEW_ENVIRON : - return this.socket.dont.new_environ(); - - default : - // :TODO: Log me - break; - } - }); - - this.socket.on('SB', command => { - switch (command.option) { - case Options.TTYPE : - this.setTermType(command.optionData.ttype); - this.clientReady(); - break; - - case Options.NEW_ENVIRON : - { - if ('unknown' === this.term.termType) { - const term = - command.optionData.vars.find(nv => nv.TERM) || - command.optionData.userVars.find(nv => nv.TERM); - if (term) { - this.setTermType(term); - } - } - - command.optionData.vars.forEach(nv => { - console.log(nv); - }); - } - break; - - case Options.NAWS : - { - const { width, height } = command.optionData; - - this.term.termWidth = width; - this.term.termHeight = height; - - if (width) { - this.term.env.COLUMNS = width; - } - - if (height) { - this.term.env.ROWS = height; - } - - this.clearMciCache(); - - // :TODO: Log negotiation - } - break; - } - }); - - this.socket.on('IP', command => { - // :TODO: Log me - return this.disconnect(); - }); - - this.socket.on('AYT', () => { - // :TODO: Log me - return this.socket.write('\b'); - }); - - // kick off negotiations - this.banner(); - } - - // dataHandler(data) { - // this.emit('data', data); - // } - - clientReady() { - if (this.clientReadyHandled) { - return; // already processed - } - - this.clientReadyHandled = true; - this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); - } - - disconnect() { - try { - return this.socket.rawSocket.end(); - } catch (e) { - // ignored - } - } - - banner() { - this.socket.do.echo(); - this.socket.will.echo(); // we'll echo back - - this.socket.will.sga(); - this.socket.do.sga(); - - this.socket.do.transmit_binary(); - this.socket.will.transmit_binary(); - - this.socket.do.ttype(); - this.socket.do.naws(); - this.socket.do.new_environ(); - } -}; - -inherits(TelnetClient, Client); - -exports.getModule = class TelnetServerModule extends LoginServerModule { - constructor() { - super(); - } - - createServer(cb) { - this.server = net.createServer( socket => { - const client = new TelnetClient(socket); - this.handleNewClient(client, socket, ModuleInfo); - }); - - this.server.on('error', err => { - Log.info( { error : err.message }, 'Telnet server error'); - }); - - return cb(null); - } - - listen(cb) { - const config = Config(); - //const port = parseInt(config.loginServers.telnet.port); - const port = 8810; // :TODO: Put me back ;) - if(isNaN(port)) { - Log.error( { server : ModuleInfo.name, port : config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); - return cb(Errors.Invalid(`Invalid port: ${config.loginServers.telnet.port}`)); - } - - this.server.listen(port, config.loginServers.telnet.address, err => { - if(!err) { - Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); - } - return cb(err); - }); - } -}; diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index dadcdbe6..1cff80e4 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -95,7 +95,7 @@ function WebSocketClient(ws, req, serverType) { ws.isConnectionAlive = true; }); - TelnetClient.call(this, this.socketBridge, this.socketBridge); + TelnetClient.call(this, this.socketBridge); Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); From e85ba322cefd2b40b4c76110d7fa52af55957ec0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 May 2020 19:19:30 -0600 Subject: [PATCH 06/25] WebSockets work with new telnet server --- core/servers/login/telnet.js | 34 +++---- core/servers/login/websocket.js | 174 +++++++++++++++++--------------- 2 files changed, 106 insertions(+), 102 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 111167fb..7bb938a4 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -176,9 +176,6 @@ class TelnetClient { this.socket.write('\b'); return this._logTrace(command, 'Are You There (AYT) - Replied'); }); - - // kick off negotiations - this._banner(); } disconnect() { @@ -189,6 +186,21 @@ class TelnetClient { } } + banner() { + this.socket.do.echo(); + this.socket.will.echo(); // we'll echo back + + this.socket.will.sga(); + this.socket.do.sga(); + + this.socket.do.transmit_binary(); + this.socket.will.transmit_binary(); + + this.socket.do.ttype(); + this.socket.do.naws(); + this.socket.do.new_environ(); + } + _logTrace(info, msg) { if (Config().loginServers.telnet.traceConnections) { const log = this.log || Log; @@ -209,21 +221,6 @@ class TelnetClient { this.clientReadyHandled = true; this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); } - - _banner() { - this.socket.do.echo(); - this.socket.will.echo(); // we'll echo back - - this.socket.will.sga(); - this.socket.do.sga(); - - this.socket.do.transmit_binary(); - this.socket.will.transmit_binary(); - - this.socket.do.ttype(); - this.socket.do.naws(); - this.socket.do.new_environ(); - } }; inherits(TelnetClient, Client); @@ -236,6 +233,7 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { createServer(cb) { this.server = net.createServer( socket => { const client = new TelnetClient(socket); + client.banner(); // start negotiations this.handleNewClient(client, socket, ModuleInfo); }); diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index 1cff80e4..f0cdc0b1 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -24,100 +24,106 @@ const ModuleInfo = exports.moduleInfo = { packageName : 'codes.l33t.enigma.websocket.server', }; -function WebSocketClient(ws, req, serverType) { +class WebSocketClient extends TelnetClient { + constructor(ws, req, serverType) { + // + // This bridge makes accessible various calls that client sub classes + // want to access on I/O socket + // + const socketBridge = new class SocketBridge extends Writable { + constructor(ws) { + super(); + this.ws = ws; + } - Object.defineProperty(this, 'isSecure', { - get : () => ('secure' === serverType || true === this.proxied) ? true : false, - }); + setClient(client) { + this.client = client; + } - const self = this; + end() { + return ws.close(); + } - this.dataHandler = function(data) { - if(self.pipedDest) { - self.pipedDest.write(data); + write(data, cb) { + cb = cb || ( () => { /* eat it up */} ); // handle data writes after close + + return this.ws.send(data, { binary : true }, cb); + } + + pipe(dest) { + Log.trace('WebSocket SocketBridge pipe()'); + this.client.pipedDest = dest; + } + + unpipe() { + Log.trace('WebSocket SocketBridge unpipe()'); + this.client.pipedDest = null; + } + + resume() { + Log.trace('WebSocket SocketBridge resume()'); + } + + get remoteAddress() { + // Support X-Forwarded-For and X-Real-IP headers for proxied connections + return (this.client.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; + } + }(ws); + + // :TODO: this is quite the clusterfuck... + super(socketBridge); + this.socketBridge = socketBridge; + this.serverType = serverType; + + this.socketBridge.setClient(this); + + this.dataHandler = function(data) { + if(this.pipedDest) { + this.pipedDest.write(data); + } else { + this.socketBridge.emit('data', data); + } + }.bind(this); + + ws.on('message', this.dataHandler); + + ws.on('close', () => { + // we'll remove client connection which will in turn end() via our SocketBridge above + return this.emit('end'); + }); + + // + // Monitor connection status with ping/pong + // + ws.on('pong', () => { + Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); + ws.isConnectionAlive = true; + }); + + Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); + + // + // If the config allows it, look for 'x-forwarded-proto' as "https" + // to override |isSecure| + // + if(true === _.get(Config(), 'loginServers.webSocket.proxied') && + 'https' === req.headers['x-forwarded-proto']) + { + Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); + this.proxied = true; } else { - self.socketBridge.emit('data', data); - } - }; - - // - // This bridge makes accessible various calls that client sub classes - // want to access on I/O socket - // - this.socketBridge = new class SocketBridge extends Writable { - constructor(ws) { - super(); - this.ws = ws; + this.proxied = false; } - end() { - return ws.close(); - } - - write(data, cb) { - cb = cb || ( () => { /* eat it up */} ); // handle data writes after close - - return this.ws.send(data, { binary : true }, cb); - } - - pipe(dest) { - Log.trace('WebSocket SocketBridge pipe()'); - self.pipedDest = dest; - } - - unpipe() { - Log.trace('WebSocket SocketBridge unpipe()'); - self.pipedDest = null; - } - - resume() { - Log.trace('WebSocket SocketBridge resume()'); - } - - get remoteAddress() { - // Support X-Forwarded-For and X-Real-IP headers for proxied connections - return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; - } - }(ws); - - ws.on('message', this.dataHandler); - - ws.on('close', () => { - // we'll remove client connection which will in turn end() via our SocketBridge above - return this.emit('end'); - }); - - // - // Monitor connection status with ping/pong - // - ws.on('pong', () => { - Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); - ws.isConnectionAlive = true; - }); - - TelnetClient.call(this, this.socketBridge); - - Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); - - // - // If the config allows it, look for 'x-forwarded-proto' as "https" - // to override |isSecure| - // - if(true === _.get(Config(), 'loginServers.webSocket.proxied') && - 'https' === req.headers['x-forwarded-proto']) - { - Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); - this.proxied = true; - } else { - this.proxied = false; + // start handshake process + this.banner(); } - // start handshake process - this.banner(); + get isSecure() { + return ('secure' === this.serverType || true === this.proxied) ? true : false; + } } -require('util').inherits(WebSocketClient, TelnetClient); - const WSS_SERVER_TYPES = [ 'insecure', 'secure' ]; exports.getModule = class WebSocketLoginServer extends LoginServerModule { From 65d68f33dc33c3d9014989c119d73f10b20ef576 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 20 May 2020 20:23:09 -0600 Subject: [PATCH 07/25] Allow passthrough --- core/servers/login/telnet.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 7bb938a4..318dd9fc 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -24,8 +24,8 @@ class TelnetClient { constructor(socket) { Client.apply(this, socket, socket); - this.setInputOutput(socket, socket); this.socket = new TelnetSocket(socket); + this.setInputOutput(this.socket, this.socket); // // Wait up to 3s to hear about from our terminal type request @@ -50,6 +50,8 @@ class TelnetClient { this.emit('end'); }); + // :TODO: handle 'command error' event + this.socket.on('DO', command => { switch (command.option) { case Options.ECHO : @@ -178,6 +180,14 @@ class TelnetClient { }); } + get dataPassthrough() { + return this.socket.passthrough; + } + + set dataPassthrough(passthrough) { + this.socket.passthrough = passthrough; + } + disconnect() { try { return this.socket.rawSocket.end(); From 72301104ffb817b2175bf50ffbc3284f8968ed48 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 20 May 2020 20:24:12 -0600 Subject: [PATCH 08/25] Update passthrough state --- core/client.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/client.js b/core/client.js index 40872fa2..88c9ea0f 100644 --- a/core/client.js +++ b/core/client.js @@ -107,11 +107,13 @@ function Client(/*input, output*/) { }); this.setTemporaryDirectDataHandler = function(handler) { + this.dataPassthrough = true; // let implementations do with what they will here this.input.removeAllListeners('data'); this.input.on('data', handler); }; this.restoreDataHandler = function() { + this.dataPassthrough = false; this.input.removeAllListeners('data'); this.input.on('data', this.dataHandler); }; From 03b79fa9f294bc840f3d7a6586d19a7ef3a22707 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 20 May 2020 20:24:26 -0600 Subject: [PATCH 09/25] TODO for IAC processing --- core/file_transfer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/file_transfer.js b/core/file_transfer.js index ff9216d6..c5d5030c 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -390,6 +390,7 @@ exports.getModule = class TransferFileModule extends MenuModule { // needed for things like sz/rz if(external.escapeTelnet) { + // :TODO: do this faster for already-buffers... const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape externalProc.write(Buffer.from(tmp, 'binary')); } else { From bbae082f7dd2c39253f7ec274670ff76dd93a998 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 20 May 2020 20:28:55 -0600 Subject: [PATCH 10/25] Temp: Use local mod for development --- package.json | 2 +- yarn.lock | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6bb5a613..e0e5f0d1 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "ws": "^7.3.0", "xxhash": "^0.3.0", "yazl": "^2.5.1", - "telnet-socket" : "github:NuSkooler/telnet-socket" + "telnet-socket" : "../telnet-socket" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index 5dd80c0e..4f91f7e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,9 +1840,8 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" -"telnet-socket@github:NuSkooler/telnet-socket": +telnet-socket@../telnet-socket: version "0.1.0" - resolved "https://codeload.github.com/NuSkooler/telnet-socket/tar.gz/a29ea2edaec98bb844f2b0d27fa0977f33eaa7bf" dependencies: binary-parser "^1.5.0" buffers "github:NuSkooler/node-buffers" From 1e6c577cd3001cc2cad7b4bf284efa6002896ad8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 May 2020 19:16:40 -0600 Subject: [PATCH 11/25] More ANSI-BBS compliant... still some to go --- core/servers/login/telnet.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 318dd9fc..fe0393c2 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -42,7 +42,7 @@ class TelnetClient { this.socket.on('data', this.dataHandler); this.socket.on('error', err => { - this._logDebug( { error : err.message }, 'Socket error'); + this._logDebug({ error : err.message }, 'Socket error'); return this.emit('end'); }); @@ -50,12 +50,17 @@ class TelnetClient { this.emit('end'); }); - // :TODO: handle 'command error' event + this.socket.on('command error', (command, err) => { + this._logDebug({ command, error : err.message }, 'Command error'); + }); this.socket.on('DO', command => { switch (command.option) { - case Options.ECHO : - return this.socket.will.echo(); + case Options.SGA : + return this.socket.will.sga(); + + case Options.TRANSMIT_BINARY : + return this.socket.will.transmit_binary(); default : return this.socket.command(Commands.WONT, command.option); @@ -68,9 +73,6 @@ class TelnetClient { this.socket.on('WILL', command => { switch (command.option) { - case Options.LINEMODE : - return this.socket.dont.linemode(); - case Options.TTYPE : return this.socket.sb.send.ttype(); @@ -197,8 +199,7 @@ class TelnetClient { } banner() { - this.socket.do.echo(); - this.socket.will.echo(); // we'll echo back + this.socket.will.echo(); // we'll echo back this.socket.will.sga(); this.socket.do.sga(); From 2a93de94876c8881be802081e11a0e12227e83a5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 May 2020 21:22:15 -0600 Subject: [PATCH 12/25] Yet more ANSI-BBS related updates --- core/servers/login/telnet.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index fe0393c2..39f8abbb 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -56,11 +56,14 @@ class TelnetClient { this.socket.on('DO', command => { switch (command.option) { + // We've already stated we WILL do the following via + // the banner - some terminals will ask over and over + // if we respond to a DO with a WILL, so just don't + // do anything... case Options.SGA : - return this.socket.will.sga(); - + case Options.ECHO : case Options.TRANSMIT_BINARY : - return this.socket.will.transmit_binary(); + break; default : return this.socket.command(Commands.WONT, command.option); @@ -199,6 +202,7 @@ class TelnetClient { } banner() { + this.socket.do.echo(); this.socket.will.echo(); // we'll echo back this.socket.will.sga(); From 5aaf568ed55b79c5e1af9af56ff0d06ccbd8178b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 May 2020 21:22:49 -0600 Subject: [PATCH 13/25] Fix bug reported by user before I forget --- core/door_util.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/door_util.js b/core/door_util.js index 6517f1be..24c2cd80 100644 --- a/core/door_util.js +++ b/core/door_util.js @@ -16,6 +16,10 @@ function trackDoorRunBegin(client, doorTag) { } function trackDoorRunEnd(trackInfo) { + if (!trackInfo) { + return; + } + const { startTime, client, doorTag } = trackInfo; const diff = moment.duration(moment().diff(startTime)); From eeba6095607a6ecd872abbc80bee2c2c7b230bff Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 22 May 2020 11:41:59 -0600 Subject: [PATCH 14/25] This is broken WIP. Goign to refactor, but on the road... --- core/servers/login/websocket.js | 89 ++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index f0cdc0b1..a8e9ad1b 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -15,6 +15,7 @@ const http = require('http'); const https = require('https'); const fs = require('graceful-fs'); const Writable = require('stream'); +const { Duplex } = require('stream'); const forEachSeries = require('async/forEachSeries'); const ModuleInfo = exports.moduleInfo = { @@ -30,45 +31,83 @@ class WebSocketClient extends TelnetClient { // This bridge makes accessible various calls that client sub classes // want to access on I/O socket // - const socketBridge = new class SocketBridge extends Writable { + const socketBridge = new class SocketBridge extends Duplex { constructor(ws) { super(); this.ws = ws; + + this.ws.on('close', err => this.emit('close', err)); + //this.ws.on('connect', () => this.emit('connect')); + //this.ws.on('drain', () => this.emit('drain')); + //this.ws.on('end', () => this.emit('end')); + this.ws.on('error', err => this.emit('error', err)); + + //this.ws.on('ready', () => this.emit('ready')); + //this.ws.on('timeout', () => this.emit('timeout')); + this.ws.on('data', data => this._data(data)); } setClient(client) { this.client = client; } - end() { - return ws.close(); - } - - write(data, cb) { - cb = cb || ( () => { /* eat it up */} ); // handle data writes after close - - return this.ws.send(data, { binary : true }, cb); - } - - pipe(dest) { - Log.trace('WebSocket SocketBridge pipe()'); - this.client.pipedDest = dest; - } - - unpipe() { - Log.trace('WebSocket SocketBridge unpipe()'); - this.client.pipedDest = null; - } - - resume() { - Log.trace('WebSocket SocketBridge resume()'); - } - get remoteAddress() { // Support X-Forwarded-For and X-Real-IP headers for proxied connections return (this.client.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; } + + _write(data, encoding, cb) { + cb = cb || ( () => { /* eat it up */} ); // handle data writes after close + return this.ws.send(data, { binary : true }, cb); + } + + _read() { + // dummy + } + + _data(data) { + this.push(data); + } }(ws); + // const socketBridge = new class SocketBridge extends Writable { + // constructor(ws) { + // super(); + // this.ws = ws; + // } + + // setClient(client) { + // this.client = client; + // } + + // end() { + // return ws.close(); + // } + + // write(data, cb) { + // cb = cb || ( () => { /* eat it up */} ); // handle data writes after close + + // return this.ws.send(data, { binary : true }, cb); + // } + + // pipe(dest) { + // Log.trace('WebSocket SocketBridge pipe()'); + // this.client.pipedDest = dest; + // } + + // unpipe() { + // Log.trace('WebSocket SocketBridge unpipe()'); + // this.client.pipedDest = null; + // } + + // resume() { + // Log.trace('WebSocket SocketBridge resume()'); + // } + + // get remoteAddress() { + // // Support X-Forwarded-For and X-Real-IP headers for proxied connections + // return (this.client.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; + // } + // }(ws); // :TODO: this is quite the clusterfuck... super(socketBridge); From 2234a717059f6e281ab62857c46c510a290d2328 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 22 May 2020 12:55:52 -0600 Subject: [PATCH 15/25] Working much nicer --- core/servers/login/websocket.js | 85 ++++----------------------------- 1 file changed, 10 insertions(+), 75 deletions(-) diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index a8e9ad1b..fcf0ea07 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -27,24 +27,15 @@ const ModuleInfo = exports.moduleInfo = { class WebSocketClient extends TelnetClient { constructor(ws, req, serverType) { - // - // This bridge makes accessible various calls that client sub classes - // want to access on I/O socket - // - const socketBridge = new class SocketBridge extends Duplex { + // allow WebSocket to act like a Duplex (socket) + const wsDuplex = new class WebSocketDuplex extends Duplex { constructor(ws) { super(); this.ws = ws; this.ws.on('close', err => this.emit('close', err)); - //this.ws.on('connect', () => this.emit('connect')); - //this.ws.on('drain', () => this.emit('drain')); - //this.ws.on('end', () => this.emit('end')); this.ws.on('error', err => this.emit('error', err)); - - //this.ws.on('ready', () => this.emit('ready')); - //this.ws.on('timeout', () => this.emit('timeout')); - this.ws.on('data', data => this._data(data)); + this.ws.on('message', data => this._data(data)); } setClient(client) { @@ -69,73 +60,22 @@ class WebSocketClient extends TelnetClient { this.push(data); } }(ws); - // const socketBridge = new class SocketBridge extends Writable { - // constructor(ws) { - // super(); - // this.ws = ws; - // } - // setClient(client) { - // this.client = client; - // } + super(wsDuplex); + wsDuplex.setClient(this); - // end() { - // return ws.close(); - // } - - // write(data, cb) { - // cb = cb || ( () => { /* eat it up */} ); // handle data writes after close - - // return this.ws.send(data, { binary : true }, cb); - // } - - // pipe(dest) { - // Log.trace('WebSocket SocketBridge pipe()'); - // this.client.pipedDest = dest; - // } - - // unpipe() { - // Log.trace('WebSocket SocketBridge unpipe()'); - // this.client.pipedDest = null; - // } - - // resume() { - // Log.trace('WebSocket SocketBridge resume()'); - // } - - // get remoteAddress() { - // // Support X-Forwarded-For and X-Real-IP headers for proxied connections - // return (this.client.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; - // } - // }(ws); - - // :TODO: this is quite the clusterfuck... - super(socketBridge); - this.socketBridge = socketBridge; - this.serverType = serverType; - - this.socketBridge.setClient(this); - - this.dataHandler = function(data) { - if(this.pipedDest) { - this.pipedDest.write(data); - } else { - this.socketBridge.emit('data', data); - } - }.bind(this); - - ws.on('message', this.dataHandler); - - ws.on('close', () => { + wsDuplex.on('close', () => { // we'll remove client connection which will in turn end() via our SocketBridge above return this.emit('end'); }); + this.serverType = serverType; + // // Monitor connection status with ping/pong // ws.on('pong', () => { - Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); + Log.trace(`Pong from ${wsDuplex.remoteAddress}`); ws.isConnectionAlive = true; }); @@ -261,7 +201,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { server.wsServer.on('connection', (ws, req) => { const webSocketClient = new WebSocketClient(ws, req, serverType); - this.handleNewClient(webSocketClient, webSocketClient.socketBridge, ModuleInfo); + this.handleNewClient(webSocketClient, webSocketClient.socket, ModuleInfo); }); Log.info( { server : serverName, port : port }, 'Listening for connections' ); @@ -272,9 +212,4 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { cb(err); }); } - - webSocketConnection(conn) { - const webSocketClient = new WebSocketClient(conn); - this.handleNewClient(webSocketClient, webSocketClient.socketShim, ModuleInfo); - } }; From 0bf3031d9f2dfa86eae073fe912c873af2c21571 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 26 May 2020 22:55:51 -0600 Subject: [PATCH 16/25] WIP on better data handling for file transfer --- WHATSNEW.md | 2 +- core/config.js | 3 +- core/file_transfer.js | 121 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 115 insertions(+), 11 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index bf6ac243..21626d92 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -6,7 +6,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Development is now against Node.js 12.x LTS. Other versions may work but are not currently supported! * [QWK support](/docs/messageareas/qwk.md) * `oputil fb scan *areaTagWildcard*` scans all areas in which wildcard is matched. - +* The archiver configuration `escapeTelnet` has been renamed `escapeIACs`. Support for the old value will be removed in the future. ## 0.0.10-alpha + `oputil.js user rename USERNAME NEWNAME` diff --git a/core/config.js b/core/config.js index 66ffab14..7e467709 100644 --- a/core/config.js +++ b/core/config.js @@ -865,8 +865,7 @@ function getDefaultConfig() { recvArgs : [ '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} ], - // :TODO: can we not just use --escape ? - escapeTelnet : true, // set to true to escape Telnet codes such as IAC + processIACs : true, // escape/de-escape IACs (0xff) } } }, diff --git a/core/file_transfer.js b/core/file_transfer.js index c5d5030c..bb4427b7 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -364,6 +364,16 @@ exports.getModule = class TransferFileModule extends MenuModule { const external = this.protocolConfig.external; const cmd = external[`${this.direction}Cmd`]; + // support for handlers that need IACs taken care of over Telnet/etc. + const processIACs = + external.processIACs || + external.escapeTelnet; // deprecated name + + // :TODO: we should only do this when over Telnet (or derived, such as WebSockets)? + + const IAC = Buffer.from([255]); + const EscapedIAC = Buffer.from([255, 255]); + this.client.log.debug( { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, 'Executing external protocol' @@ -385,14 +395,92 @@ exports.getModule = class TransferFileModule extends MenuModule { } }; + // const procBuffer = []; + + // let buffering = false; + // const procWrite = (data) => { + // if (!externalProc._socket) { + // return externalProc.write(data); + // } + + // if (buffering) { + // return procBuffer.push(data); + // } + + // // while (procBuffer.length && externalProc._socket.writable) { + // // externalProc.write(procBuffer.unshift()); + // // } + + // if (externalProc._socket.writable) { + // externalProc.write(data); + // } else { + // // last write put us into buffer mode + // externalProc._socket.once('drain', () => { + // while (procBuffer.length && externalProc._socket.writable) { + // externalProc.write(procBuffer.unshift()); + // } + // buffering = !externalProc._socket.writable; + // }); + // buffering = true; + // procBuffer.push(data); + // } + // }; + + // const writeData = (data) => { + // updateActivity(); + + // if(processIACs) { + // let iacPos = data.indexOf(EscapedIAC); + // if (-1 === iacPos) { + // return procWrite(data); + // } + + // // at least one double (escaped) IAC + // let lastPos = 0; + // while (iacPos > -1) { + // let rem = iacPos - lastPos; + // if (rem >= 0) { + // procWrite(data.slice(lastPos, iacPos + 1)); + // } + // lastPos = iacPos + 2; + // iacPos = data.indexOf(EscapedIAC, lastPos); + // } + + // if (lastPos < data.length) { + // procWrite(data.slice(lastPos)); + // } + // } else { + // procWrite(data); + // } + // }; + this.client.setTemporaryDirectDataHandler(data => { updateActivity(); - // needed for things like sz/rz - if(external.escapeTelnet) { - // :TODO: do this faster for already-buffers... - const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape - externalProc.write(Buffer.from(tmp, 'binary')); + if(processIACs) { + let iacPos = data.indexOf(EscapedIAC); + if (-1 === iacPos) { + return externalProc.write(data); + } + + // at least one double (escaped) IAC + externalProc._socket.cork(); + let lastPos = 0; + while (iacPos > -1) { + let rem = iacPos - lastPos; + if (rem >= 0) { + externalProc.write(data.slice(lastPos, iacPos + 1)); + } + lastPos = iacPos + 2; + iacPos = data.indexOf(EscapedIAC, lastPos); + } + + if (lastPos < data.length) { + externalProc.write(data.slice(lastPos)); + } + externalProc._socket.uncork(); + // const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape + // externalProc.write(Buffer.from(tmp, 'binary')); } else { externalProc.write(data); } @@ -402,9 +490,26 @@ exports.getModule = class TransferFileModule extends MenuModule { updateActivity(); // needed for things like sz/rz - if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape - this.client.term.rawWrite(Buffer.from(tmp, 'binary')); + if(processIACs) { + let iacPos = data.indexOf(IAC); + if (-1 === iacPos) { + return this.client.term.rawWrite(data); + } + + // Has at least a single IAC + let lastPos = 0; + while (iacPos !== -1) { + if (iacPos - lastPos > 0) { + this.client.term.rawWrite(data.slice(lastPos, iacPos)); + } + this.client.term.rawWrite(EscapedIAC); + lastPos = iacPos + 1; + iacPos = data.indexOf(IAC, lastPos); + } + + if (lastPos < data.length) { + this.client.term.rawWrite(data.slice(lastPos)); + } } else { this.client.term.rawWrite(data); } From a1c9d8538f4c0fbee4fbb872835296d0d45a24e4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 27 May 2020 19:24:45 -0600 Subject: [PATCH 17/25] Don't cork --- core/file_transfer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/file_transfer.js b/core/file_transfer.js index bb4427b7..20e60e07 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -464,7 +464,6 @@ exports.getModule = class TransferFileModule extends MenuModule { } // at least one double (escaped) IAC - externalProc._socket.cork(); let lastPos = 0; while (iacPos > -1) { let rem = iacPos - lastPos; @@ -478,7 +477,6 @@ exports.getModule = class TransferFileModule extends MenuModule { if (lastPos < data.length) { externalProc.write(data.slice(lastPos)); } - externalProc._socket.uncork(); // const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape // externalProc.write(Buffer.from(tmp, 'binary')); } else { From 4dea9bdeb615316578a58645c70dd35dd6d9d23f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 27 May 2020 19:25:15 -0600 Subject: [PATCH 18/25] Remove dead code --- core/file_transfer.js | 59 ------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/core/file_transfer.js b/core/file_transfer.js index 20e60e07..90f38667 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -395,65 +395,6 @@ exports.getModule = class TransferFileModule extends MenuModule { } }; - // const procBuffer = []; - - // let buffering = false; - // const procWrite = (data) => { - // if (!externalProc._socket) { - // return externalProc.write(data); - // } - - // if (buffering) { - // return procBuffer.push(data); - // } - - // // while (procBuffer.length && externalProc._socket.writable) { - // // externalProc.write(procBuffer.unshift()); - // // } - - // if (externalProc._socket.writable) { - // externalProc.write(data); - // } else { - // // last write put us into buffer mode - // externalProc._socket.once('drain', () => { - // while (procBuffer.length && externalProc._socket.writable) { - // externalProc.write(procBuffer.unshift()); - // } - // buffering = !externalProc._socket.writable; - // }); - // buffering = true; - // procBuffer.push(data); - // } - // }; - - // const writeData = (data) => { - // updateActivity(); - - // if(processIACs) { - // let iacPos = data.indexOf(EscapedIAC); - // if (-1 === iacPos) { - // return procWrite(data); - // } - - // // at least one double (escaped) IAC - // let lastPos = 0; - // while (iacPos > -1) { - // let rem = iacPos - lastPos; - // if (rem >= 0) { - // procWrite(data.slice(lastPos, iacPos + 1)); - // } - // lastPos = iacPos + 2; - // iacPos = data.indexOf(EscapedIAC, lastPos); - // } - - // if (lastPos < data.length) { - // procWrite(data.slice(lastPos)); - // } - // } else { - // procWrite(data); - // } - // }; - this.client.setTemporaryDirectDataHandler(data => { updateActivity(); // needed for things like sz/rz From 87a23bd3c889b2b7ab758fe59ad9147744d0e73f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 27 May 2020 21:05:19 -0600 Subject: [PATCH 19/25] Telnet Bridge updates * Use telnet-socket TelnetSpec/etc. consts & helpers * Enable passthrough --- core/file_transfer.js | 1 + core/telnet_bridge.js | 43 +++++++++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/core/file_transfer.js b/core/file_transfer.js index 90f38667..68daf2db 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -397,6 +397,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.client.setTemporaryDirectDataHandler(data => { updateActivity(); + // needed for things like sz/rz if(processIACs) { let iacPos = data.indexOf(EscapedIAC); diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index a3a4d672..5c43ced6 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -11,7 +11,16 @@ const async = require('async'); const _ = require('lodash'); const net = require('net'); const EventEmitter = require('events'); -const buffers = require('buffers'); + +const { + TelnetSocket, + TelnetSpec : + { + Commands, + Options, + SubNegotiationCommands, + }, +} = require('telnet-socket'); /* Expected configuration block: @@ -33,7 +42,10 @@ exports.moduleInfo = { author : 'Andrew Pamment', }; -const IAC_DO_TERM_TYPE = Buffer.from( [ 255, 253, 24 ] ); +const IAC_DO_TERM_TYPE = TelnetSocket.commandBuffer( + Commands.DO, + Options.TTYPE, +); class TelnetClientConnection extends EventEmitter { constructor(client) { @@ -46,6 +58,7 @@ class TelnetClientConnection extends EventEmitter { restorePipe() { if(!this.pipeRestored) { this.pipeRestored = true; + this.client.dataPassthrough = false; // client may have bailed if(null !== _.get(this, 'client.term.output', null)) { @@ -62,6 +75,7 @@ class TelnetClientConnection extends EventEmitter { this.emit('connected'); this.pipeRestored = false; + this.client.dataPassthrough = true; this.client.term.output.pipe(this.bridgeConnection); }); @@ -69,7 +83,7 @@ class TelnetClientConnection extends EventEmitter { this.client.term.rawWrite(data); // - // Wait for a terminal type request, and send it eactly once. + // Wait for a terminal type request, and send it exactly once. // This is enough (in additional to other negotiations handled in telnet.js) // to get us in on most systems // @@ -110,25 +124,18 @@ class TelnetClientConnection extends EventEmitter { // Create a TERMINAL-TYPE sub negotiation buffer using the // actual/current terminal type. // - let bufs = buffers(); - - bufs.push(Buffer.from( + const sendTermType = TelnetSocket.commandBuffer( + Commands.SB, + Options.TTYPE, [ - 255, // IAC - 250, // SB - 24, // TERMINAL-TYPE - 0, // IS + SubNegotiationCommands.IS, + ...Buffer.from(this.client.term.termType), // e.g. "ansi" + Commands.IAC, + Commands.SE, ] - )); - - bufs.push( - Buffer.from(this.client.term.termType), // e.g. "ansi" - Buffer.from( [ 255, 240 ] ) // IAC, SE ); - - return bufs.toBuffer(); + return sendTermType; } - } exports.getModule = class TelnetBridgeModule extends MenuModule { From 438a3161d0a42f94bf9a2a88ff04296f75c987ad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 27 May 2020 21:31:51 -0600 Subject: [PATCH 20/25] Fix up remoteAddress for WebSocket connections --- core/servers/login/websocket.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index fcf0ea07..42a22723 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -38,13 +38,17 @@ class WebSocketClient extends TelnetClient { this.ws.on('message', data => this._data(data)); } - setClient(client) { + setClient(client, httpRequest) { this.client = client; + + // Support X-Forwarded-For and X-Real-IP headers for proxied connections + this.resolvedRemoteAddress = + (this.client.proxied && (httpRequest.headers['x-forwarded-for'] || httpRequest.headers['x-real-ip'])) || + httpRequest.connection.remoteAddress; } get remoteAddress() { - // Support X-Forwarded-For and X-Real-IP headers for proxied connections - return (this.client.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; + return this.resolvedRemoteAddress; } _write(data, encoding, cb) { @@ -62,7 +66,10 @@ class WebSocketClient extends TelnetClient { }(ws); super(wsDuplex); - wsDuplex.setClient(this); + wsDuplex.setClient(this, req); + + // fudge remoteAddress on socket, which is now TelnetSocket + this.socket.remoteAddress = wsDuplex.remoteAddress; wsDuplex.on('close', () => { // we'll remove client connection which will in turn end() via our SocketBridge above From fb22d89328ba1d14eed9c00caaa8faefe260d7ad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 27 May 2020 21:52:28 -0600 Subject: [PATCH 21/25] Use telnet-socket published package --- package.json | 2 +- yarn.lock | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e0e5f0d1..4effb16b 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "ws": "^7.3.0", "xxhash": "^0.3.0", "yazl": "^2.5.1", - "telnet-socket" : "../telnet-socket" + "telnet-socket" : "^0.2.1" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index 4f91f7e2..12199dfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -160,7 +160,7 @@ bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -binary-parser@^1.5.0, binary-parser@^1.6.2: +binary-parser@1.6.2, binary-parser@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.6.2.tgz#8410a82ffd9403271ec182bd91e63a09cee88cbe" integrity sha512-cYAhKB51A9T/uylDvMK7uAYaPLWLwlferNOpnQ0E0fuO73yPi7kWaWiOm22BvuKxCbggmkiFN0VkuLg6gc+KQQ== @@ -1840,10 +1840,12 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" -telnet-socket@../telnet-socket: - version "0.1.0" +telnet-socket@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/telnet-socket/-/telnet-socket-0.2.1.tgz#1b7d74152a90740a630c166070ab6e751f50fda1" + integrity sha512-8o5kIq5CGvaEuVbVTl40Lw9NCz2v68Il2Skb7iMLuid23kWV3Sq5AAg5lIl5o0H7agoJkEGNlmMRp/6+saDaNA== dependencies: - binary-parser "^1.5.0" + binary-parser "1.6.2" buffers "github:NuSkooler/node-buffers" temptmp@^1.1.0: From a15abc60627a4886c1914bf10e2bb623c7d6f640 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 May 2020 23:25:34 -0600 Subject: [PATCH 22/25] Use telnet-socket 0.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4effb16b..19cf7662 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "ws": "^7.3.0", "xxhash": "^0.3.0", "yazl": "^2.5.1", - "telnet-socket" : "^0.2.1" + "telnet-socket" : "^0.2.2" }, "devDependencies": {}, "engines": { From 29ef0935c3cef51081c3bd33b919ea386afc2995 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 31 May 2020 11:49:32 -0600 Subject: [PATCH 23/25] Don't send back don't in respond to wont new_environ --- core/servers/login/telnet.js | 8 +------- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 39f8abbb..cf90b92f 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -90,13 +90,7 @@ class TelnetClient { }); this.socket.on('WONT', command => { - switch (command.option) { - case Options.NEW_ENVIRON : - return this.socket.dont.new_environ(); - - default : - return this._logTrace(command, 'WONT'); - } + return this._logTrace(command, 'WONT'); }); this.socket.on('SB', command => { diff --git a/yarn.lock b/yarn.lock index 12199dfe..fb655a43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,10 +1840,10 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" -telnet-socket@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/telnet-socket/-/telnet-socket-0.2.1.tgz#1b7d74152a90740a630c166070ab6e751f50fda1" - integrity sha512-8o5kIq5CGvaEuVbVTl40Lw9NCz2v68Il2Skb7iMLuid23kWV3Sq5AAg5lIl5o0H7agoJkEGNlmMRp/6+saDaNA== +telnet-socket@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/telnet-socket/-/telnet-socket-0.2.2.tgz#9abf29bbd022f2bc1176fd35ab62ca94e62916c1" + integrity sha512-fKcpbCNn8NQ1wBshjliTLh4UmbXq0iYhoPsc48RTJfgdisxQOyLJe8dMwH26ezx03pEM4GWpg9ge+g4DGz7c/Q== dependencies: binary-parser "1.6.2" buffers "github:NuSkooler/node-buffers" From 74078939cc85e07e06aa167630404e67084c4a56 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 31 May 2020 14:54:33 -0600 Subject: [PATCH 24/25] DO should be a DONT for echo --- core/servers/login/telnet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index cf90b92f..240ce06b 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -196,8 +196,8 @@ class TelnetClient { } banner() { - this.socket.do.echo(); - this.socket.will.echo(); // we'll echo back + this.socket.dont.echo(); // don't echo characters + this.socket.will.echo(); // ...we'll echo them back this.socket.will.sga(); this.socket.do.sga(); From 40739d028f8c960f61c6896b5bb117f902cdc0ca Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 1 Jun 2020 21:18:12 -0600 Subject: [PATCH 25/25] Update to telnet-socket 0.2.3 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 19cf7662..aee75136 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "ws": "^7.3.0", "xxhash": "^0.3.0", "yazl": "^2.5.1", - "telnet-socket" : "^0.2.2" + "telnet-socket" : "^0.2.3" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock index fb655a43..0954c769 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,10 +1840,10 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" -telnet-socket@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/telnet-socket/-/telnet-socket-0.2.2.tgz#9abf29bbd022f2bc1176fd35ab62ca94e62916c1" - integrity sha512-fKcpbCNn8NQ1wBshjliTLh4UmbXq0iYhoPsc48RTJfgdisxQOyLJe8dMwH26ezx03pEM4GWpg9ge+g4DGz7c/Q== +telnet-socket@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/telnet-socket/-/telnet-socket-0.2.3.tgz#0ffdc64ea957cb64f8ac5287d45a857f1c05a16e" + integrity sha512-PbZycTkGq6VcVUa35FYFySx4pCzmJo4xoMX6cimls1/kv/lrgMfddKfgjBKt6HQuokkkDfieDhGLq/L/P2Unaw== dependencies: binary-parser "1.6.2" buffers "github:NuSkooler/node-buffers"