cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

GuacdClient.js (5693B)


      1const Net = require('net');
      2
      3class GuacdClient {
      4
      5    /**
      6     *
      7     * @param {Server} server
      8     * @param {ClientConnection} clientConnection
      9     */
     10    constructor(server, clientConnection) {
     11        this.STATE_OPENING = 0;
     12        this.STATE_OPEN = 1;
     13        this.STATE_CLOSED = 2;
     14
     15        this.state = this.STATE_OPENING;
     16
     17        this.server = server;
     18        this.clientConnection = clientConnection;
     19        this.handshakeReplySent = false;
     20        this.receivedBuffer = '';
     21        this.lastActivity = Date.now();
     22
     23        this.guacdConnection = Net.connect(server.guacdOptions.port, server.guacdOptions.host);
     24
     25        this.guacdConnection.on('connect', this.processConnectionOpen.bind(this));
     26        this.guacdConnection.on('data', this.processReceivedData.bind(this));
     27        this.guacdConnection.on('close', this.clientConnection.close.bind(this.clientConnection));
     28        this.guacdConnection.on('error', this.clientConnection.error.bind(this.clientConnection));
     29
     30        this.activityCheckInterval = setInterval(this.checkActivity.bind(this), 1000);
     31    }
     32
     33    checkActivity() {
     34        if (Date.now() > (this.lastActivity + 10000)) {
     35            this.clientConnection.close(new Error('guacd was inactive for too long'));
     36        }
     37    }
     38
     39    close(error) {
     40        if (this.state == this.STATE_CLOSED) {
     41            return;
     42        }
     43
     44        if (error) {
     45            this.clientConnection.log(this.server.LOGLEVEL.ERRORS, error);
     46        }
     47
     48        this.log(this.server.LOGLEVEL.VERBOSE, 'Closing guacd connection');
     49        clearInterval(this.activityCheckInterval);
     50
     51        this.guacdConnection.removeAllListeners('close');
     52        this.guacdConnection.end();
     53        this.guacdConnection.destroy();
     54
     55        this.state = this.STATE_CLOSED;
     56        this.server.emit('close', this.clientConnection);
     57    }
     58
     59    send(data) {
     60        if (this.state == this.STATE_CLOSED) {
     61            return;
     62        }
     63
     64        this.log(this.server.LOGLEVEL.DEBUG, '<<<W2G< ' + data + '***');
     65        this.guacdConnection.write(data);
     66    }
     67
     68    log(level, ...args) {
     69        this.clientConnection.log(level, ...args);
     70    }
     71
     72    processConnectionOpen() {
     73        this.log(this.server.LOGLEVEL.VERBOSE, 'guacd connection open');
     74
     75        this.log(this.server.LOGLEVEL.VERBOSE, 'Selecting connection type: ' + this.clientConnection.connectionType);
     76        this.sendOpCode(['select', this.clientConnection.connectionType]);
     77    }
     78
     79    sendHandshakeReply() {
     80        this.sendOpCode([
     81            'size',
     82            this.clientConnection.connectionSettings.connection.width,
     83            this.clientConnection.connectionSettings.connection.height,
     84            this.clientConnection.connectionSettings.connection.dpi
     85        ]);
     86        this.sendOpCode(['audio'].concat(this.clientConnection.query.GUAC_AUDIO || []));
     87        this.sendOpCode(['video'].concat(this.clientConnection.query.GUAC_VIDEO || []));
     88        this.sendOpCode(['image']);
     89
     90        let serverHandshake = this.getFirstOpCodeFromBuffer();
     91
     92        this.log(this.server.LOGLEVEL.VERBOSE, 'Server sent handshake: ' + serverHandshake);
     93
     94        serverHandshake = serverHandshake.split(',');
     95        let connectionOptions = [];
     96
     97        serverHandshake.forEach((attribute) => {
     98            connectionOptions.push(this.getConnectionOption(attribute));
     99        });
    100
    101        this.sendOpCode(connectionOptions);
    102
    103        this.handshakeReplySent = true;
    104
    105        if (this.state != this.STATE_OPEN) {
    106            this.state = this.STATE_OPEN;
    107            this.server.emit('open', this.clientConnection);
    108        }
    109    }
    110
    111    getConnectionOption(optionName) {
    112        return this.clientConnection.connectionSettings.connection[this.constructor.parseOpCodeAttribute(optionName)] || null
    113    }
    114
    115    getFirstOpCodeFromBuffer() {
    116        let delimiterPos = this.receivedBuffer.indexOf(';');
    117        let opCode = this.receivedBuffer.substring(0, delimiterPos);
    118
    119        this.receivedBuffer = this.receivedBuffer.substring(delimiterPos + 1, this.receivedBuffer.length);
    120
    121        return opCode;
    122    }
    123
    124    sendOpCode(opCode) {
    125        opCode = this.constructor.formatOpCode(opCode);
    126        this.log(this.server.LOGLEVEL.VERBOSE, 'Sending opCode: ' + opCode);
    127        this.send(opCode);
    128    }
    129
    130    static formatOpCode(opCodeParts) {
    131        opCodeParts.forEach((part, index, opCodeParts) => {
    132            part = this.stringifyOpCodePart(part);
    133            opCodeParts[index] = part.length + '.' + part;
    134        });
    135
    136        return opCodeParts.join(',') + ';';
    137    }
    138
    139    static stringifyOpCodePart(part) {
    140        if (part === null) {
    141            part = '';
    142        }
    143
    144        return String(part);
    145    }
    146
    147    static parseOpCodeAttribute(opCodeAttribute) {
    148        return opCodeAttribute.substring(opCodeAttribute.indexOf('.') + 1, opCodeAttribute.length);
    149    }
    150
    151    processReceivedData(data) {
    152        this.receivedBuffer += data;
    153        this.lastActivity = Date.now();
    154
    155        if (!this.handshakeReplySent) {
    156            if (this.receivedBuffer.indexOf(';') === -1) {
    157                return; // incomplete handshake received from guacd. Will wait for the next part
    158            } else {
    159                this.sendHandshakeReply();
    160            }
    161        }
    162
    163        this.sendBufferToWebSocket();
    164    }
    165
    166    sendBufferToWebSocket() {
    167        const delimiterPos = this.receivedBuffer.lastIndexOf(';');
    168        const bufferPartToSend = this.receivedBuffer.substring(0, delimiterPos + 1);
    169
    170        if (bufferPartToSend) {
    171            this.receivedBuffer = this.receivedBuffer.substring(delimiterPos + 1, this.receivedBuffer.length);
    172            this.clientConnection.send(bufferPartToSend);
    173        }
    174    }
    175
    176
    177}
    178
    179module.exports = GuacdClient;