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!
|
||||
* [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`
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
@ -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,100 +25,91 @@ const ModuleInfo = exports.moduleInfo = {
|
|||
packageName : 'codes.l33t.enigma.websocket.server',
|
||||
};
|
||||
|
||||
function WebSocketClient(ws, req, serverType) {
|
||||
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;
|
||||
|
||||
Object.defineProperty(this, 'isSecure', {
|
||||
get : () => ('secure' === serverType || true === this.proxied) ? true : false,
|
||||
});
|
||||
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));
|
||||
}
|
||||
|
||||
const self = this;
|
||||
setClient(client, httpRequest) {
|
||||
this.client = client;
|
||||
|
||||
this.dataHandler = function(data) {
|
||||
if(self.pipedDest) {
|
||||
self.pipedDest.write(data);
|
||||
// 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() {
|
||||
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);
|
||||
|
||||
super(wsDuplex);
|
||||
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
|
||||
return this.emit('end');
|
||||
});
|
||||
|
||||
this.serverType = serverType;
|
||||
|
||||
//
|
||||
// Monitor connection status with ping/pong
|
||||
//
|
||||
ws.on('pong', () => {
|
||||
Log.trace(`Pong from ${wsDuplex.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, 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 {
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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": {
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue