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!
* [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`

View File

@ -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);
};

View File

@ -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)
}
}
},

View File

@ -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));

View File

@ -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'
@ -389,9 +399,28 @@ exports.getModule = class TransferFileModule extends MenuModule {
updateActivity();
// needed for things like sz/rz
if(external.escapeTelnet) {
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
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 {
externalProc.write(data);
}
@ -401,9 +430,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);
}

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 fs = require('graceful-fs');
const Writable = require('stream');
const { Duplex } = require('stream');
const forEachSeries = require('async/forEachSeries');
const ModuleInfo = exports.moduleInfo = {
@ -24,79 +25,67 @@ const ModuleInfo = exports.moduleInfo = {
packageName : 'codes.l33t.enigma.websocket.server',
};
function WebSocketClient(ws, req, serverType) {
Object.defineProperty(this, 'isSecure', {
get : () => ('secure' === serverType || true === this.proxied) ? true : false,
});
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 {
class WebSocketClient extends TelnetClient {
constructor(ws, req, serverType) {
// 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('error', err => this.emit('error', err));
this.ws.on('message', data => this._data(data));
}
end() {
return ws.close();
}
setClient(client, httpRequest) {
this.client = client;
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()');
// 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 (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress;
return this.resolvedRemoteAddress;
}
_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.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
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;
});
TelnetClient.call(this, this.socketBridge, this.socketBridge);
Log.trace( { headers : req.headers }, 'WebSocket connection headers' );
//
@ -114,9 +103,12 @@ function WebSocketClient(ws, req, serverType) {
// start handshake process
this.banner();
}
}
require('util').inherits(WebSocketClient, TelnetClient);
get isSecure() {
return ('secure' === this.serverType || true === this.proxied) ? true : false;
}
}
const WSS_SERVER_TYPES = [ 'insecure', 'secure' ];
@ -216,7 +208,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' );
@ -227,9 +219,4 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule {
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 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 {

View File

@ -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" : "^0.2.3"
},
"devDependencies": {},
"engines": {

View File

@ -160,7 +160,7 @@ bcrypt-pbkdf@^1.0.2:
dependencies:
tweetnacl "^0.14.3"
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,6 +1840,14 @@ tar@^4:
safe-buffer "^5.1.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:
version "1.1.0"
resolved "https://registry.yarnpkg.com/temptmp/-/temptmp-1.1.0.tgz#bfbbff858d7f7d59c563fbf069758a7775ecd431"