From f9e65fc96887478bfcb00a90f2eb25800a5e7fc6 Mon Sep 17 00:00:00 2001
From: echicken <echicken@bbs.electronicchicken.com>
Date: Thu, 31 Dec 2015 02:52:09 -0500
Subject: [PATCH] De-obfuscate crappy RLogin client.  Meh.

---
 mods/websocket-rlogin-service.js | 346 ++++++++++++++++++++++++++++++-
 1 file changed, 340 insertions(+), 6 deletions(-)

diff --git a/mods/websocket-rlogin-service.js b/mods/websocket-rlogin-service.js
index 9ba685899b..7d653ec550 100644
--- a/mods/websocket-rlogin-service.js
+++ b/mods/websocket-rlogin-service.js
@@ -17,10 +17,342 @@ function getSession(un) {
 	return session;
 }
 
-// Obfuscated lazy port of an unfinished rlogin client I made quite some time
-// ago. It does what it needs to do.  Unless it doesn't - in which case,
-// replace it with something better.
-var RLogin=function(e){var n=this;const t=24,i=13,s=17,o=19,c=10;var r=[],p=[],u={connected:!1,cooked:!0,suspendInput:!1,suspendOutput:!1,watchForClientEscape:!0,clientHasEscaped:!1},d={rows:24,columns:80,pixelsX:640,pixelsY:480,clientEscape:"~"},f={DOT:n.disconnect,EOT:n.disconnect,SUB:function(){u.suspendInput=u.suspendInput?!1:!0,u.suspendOutput=u.suspendInput},EOM:function(){u.suspendInput=u.suspendInput?!1:!0,u.suspendOutput=!1}};this.__defineGetter__("connected",function(){return u.connected}),this.__defineSetter__("connected",function(e){"boolean"!=typeof e||e||n.disconnect()}),this.__defineGetter__("rows",function(){return d.rows}),this.__defineSetter__("rows",function(e){if(!("number"==typeof e&&e>0))throw"RLogin: Invalid 'rows' setting "+e;d.rows=e}),this.__defineGetter__("columns",function(){return d.columns}),this.__defineSetter__("columns",function(e){if(!("number"==typeof e&&e>0))throw"RLogin: Invalid 'columns' setting "+e;d.columns=e}),this.__defineGetter__("pixelsX",function(){return d.pixelsX}),this.__defineSetter__("pixelsX",function(e){if(!("number"==typeof e&&e>0))throw"RLogin: Invalid 'pixelsX' setting "+e;d.pixelsX=e}),this.__defineGetter__("pixelsY",function(){return d.pixelsY}),this.__defineSetter__("pixelsY",function(e){if(!("number"==typeof e&&e>0))throw"RLogin: Invalid 'pixelsY' setting "+e;d.pixelsY=e}),this.__defineGetter__("clientEscape",function(){return d.clientEscape}),this.__defineSetter__("clientEscape",function(e){if("string"!=typeof e||1!=e.length)throw"RLogin: Invalid 'clientEscape' setting "+e;d.clientEscape=e});var a=new Socket,l=function(){if(!(a.nread<1)){for(var e=[];a.nread>0;)e.push(a.recvBin(1));if(!u.connected)if(0==e[0]){if(u.connected=!0,!(e.length>1))return;e=e.slice(1)}else n.disconnect();u.suspendOutput||(r=r.concat(e))}};this.send=function(e){if(!u.connected)throw"RLogin.send: not connected.";if(u.suspendInput)throw"RLogin.send: input has been suspended.";"string"==typeof e&&(e=e.split("").map(function(e){return ascii(e)}));for(var n=[],r=0;r<e.length;r++)u.watchForClientEscape&&e[r]==d.clientEscape.charCodeAt(0)?(u.watchForClientEscape=!1,u.clientHasEscaped=!0):u.clientHasEscaped?(u.clientHasEscaped=!1,"undefined"!=typeof f[e[r]]&&f[e[r]]()):!u.cooked||e[r]!=s&&e[r]!=o?((r>0&&e[r-1]==i&&e[r]==c||e[r]==t)&&(u.watchForClientEscape=!0),n.push(e[r])):u.suspendOutput==(e[r]==o);p=p.concat(n)},this.receive=function(){return r.splice(0,r.length)},this.addClientEscape=function(e,n){if("string"!=typeof e&&"number"!=typeof e||"string"==typeof e&&e.length>1||"function"!=typeof n)throw"RLogin.addClientEscape: invalid arguments.";f[e.charCodeAt(0)]=n},this.connect=function(){if("number"!=typeof e.port||"string"!=typeof e.host)throw"RLogin: invalid host or port argument.";if("string"!=typeof e.clientUsername)throw"RLogin: invalid clientUsername argument.";if("string"!=typeof e.serverUsername)throw"RLogin: invalid serverUsername argument.";if("string"!=typeof e.terminalType)throw"RLogin: invalid terminalType argument.";if("number"!=typeof e.terminalSpeed)throw"RLogin: invalid terminalSpeed argument.";if(!a.connect(e.host,e.port))throw"RLogin: Unable to connect to server.";for(a.sendBin(0,1),a.send(e.clientUsername),a.sendBin(0,1),a.send(e.serverUsername),a.sendBin(0,1),a.send(e.terminalType+"/"+e.terminalSpeed),a.sendBin(0,1);a.is_connected&&a.nread<1;)mswait(5);l()},this.cycle=function(){if(l(),!(u.suspendInput||p.length<1))for(;p.length>0;)a.sendBin(p.shift(),1)},this.disconnect=function(){a.close(),u.connected=!1}};
+var RLoginClient = function(options) {
+
+	var self = this;
+
+	const	CAN = 0x18,
+			CR = 0x0D,
+			DC1 = 0x11,
+			DC3 = 0x13,
+			DOT = 0x2E,
+			EOM = 0x19,
+			EOT = 0x04,
+			LF = 0x0A,
+			SUB = 0x1A,
+			DISCARD = 0x02,
+			RAW = 0x10,
+			COOKED = 0x20,
+			WINDOW = 0x80;
+
+	var serverBuffer = []; // From server
+	var clientBuffer = []; // From client
+
+	var state = {
+		connected : false,
+		cooked : true,
+		suspendInput : false,
+		suspendOutput : false,
+		watchForClientEscape : true,
+		clientHasEscaped : false
+	};
+
+	var properties = {
+		rows : 24,
+		columns : 80,
+		pixelsX : 640,
+		pixelsY : 480,
+		clientEscape : '~'
+	};
+
+	// As suggested by RFC1282
+	var clientEscapes = {
+		DOT : self.disconnect,
+		EOT : self.disconnect,
+		SUB : function() {
+			state.suspendInput = (state.suspendInput) ? false : true;
+			state.suspendOutput = state.suspendInput;
+		},
+		EOM : function() {
+			state.suspendInput = (state.suspendInput) ? false : true;
+			state.suspendOutput = false;
+		}
+	};
+
+	this.__defineGetter__('connected', function () { return state.connected; });
+
+	this.__defineSetter__(
+		'connected',
+		function (value) {
+			if (typeof value === 'boolean' && !value) self.disconnect();
+		}
+	);
+
+	this.__defineGetter__('rows', function () { return properties.rows; });
+
+	this.__defineSetter__(
+		'rows',
+		function(value) {
+			if (typeof value === 'number' && value > 0) {
+				properties.rows = value;
+			} else {
+				throw 'RLogin: Invalid \'rows\' setting ' + value;
+			}
+		}
+	);
+
+	this.__defineGetter__(
+		'columns',
+		function () { return properties.columns; }
+	);
+
+	this.__defineSetter__(
+		'columns',
+		function (value) {
+			if (typeof value === 'number' && value > 0) {
+				properties.columns = value;
+			} else {
+				throw 'RLogin: Invalid \'columns\' setting ' + value;
+			}
+		}
+	);
+
+	this.__defineGetter__(
+		'pixelsX',
+		function () { return properties.pixelsX; }
+	);
+
+	this.__defineSetter__(
+		'pixelsX',
+		function (value) {
+			if (typeof value === 'number' && value > 0) {
+				properties.pixelsX = value;
+			} else {
+				throw 'RLogin: Invalid \'pixelsX\' setting ' + value;
+			}
+		}
+	);
+
+	this.__defineGetter__(
+		'pixelsY',
+		function () { return properties.pixelsY; }
+	);
+
+	this.__defineSetter__(
+		'pixelsY',
+		function (value) {
+			if (typeof value === 'number' && value > 0) {
+				properties.pixelsY = value;
+			} else {
+				throw 'RLogin: Invalid \'pixelsY\' setting ' + value;
+			}
+		}
+	);
+
+	this.__defineGetter__(
+		'clientEscape',
+		function() { return properties.clientEscape; }
+	);
+
+	this.__defineSetter__(
+		'clientEscape',
+		function (value) {
+			if (typeof value === 'string' && value.length === 1) {
+				properties.clientEscape = value;
+			} else {
+				throw 'RLogin: Invalid \'clientEscape\' setting ' + value;
+			}
+		}
+	);
+
+	var handle = new Socket();
+
+	function getServerData() {
+
+		if (handle.nread < 1) return;
+
+		var data = [];
+		while (handle.nread > 0) {
+			data.push(handle.recvBin(1));
+		}
+
+		if (!state.connected) {
+			if (data[0] === 0) {
+				state.connected = true;
+				if (data.length > 1) {
+					data = data.slice(1);
+				} else {
+					return;
+				}
+			} else {
+				self.disconnect();
+			}
+		}
+
+		// If I could tell if the TCP urgent-data pointer had been set,
+		// I would uncomment (and complete) this block.  We'll settle
+		// for a partial implementation for the time being.
+		// We would need something to tell us if urgent data was sent,
+		// eg. var lookingForControlCode = urgentDataPointerIsSet();
+		/*
+		var temp = [];
+		for (var d = 0; d < data.length; d++) {
+			if (!lookingForControlCode) {
+				temp.push(data[d]);
+				continue;
+			}
+			switch (data[d]) {
+				case DISCARD:
+					temp = [];
+					// We found our control code
+					lookingForControlCode = false;
+					break;
+				case RAW:
+					state.cooked = false;
+					lookingForControlCode = false;
+					break;
+				case COOKED:
+					state.cooked = true;
+					lookingForControlCode = false;
+					break;
+				case WINDOW:
+					self.sendWCCS();
+					lookingForControlCode = false;
+					break;
+				default:
+					temp.push(data[d]);
+					break;
+			}
+		}
+		if (!state.suspendOutput) self.emit('data', new Buffer(temp));
+		*/
+		if (!state.suspendOutput) serverBuffer = serverBuffer.concat(data);
+	}
+
+	// Send a Window Change Control Sequence
+	// this.sendWCCS = function() {
+	// 	var magicCookie = [0xFF, 0xFF, 0x73, 0x73];
+	// 	var rcxy = new Buffer(8);
+	// 	rcxy.writeUInt16LE(properties.rows, 0);
+	// 	rcxy.writeUInt16LE(properties.columns, 2);
+	// 	rcxy.writeUInt16LE(properties.pixelsX, 4);
+	// 	rcxy.writeUInt16LE(properties.pixelsY, 6);
+	// 	if(state.connected)
+	// 		handle.write(Buffer.concat([magicCookie, rcxy]));
+	// }
+
+	// Send 'data' (String or Buffer) to the rlogin server
+	this.send = function (data) {
+
+		if (!state.connected) throw 'RLogin.send: not connected.';
+		if (state.suspendInput) throw 'RLogin.send: input has been suspended.';
+		
+		if (typeof data === 'string') {
+			data = data.split('').map(function (d) { return ascii(d); });
+		}
+
+		var temp = [];
+		for (var d = 0; d < data.length; d++) {
+			if (state.watchForClientEscape &&
+				data[d] == properties.clientEscape.charCodeAt(0)
+			) {
+				state.watchForClientEscape = false;
+				state.clientHasEscaped = true;
+				continue;
+			}
+			if (state.clientHasEscaped) {
+				state.clientHasEscaped = false;
+				if (typeof clientEscapes[data[d]] !== 'undefined') {
+					clientEscapes[data[d]]();
+				}
+				continue;
+			}
+			if (state.cooked && (data[d] === DC1 || data[d] === DC3)) {
+				state.suspendOutput = (data[d] === DC3);
+				continue;
+			}
+			if ((d > 0 && data[d - 1] === CR && data[d] === LF) ||
+				data[d] == CAN
+			) {
+				state.watchForClientEscape = true;
+			}
+			temp.push(data[d]);
+		}
+		clientBuffer = clientBuffer.concat(temp);
+
+	}
+
+	this.receive = function () {
+		return serverBuffer.splice(0, serverBuffer.length);
+	}
+
+	/*	If 'ch' is found in client input immediately after the
+		'this.clientEscape' character when:
+			- this is the first input after connection establishment or
+			- these are the first characters on a new line or
+			- these are the first characters after a line-cancel character
+		then the function 'callback' will be called.  Use this to allow
+		client input to trigger a particular action.	*/
+	this.addClientEscape = function (ch, callback) {
+		if(	(typeof ch !== 'string' && typeof ch !== 'number') ||
+			(typeof ch === 'string' && ch.length > 1) ||
+			typeof callback !== 'function'
+		) {
+			throw 'RLogin.addClientEscape: invalid arguments.';
+		}
+		clientEscapes[ch.charCodeAt(0)] = callback;
+	}
+
+	this.connect = function () {
+		
+		if (typeof options.port !== 'number' ||
+			typeof options.host != 'string'
+		) {
+			throw 'RLogin: invalid host or port argument.';
+		}
+		
+		if (typeof options.clientUsername !== 'string') {
+			throw 'RLogin: invalid clientUsername argument.';
+		}
+		
+		if (typeof options.serverUsername !== 'string') {
+			throw 'RLogin: invalid serverUsername argument.';
+		}
+		
+		if (typeof options.terminalType !== 'string') {
+			throw 'RLogin: invalid terminalType argument.';
+		}
+
+		if (typeof options.terminalSpeed !== 'number') {
+			throw 'RLogin: invalid terminalSpeed argument.';
+		}
+
+		if (handle.connect(options.host, options.port)) {
+			handle.sendBin(0, 1);
+			handle.send(options.clientUsername);
+			handle.sendBin(0, 1);
+			handle.send(options.serverUsername);
+			handle.sendBin(0, 1);
+			handle.send(options.terminalType + '/' + options.terminalSpeed);
+			handle.sendBin(0, 1);
+			while (handle.is_connected && handle.nread < 1) {
+				mswait(5);
+			}
+			getServerData();
+		} else {
+			throw 'RLogin: Unable to connect to server.';
+		}
+
+	}
+
+	this.cycle = function () {
+
+		getServerData();
+
+		if (state.suspendInput || clientBuffer.length < 1) return;
+
+		while (clientBuffer.length > 0) {
+			handle.sendBin(clientBuffer.shift(), 1);
+		}
+
+	}
+
+	this.disconnect = function () {
+		handle.close();
+		state.connected = false;
+	}
+
+}
 
 try {
 
@@ -65,7 +397,7 @@ try {
 	var ini = f.iniGetObject('BBS');
 	f.close();
 
-	rlogin = new RLogin(
+	rlogin = new RLoginClient(
 		{	host : system.inet_addr,
 			port : ini.RLoginPort,
 			clientUsername : usr.security.password,
@@ -90,11 +422,13 @@ try {
 			rlogin.send(data);
 		}
 
+		mswait(5);
+
 	}
 
 } catch (err) {
 
-	log(err);
+	log(LOG_ERR, err);
 
 } finally {
 	rlogin.disconnect();
-- 
GitLab