/* jslint node: true */ 'use strict'; const { MenuModule } = require('./menu_module.js'); const { resetScreen } = require('./ansi_term.js'); const { Errors } = require('./enig_error.js'); const Events = require('./events.js'); const StatLog = require('./stat_log.js'); const UserProps = require('./user_property.js'); // deps const async = require('async'); const http = require('http'); const net = require('net'); const crypto = require('crypto'); const moment = require('moment'); const packageJson = require('../package.json'); /* Expected configuration block: { module: bbs_link ... config: { sysCode: XXXXX authCode: XXXXX schemeCode: XXXX door: lord // default hoss: games.bbslink.net host: games.bbslink.net // defualt port: 23 port: 23 } } */ // :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors // :TODO: ENH: Support nodeMax and tooManyArt exports.moduleInfo = { name : 'BBSLink', desc : 'BBSLink Access Module', author : 'NuSkooler', }; exports.getModule = class BBSLinkModule extends MenuModule { constructor(options) { super(options); this.config = options.menuConfig.config; this.config.host = this.config.host || 'games.bbslink.net'; this.config.port = this.config.port || 23; } initSequence() { let token; let randomKey; let clientTerminated; const self = this; async.series( [ function validateConfig(callback) { return self.validateConfigFields( { host : 'string', sysCode : 'string', authCode : 'string', schemeCode : 'string', door : 'string', port : 'number', }, callback ); }, function acquireToken(callback) { // // Acquire an authentication token // crypto.randomBytes(16, function rand(ex, buf) { if(ex) { callback(ex); } else { randomKey = buf.toString('base64').substr(0, 6); self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) { if(err) { callback(err); } else { token = body.trim(); self.client.log.trace( { token : token }, 'BBSLink token'); callback(null); } }); } }); }, function authenticateToken(callback) { // // Authenticate the token we acquired previously // const headers = { 'X-User' : self.client.user.userId.toString(), 'X-System' : self.config.sysCode, 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), 'X-Rows' : self.client.term.termHeight.toString(), 'X-Key' : randomKey, 'X-Door' : self.config.door, 'X-Token' : token, 'X-Type' : 'enigma-bbs', 'X-Version' : packageJson.version, }; self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { var status = body.trim(); if('complete' === status) { return callback(null); } return callback(Errors.AccessDenied(`Bad authentication status: ${status}`)); }); }, function createTelnetBridge(callback) { // // Authentication with BBSLink successful. Now, we need to create a telnet // bridge from us to them // const connectOpts = { port : self.config.port, host : self.config.host, }; let clientTerminated; self.client.term.write(resetScreen()); self.client.term.write(' Connecting to BBSLink.net, please wait...\n'); const startTime = moment(); const bridgeConnection = net.createConnection(connectOpts, function connected() { // bump stats, fire events, etc. StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); self.client.log.info(connectOpts, 'BBSLink bridge connection established'); self.client.term.output.pipe(bridgeConnection); self.client.once('end', function clientEnd() { self.client.log.info('Connection ended. Terminating BBSLink connection'); clientTerminated = true; bridgeConnection.end(); }); }); const restorePipe = function() { self.client.term.output.unpipe(bridgeConnection); self.client.term.output.resume(); const endTime = moment(); const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); if(runTimeMinutes > 0) { StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); } }; bridgeConnection.on('data', function incomingData(data) { // pass along // :TODO: just pipe this as well self.client.term.rawWrite(data); }); bridgeConnection.on('end', function connectionEnd() { restorePipe(); return callback(clientTerminated ? Errors.General('Client connection terminated') : null); }); bridgeConnection.on('error', function error(err) { self.client.log.info('BBSLink bridge connection error: ' + err.message); restorePipe(); callback(err); }); } ], function complete(err) { if(err) { self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); } if(!clientTerminated) { self.prevMenu(); } } ); } simpleHttpRequest(path, headers, cb) { const getOpts = { host : this.config.host, path : path, headers : headers, }; const req = http.get(getOpts, function response(resp) { let data = ''; resp.on('data', function chunk(c) { data += c; }); resp.on('end', function respEnd() { cb(null, data); req.end(); }); }); req.on('error', function reqErr(err) { cb(err); }); } };