2017-05-26 14:25:41 +00:00
|
|
|
/* 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');
|
2017-08-06 16:20:55 +00:00
|
|
|
const Writable = require('stream');
|
2017-05-26 14:25:41 +00:00
|
|
|
|
|
|
|
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', {
|
2017-06-02 00:48:14 +00:00
|
|
|
get : () => ('secure' === serverType || true === this.proxied) ? true : false,
|
2017-05-26 14:25:41 +00:00
|
|
|
});
|
|
|
|
|
2017-06-02 00:56:05 +00:00
|
|
|
const self = this;
|
|
|
|
|
2018-05-12 15:33:41 +00:00
|
|
|
this.dataHandler = function(data) {
|
|
|
|
self.socketBridge.emit('data', data);
|
|
|
|
};
|
|
|
|
|
2017-05-31 03:31:35 +00:00
|
|
|
//
|
|
|
|
// This bridge makes accessible various calls that client sub classes
|
|
|
|
// want to access on I/O socket
|
|
|
|
//
|
2017-08-06 16:20:55 +00:00
|
|
|
this.socketBridge = new class SocketBridge extends Writable {
|
2017-05-26 14:25:41 +00:00
|
|
|
constructor(ws) {
|
|
|
|
super();
|
|
|
|
this.ws = ws;
|
|
|
|
}
|
|
|
|
|
|
|
|
end() {
|
2017-08-15 03:22:03 +00:00
|
|
|
return ws.close();
|
2017-05-26 14:25:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
write(data, cb) {
|
2017-08-15 03:22:03 +00:00
|
|
|
cb = cb || ( () => { /* eat it up */} ); // handle data writes after close
|
|
|
|
|
2017-05-26 14:25:41 +00:00
|
|
|
return this.ws.send(data, { binary : true }, cb);
|
|
|
|
}
|
|
|
|
|
2017-08-06 16:20:55 +00:00
|
|
|
// we need to fake some streaming work
|
|
|
|
unpipe() {
|
|
|
|
Log.trace('WebSocket SocketBridge unpipe()');
|
|
|
|
}
|
|
|
|
|
|
|
|
resume() {
|
|
|
|
Log.trace('WebSocket SocketBridge resume()');
|
|
|
|
}
|
|
|
|
|
2017-05-26 14:25:41 +00:00
|
|
|
get remoteAddress() {
|
2017-06-02 00:48:14 +00:00
|
|
|
// Support X-Forwarded-For and X-Real-IP headers for proxied connections
|
2017-06-02 00:56:05 +00:00
|
|
|
return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress;
|
2017-05-26 14:25:41 +00:00
|
|
|
}
|
|
|
|
}(ws);
|
|
|
|
|
2018-05-12 15:33:41 +00:00
|
|
|
ws.on('message', this.dataHandler);
|
2017-05-26 14:25:41 +00:00
|
|
|
|
|
|
|
ws.on('close', () => {
|
2017-06-07 02:04:28 +00:00
|
|
|
// we'll remove client connection which will in turn end() via our SocketBridge above
|
|
|
|
return this.emit('end');
|
2017-05-26 14:25:41 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
//
|
|
|
|
// Montior connection status with ping/pong
|
|
|
|
//
|
2018-03-15 02:26:40 +00:00
|
|
|
ws.on('pong', () => {
|
2017-05-26 14:25:41 +00:00
|
|
|
Log.trace(`Pong from ${this.socketBridge.remoteAddress}`);
|
|
|
|
ws.isConnectionAlive = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
TelnetClient.call(this, this.socketBridge, this.socketBridge);
|
|
|
|
|
2017-06-01 04:13:44 +00:00
|
|
|
Log.trace( { headers : req.headers }, 'WebSocket connection headers' );
|
|
|
|
|
|
|
|
//
|
|
|
|
// If the config allows it, look for 'x-forwarded-proto' as "https"
|
|
|
|
// to override |isSecure|
|
|
|
|
//
|
2017-06-02 00:48:14 +00:00
|
|
|
if(true === _.get(Config, 'loginServers.webSocket.proxied') &&
|
2017-06-01 04:13:44 +00:00
|
|
|
'https' === req.headers['x-forwarded-proto'])
|
|
|
|
{
|
2017-06-02 00:48:14 +00:00
|
|
|
Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`);
|
|
|
|
this.proxied = true;
|
2017-06-02 00:56:05 +00:00
|
|
|
} else {
|
|
|
|
this.proxied = false;
|
2017-06-01 04:13:44 +00:00
|
|
|
}
|
|
|
|
|
2017-05-26 14:25:41 +00:00
|
|
|
// 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://)
|
|
|
|
//
|
2018-04-24 01:03:35 +00:00
|
|
|
const config = _.get(Config, 'loginServers.webSocket');
|
|
|
|
if(!_.isObject(config)) {
|
2017-05-31 03:31:35 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-05-26 14:25:41 +00:00
|
|
|
|
2018-04-24 01:03:35 +00:00
|
|
|
const wsPort = _.get(config, 'ws.port');
|
|
|
|
const wssPort = _.get(config, 'wss.port');
|
|
|
|
|
|
|
|
if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) {
|
2017-05-26 14:25:41 +00:00
|
|
|
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 } ),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-04-24 01:03:35 +00:00
|
|
|
if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) {
|
2017-05-26 14:25:41 +00:00
|
|
|
const httpServer = https.createServer({
|
2018-04-24 01:03:35 +00:00
|
|
|
key : fs.readFileSync(config.wss.keyPem),
|
|
|
|
cert : fs.readFileSync(config.wss.certPem),
|
2017-05-26 14:25:41 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.secure = {
|
|
|
|
httpServer : httpServer,
|
|
|
|
wsServer : new WebSocketServer( { server : httpServer } ),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
listen() {
|
|
|
|
WSS_SERVER_TYPES.forEach(serverType => {
|
|
|
|
const server = this[serverType];
|
|
|
|
if(!server) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const serverName = `${ModuleInfo.name} (${serverType})`;
|
2018-04-24 01:03:35 +00:00
|
|
|
const port = parseInt(_.get(Config, [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] ));
|
2017-05-26 14:25:41 +00:00
|
|
|
|
|
|
|
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
|
2018-03-15 02:26:40 +00:00
|
|
|
|
2017-05-26 14:25:41 +00:00
|
|
|
Log.trace('Ping to remote WebSocket client');
|
2018-03-15 02:26:40 +00:00
|
|
|
return ws.ping('', false); // false=don't mask
|
2017-05-26 14:25:41 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2018-03-15 02:26:40 +00:00
|
|
|
}, 30000);
|
2017-05-26 14:25:41 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
webSocketConnection(conn) {
|
|
|
|
const webSocketClient = new WebSocketClient(conn);
|
|
|
|
this.handleNewClient(webSocketClient, webSocketClient.socketShim, ModuleInfo);
|
|
|
|
}
|
|
|
|
};
|