From 2e18833014ac3e8562829882e850b137b2f28db6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 May 2017 08:25:41 -0600 Subject: [PATCH] Working WebSocket connections - not yet complete, but working well --- core/login_server_module.js | 2 +- core/servers/login/telnet.js | 50 +-------- core/servers/login/websocket.js | 173 ++++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 core/servers/login/websocket.js diff --git a/core/login_server_module.js b/core/login_server_module.js index 4f003982..212d2e27 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -43,7 +43,7 @@ module.exports = class LoginServerModule extends ServerModule { } client.session.serverName = modInfo.name; - client.session.isSecure = modInfo.isSecure || false; + client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); clientConns.addNewClient(client, clientSock); diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index e340d512..5c471473 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -24,6 +24,8 @@ const ModuleInfo = exports.moduleInfo = { packageName : 'codes.l33t.enigma.telnet.server', }; +exports.TelnetClient = TelnetClient; + // // Telnet Protocol Resources // * http://pcmicro.com/netfoss/telnet.html @@ -498,54 +500,6 @@ function TelnetClient(input, output) { this.input.on('data', this.dataHandler); - /* - this.input.on('data', b => { - bufs.push(b); - - let i; - while((i = bufs.indexOf(IAC_BUF)) >= 0) { - - // - // Some clients will send even IAC separate from data - // - if(bufs.length <= (i + 1)) { - i = MORE_DATA_REQUIRED; - break; - } - - assert(bufs.length > (i + 1)); - - if(i > 0) { - self.emit('data', bufs.splice(0, i).toBuffer()); - } - - i = parseBufs(bufs); - - if(MORE_DATA_REQUIRED === i) { - break; - } else { - if(i.option) { - self.emit(i.option, i); // "transmit binary", "echo", ... - } - - self.handleTelnetEvent(i); - - if(i.data) { - self.emit('data', i.data); - } - } - } - - if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { - // - // Standard data payload. This can still be "non-user" data - // such as ANSI control, but we don't handle that here. - // - self.emit('data', bufs.splice(0).toBuffer()); - } - }); - */ - this.input.on('end', () => { self.emit('end'); }); diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js new file mode 100644 index 00000000..4b73bcd3 --- /dev/null +++ b/core/servers/login/websocket.js @@ -0,0 +1,173 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Config = require('../../config.js').config; +const TelnetClient = require('./telnet.js').TelnetClient; +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); + +// deps +const _ = require('lodash'); +const WebSocketServer = require('ws').Server; +const http = require('http'); +const https = require('https'); +const fs = require('graceful-fs'); +const EventEmitter = require('events'); + +const ModuleInfo = exports.moduleInfo = { + name : 'WebSocket', + desc : 'WebSocket Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.websocket.server', +}; + +function WebSocketClient(ws, req, serverType) { + + Object.defineProperty(this, 'isSecure', { + get : () => 'secure' === serverType ? true : false, + }); + + this.socketBridge = new class SocketBridge extends EventEmitter { + constructor(ws) { + super(); + this.ws = ws; + } + + end() { + return ws.terminate(); + } + + write(data, cb) { + return this.ws.send(data, { binary : true }, cb); + } + + get remoteAddress() { + return req.connection.remoteAddress; + } + }(ws); + + ws.on('message', data => { + this.socketBridge.emit('data', data); + }); + + ws.on('close', () => { + this.end(); + }); + + // + // Montior 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); + + // start handshake process + this.banner(); +} + +require('util').inherits(WebSocketClient, TelnetClient); + +const WSS_SERVER_TYPES = [ 'insecure', 'secure' ]; + +exports.getModule = class WebSocketLoginServer extends LoginServerModule { + constructor() { + super(); + } + + createServer() { + // + // We will actually create up to two servers: + // * insecure websocket (ws://) + // * secure (tls) websocket (wss://) + // + const insecureConf = _.get(Config, 'loginServers.webSocket') || { enabled : false }; + const secureConf = _.get(Config, 'loginServers.secureWebSocket') || { enabled : false }; + + if(insecureConf.enabled) { + const httpServer = http.createServer( (req, resp) => { + // dummy handler + resp.writeHead(200); + return resp.end('ENiGMA½ BBS WebSocket Server!'); + }); + + this.insecure = { + httpServer : httpServer, + wsServer : new WebSocketServer( { server : httpServer } ), + }; + } + + if(secureConf.enabled) { + const httpServer = https.createServer({ + key : fs.readFileSync(Config.loginServers.secureWebSocket.keyPem), + cert : fs.readFileSync(Config.loginServers.secureWebSocket.certPem), + }); + + this.secure = { + httpServer : httpServer, + wsServer : new WebSocketServer( { server : httpServer } ), + }; + } + } + + configName(serverType) { + return 'secure' === serverType ? 'secureWebSocket' : 'webSocket'; + } + + listen() { + WSS_SERVER_TYPES.forEach(serverType => { + const server = this[serverType]; + if(!server) { + return; + } + + const serverName = `${ModuleInfo.name} (${serverType})`; + const port = parseInt( _.get( Config, [ 'loginServers', this.configName(serverType), 'port' ] ) ); + + if(isNaN(port)) { + Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); + return; + } + + server.httpServer.listen(port); + + server.wsServer.on('connection', (ws, req) => { + const webSocketClient = new WebSocketClient(ws, req, serverType); + this.handleNewClient(webSocketClient, webSocketClient.socketBridge, ModuleInfo); + }); + + Log.info( { server : serverName, port : port }, 'Listening for connections' ); + }); + + // + // Send pings every 30s + // + setInterval( () => { + WSS_SERVER_TYPES.forEach(serverType => { + if(this[serverType]) { + this[serverType].wsServer.clients.forEach(ws => { + if(false === ws.isConnectionAlive) { + Log.debug('WebSocket connection seems inactive. Terminating.'); + return ws.terminate(); + } + + ws.isConnectionAlive = false; // pong will reset this + + Log.trace('Ping to remote WebSocket client'); + return ws.ping('', false, true); + }); + } + }); + }, 30000); + + return true; + } + + webSocketConnection(conn) { + const webSocketClient = new WebSocketClient(conn); + this.handleNewClient(webSocketClient, webSocketClient.socketShim, ModuleInfo); + } +}; diff --git a/package.json b/package.json index 2a432e33..c983edd5 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "temptmp": "^1.0.0", "uuid": "^3.0.1", "uuid-parse": "^1.0.0", - "ws" : "^2.3.1", + "ws" : "^3.0.0", "graceful-fs" : "^4.1.11", "exiftool" : "^0.0.3" },