Merge pull request #283 from NuSkooler/new-telnet-server

New telnet server implementation based on telnet-socket, includes new websocket server (which inherits from telnet) and some misc updates to I/O.
This commit is contained in:
Bryan Ashby 2020-06-03 20:36:37 -06:00 committed by GitHub
commit 6258a39c57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 383 additions and 944 deletions

View File

@ -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! * Development is now against Node.js 12.x LTS. Other versions may work but are not currently supported!
* [QWK support](/docs/messageareas/qwk.md) * [QWK support](/docs/messageareas/qwk.md)
* `oputil fb scan *areaTagWildcard*` scans all areas in which wildcard is matched. * `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 ## 0.0.10-alpha
+ `oputil.js user rename USERNAME NEWNAME` + `oputil.js user rename USERNAME NEWNAME`

View File

@ -107,11 +107,13 @@ function Client(/*input, output*/) {
}); });
this.setTemporaryDirectDataHandler = function(handler) { this.setTemporaryDirectDataHandler = function(handler) {
this.dataPassthrough = true; // let implementations do with what they will here
this.input.removeAllListeners('data'); this.input.removeAllListeners('data');
this.input.on('data', handler); this.input.on('data', handler);
}; };
this.restoreDataHandler = function() { this.restoreDataHandler = function() {
this.dataPassthrough = false;
this.input.removeAllListeners('data'); this.input.removeAllListeners('data');
this.input.on('data', this.dataHandler); this.input.on('data', this.dataHandler);
}; };

View File

@ -865,8 +865,7 @@ function getDefaultConfig() {
recvArgs : [ recvArgs : [
'--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir}
], ],
// :TODO: can we not just use --escape ? processIACs : true, // escape/de-escape IACs (0xff)
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
} }
} }
}, },

View File

@ -16,6 +16,10 @@ function trackDoorRunBegin(client, doorTag) {
} }
function trackDoorRunEnd(trackInfo) { function trackDoorRunEnd(trackInfo) {
if (!trackInfo) {
return;
}
const { startTime, client, doorTag } = trackInfo; const { startTime, client, doorTag } = trackInfo;
const diff = moment.duration(moment().diff(startTime)); const diff = moment.duration(moment().diff(startTime));

View File

@ -364,6 +364,16 @@ exports.getModule = class TransferFileModule extends MenuModule {
const external = this.protocolConfig.external; const external = this.protocolConfig.external;
const cmd = external[`${this.direction}Cmd`]; 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( this.client.log.debug(
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction },
'Executing external protocol' 'Executing external protocol'
@ -389,9 +399,28 @@ exports.getModule = class TransferFileModule extends MenuModule {
updateActivity(); updateActivity();
// needed for things like sz/rz // needed for things like sz/rz
if(external.escapeTelnet) { if(processIACs) {
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape let iacPos = data.indexOf(EscapedIAC);
externalProc.write(Buffer.from(tmp, 'binary')); if (-1 === iacPos) {
return externalProc.write(data);
}
// at least one double (escaped) IAC
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));
}
// const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
// externalProc.write(Buffer.from(tmp, 'binary'));
} else { } else {
externalProc.write(data); externalProc.write(data);
} }
@ -401,9 +430,26 @@ exports.getModule = class TransferFileModule extends MenuModule {
updateActivity(); updateActivity();
// needed for things like sz/rz // needed for things like sz/rz
if(external.escapeTelnet) { if(processIACs) {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape let iacPos = data.indexOf(IAC);
this.client.term.rawWrite(Buffer.from(tmp, 'binary')); 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 { } else {
this.client.term.rawWrite(data); this.client.term.rawWrite(data);
} }

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ const http = require('http');
const https = require('https'); const https = require('https');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const Writable = require('stream'); const Writable = require('stream');
const { Duplex } = require('stream');
const forEachSeries = require('async/forEachSeries'); const forEachSeries = require('async/forEachSeries');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
@ -24,79 +25,67 @@ const ModuleInfo = exports.moduleInfo = {
packageName : 'codes.l33t.enigma.websocket.server', packageName : 'codes.l33t.enigma.websocket.server',
}; };
function WebSocketClient(ws, req, serverType) { class WebSocketClient extends TelnetClient {
constructor(ws, req, serverType) {
Object.defineProperty(this, 'isSecure', { // allow WebSocket to act like a Duplex (socket)
get : () => ('secure' === serverType || true === this.proxied) ? true : false, const wsDuplex = new class WebSocketDuplex extends Duplex {
});
const self = this;
this.dataHandler = function(data) {
if(self.pipedDest) {
self.pipedDest.write(data);
} 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) { constructor(ws) {
super(); super();
this.ws = ws; this.ws = ws;
this.ws.on('close', err => this.emit('close', err));
this.ws.on('error', err => this.emit('error', err));
this.ws.on('message', data => this._data(data));
} }
end() { setClient(client, httpRequest) {
return ws.close(); this.client = client;
}
write(data, cb) { // Support X-Forwarded-For and X-Real-IP headers for proxied connections
cb = cb || ( () => { /* eat it up */} ); // handle data writes after close this.resolvedRemoteAddress =
(this.client.proxied && (httpRequest.headers['x-forwarded-for'] || httpRequest.headers['x-real-ip'])) ||
return this.ws.send(data, { binary : true }, cb); httpRequest.connection.remoteAddress;
}
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() { get remoteAddress() {
// Support X-Forwarded-For and X-Real-IP headers for proxied connections return this.resolvedRemoteAddress;
return (self.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); }(ws);
ws.on('message', this.dataHandler); super(wsDuplex);
wsDuplex.setClient(this, req);
ws.on('close', () => { // 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 // we'll remove client connection which will in turn end() via our SocketBridge above
return this.emit('end'); return this.emit('end');
}); });
this.serverType = serverType;
// //
// Monitor connection status with ping/pong // Monitor connection status with ping/pong
// //
ws.on('pong', () => { ws.on('pong', () => {
Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); Log.trace(`Pong from ${wsDuplex.remoteAddress}`);
ws.isConnectionAlive = true; ws.isConnectionAlive = true;
}); });
TelnetClient.call(this, this.socketBridge, this.socketBridge);
Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); Log.trace( { headers : req.headers }, 'WebSocket connection headers' );
// //
@ -116,7 +105,10 @@ function WebSocketClient(ws, req, serverType) {
this.banner(); this.banner();
} }
require('util').inherits(WebSocketClient, TelnetClient); get isSecure() {
return ('secure' === this.serverType || true === this.proxied) ? true : false;
}
}
const WSS_SERVER_TYPES = [ 'insecure', 'secure' ]; const WSS_SERVER_TYPES = [ 'insecure', 'secure' ];
@ -216,7 +208,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
server.wsServer.on('connection', (ws, req) => { server.wsServer.on('connection', (ws, req) => {
const webSocketClient = new WebSocketClient(ws, req, serverType); 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' ); Log.info( { server : serverName, port : port }, 'Listening for connections' );
@ -227,9 +219,4 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
cb(err); cb(err);
}); });
} }
webSocketConnection(conn) {
const webSocketClient = new WebSocketClient(conn);
this.handleNewClient(webSocketClient, webSocketClient.socketShim, ModuleInfo);
}
}; };

View File

@ -11,7 +11,16 @@ const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const net = require('net'); const net = require('net');
const EventEmitter = require('events'); const EventEmitter = require('events');
const buffers = require('buffers');
const {
TelnetSocket,
TelnetSpec :
{
Commands,
Options,
SubNegotiationCommands,
},
} = require('telnet-socket');
/* /*
Expected configuration block: Expected configuration block:
@ -33,7 +42,10 @@ exports.moduleInfo = {
author : 'Andrew Pamment', 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 { class TelnetClientConnection extends EventEmitter {
constructor(client) { constructor(client) {
@ -46,6 +58,7 @@ class TelnetClientConnection extends EventEmitter {
restorePipe() { restorePipe() {
if(!this.pipeRestored) { if(!this.pipeRestored) {
this.pipeRestored = true; this.pipeRestored = true;
this.client.dataPassthrough = false;
// client may have bailed // client may have bailed
if(null !== _.get(this, 'client.term.output', null)) { if(null !== _.get(this, 'client.term.output', null)) {
@ -62,6 +75,7 @@ class TelnetClientConnection extends EventEmitter {
this.emit('connected'); this.emit('connected');
this.pipeRestored = false; this.pipeRestored = false;
this.client.dataPassthrough = true;
this.client.term.output.pipe(this.bridgeConnection); this.client.term.output.pipe(this.bridgeConnection);
}); });
@ -69,7 +83,7 @@ class TelnetClientConnection extends EventEmitter {
this.client.term.rawWrite(data); 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) // This is enough (in additional to other negotiations handled in telnet.js)
// to get us in on most systems // to get us in on most systems
// //
@ -110,25 +124,18 @@ class TelnetClientConnection extends EventEmitter {
// Create a TERMINAL-TYPE sub negotiation buffer using the // Create a TERMINAL-TYPE sub negotiation buffer using the
// actual/current terminal type. // actual/current terminal type.
// //
let bufs = buffers(); const sendTermType = TelnetSocket.commandBuffer(
Commands.SB,
bufs.push(Buffer.from( Options.TTYPE,
[ [
255, // IAC SubNegotiationCommands.IS,
250, // SB ...Buffer.from(this.client.term.termType), // e.g. "ansi"
24, // TERMINAL-TYPE Commands.IAC,
0, // IS Commands.SE,
] ]
));
bufs.push(
Buffer.from(this.client.term.termType), // e.g. "ansi"
Buffer.from( [ 255, 240 ] ) // IAC, SE
); );
return sendTermType;
return bufs.toBuffer();
} }
} }
exports.getModule = class TelnetBridgeModule extends MenuModule { exports.getModule = class TelnetBridgeModule extends MenuModule {

View File

@ -57,7 +57,8 @@
"uuid-parse": "1.1.0", "uuid-parse": "1.1.0",
"ws": "^7.3.0", "ws": "^7.3.0",
"xxhash": "^0.3.0", "xxhash": "^0.3.0",
"yazl": "^2.5.1" "yazl": "^2.5.1",
"telnet-socket" : "^0.2.3"
}, },
"devDependencies": {}, "devDependencies": {},
"engines": { "engines": {

View File

@ -160,7 +160,7 @@ bcrypt-pbkdf@^1.0.2:
dependencies: dependencies:
tweetnacl "^0.14.3" tweetnacl "^0.14.3"
binary-parser@^1.6.2: binary-parser@1.6.2, binary-parser@^1.6.2:
version "1.6.2" version "1.6.2"
resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.6.2.tgz#8410a82ffd9403271ec182bd91e63a09cee88cbe" resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.6.2.tgz#8410a82ffd9403271ec182bd91e63a09cee88cbe"
integrity sha512-cYAhKB51A9T/uylDvMK7uAYaPLWLwlferNOpnQ0E0fuO73yPi7kWaWiOm22BvuKxCbggmkiFN0VkuLg6gc+KQQ== integrity sha512-cYAhKB51A9T/uylDvMK7uAYaPLWLwlferNOpnQ0E0fuO73yPi7kWaWiOm22BvuKxCbggmkiFN0VkuLg6gc+KQQ==
@ -1840,6 +1840,14 @@ tar@^4:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
yallist "^3.0.2" yallist "^3.0.2"
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"
temptmp@^1.1.0: temptmp@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/temptmp/-/temptmp-1.1.0.tgz#bfbbff858d7f7d59c563fbf069758a7775ecd431" resolved "https://registry.yarnpkg.com/temptmp/-/temptmp-1.1.0.tgz#bfbbff858d7f7d59c563fbf069758a7775ecd431"