UNPKG

guacamole-lite

Version:

Library to create servers compatible with the Guacamole protocol. Guacamole is a web client for RDP/VNC/SSH/Telnet

239 lines (179 loc) 7.12 kB
const Net = require('net'); const {LOGLEVEL} = require('./Logger.js'); const EventEmitter = require('events'); class GuacdClient extends EventEmitter { /** * * @param connectionType * @param guacdOptions * @param connectionSettings * @param logger */ constructor(guacdOptions, connectionType, connectionSettings, logger) { super(); this.STATE_OPENING = 0; this.STATE_OPEN = 1; this.STATE_CLOSED = 2; this.state = this.STATE_OPENING; this.logger = logger; this.connectionType = connectionType; this.connectionSettings = connectionSettings; this.handshakeReplySent = false; this.receivedBuffer = ''; this.lastActivity = Date.now(); this.sendBuffer = ''; this.guacdConnection = Net.connect(guacdOptions.port, guacdOptions.host); this.guacdConnection.on('connect', this.processConnectionOpen.bind(this)); this.guacdConnection.on('data', (data) => { this.processReceivedData(data.toString()); }); this.guacdConnection.on('close', (error) => { this.close(error); }); this.guacdConnection.on('error', (error) => { // console.error('********** GuacdClient[' + this.connectionType + '] error', error); this.emit('error', error); this.close(error); }); this.activityCheckInterval = setInterval(() => { if (Date.now() > (this.lastActivity + 10000)) { this.close(new Error('guacd was inactive for too long')) } }, 1000); } close(error) { if (this.state === this.STATE_CLOSED) { return; } if (error) { this.logger.log(LOGLEVEL.ERRORS, error); } this.logger.log(LOGLEVEL.VERBOSE, 'Closing guacd connection'); clearInterval(this.activityCheckInterval); this.guacdConnection.removeAllListeners('close'); if (!this.guacdConnection.closed) { this.guacdConnection.end(); } if (!this.guacdConnection.destroyed) { this.guacdConnection.destroy(); } this.state = this.STATE_CLOSED; this.emit('close', error); } send(data, afterOpened = false) { if (this.state === this.STATE_CLOSED) { return; } if (afterOpened && this.state === this.STATE_OPENING) { this.sendBuffer += data; return; } this.logger.log(LOGLEVEL.DEBUG, '<<<W2G< ' + data + '***'); this.guacdConnection.write(data, (error) => { if (error) { this.close(error); } }); } processConnectionOpen() { this.logger.log(LOGLEVEL.VERBOSE, 'guacd connection open'); this.logger.log(LOGLEVEL.VERBOSE, 'Selecting connection type: ' + this.connectionType); this.sendOpCode(['select', this.connectionType]); } sendHandshakeReply() { let protocolVersion = '1_0_0'; let serverHandshake = this.getFirstOpCodeFromBuffer(); this.logger.log(LOGLEVEL.VERBOSE, 'Server sent handshake: ' + serverHandshake); // "4.args,13.VERSION_1_1_0,8.hostname,4.port,8.password,13.swap-red-blue,9.read-only;" serverHandshake = serverHandshake.split(','); // remove the first element which is the opcode "args" serverHandshake.shift(); let connectArgs = []; serverHandshake.forEach((argName) => { // remove the length of the argument argName = argName.substring(argName.indexOf('.') + 1, argName.length); let arg; // if argName starts with "VERSION_" if (argName.indexOf('VERSION_') === 0) { let version = argName.substring(8, argName.length); // we only support 1.0.0 (default) and 1.1.0. // If guacd sends us a different version, we will use 1.1.0 // If it sends nothing, we will use 1.0.0 if (version === '1_0_0' || version === '1_1_0') { protocolVersion = version; } arg = 'VERSION_' + protocolVersion; } else { arg = this.connectionSettings[argName] || null } connectArgs.push(arg); }); this.sendOpCode([ 'size', this.connectionSettings.width, this.connectionSettings.height, this.connectionSettings.dpi, ]); this.sendOpCode(['audio'].concat(this.connectionSettings.audio || [])); this.sendOpCode(['video'].concat(this.connectionSettings.video || [])); this.sendOpCode(['image'].concat(this.connectionSettings.image || [])); if (protocolVersion === '1_1_0') { this.sendOpCode(['timezone'].concat(this.connectionSettings.timezone || [])); } this.sendOpCode(['connect'].concat(connectArgs)); this.handshakeReplySent = true; if (this.state !== this.STATE_OPEN) { this.state = this.STATE_OPEN; this.emit('open', this); if (this.sendBuffer) { this.send(this.sendBuffer); this.sendBuffer = ''; } } } getFirstOpCodeFromBuffer() { let delimiterPos = this.receivedBuffer.indexOf(';'); let opCode = this.receivedBuffer.substring(0, delimiterPos); this.receivedBuffer = this.receivedBuffer.substring(delimiterPos + 1, this.receivedBuffer.length); return opCode; } sendOpCode(opCode) { opCode = this.constructor.formatOpCode(opCode); this.logger.log(LOGLEVEL.VERBOSE, 'Sending opCode: ' + opCode); this.send(opCode); } static formatOpCode(opCodeParts) { opCodeParts.forEach((part, index, opCodeParts) => { part = this.stringifyOpCodePart(part); opCodeParts[index] = part.length + '.' + part; }); return opCodeParts.join(',') + ';'; } static stringifyOpCodePart(part) { if (part === null) { part = ''; } return String(part); } processReceivedData(data) { this.receivedBuffer += data; this.lastActivity = Date.now(); if (!this.handshakeReplySent) { if (this.receivedBuffer.indexOf(';') === -1) { return; // incomplete handshake received from guacd. Will wait for the next part } else { this.sendHandshakeReply(); } } this.sendBufferToWebSocket(); } sendBufferToWebSocket() { const delimiterPos = this.receivedBuffer.lastIndexOf(';'); const bufferPartToSend = this.receivedBuffer.substring(0, delimiterPos + 1); if (bufferPartToSend) { this.receivedBuffer = this.receivedBuffer.substring(delimiterPos + 1, this.receivedBuffer.length); this.emit('data', bufferPartToSend); } } } module.exports = GuacdClient;