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:
commit
6258a39c57
|
@ -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`
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue