cscg24-guacamole

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

vnc.c (17724B)


      1/*
      2 * Licensed to the Apache Software Foundation (ASF) under one
      3 * or more contributor license agreements.  See the NOTICE file
      4 * distributed with this work for additional information
      5 * regarding copyright ownership.  The ASF licenses this file
      6 * to you under the Apache License, Version 2.0 (the
      7 * "License"); you may not use this file except in compliance
      8 * with the License.  You may obtain a copy of the License at
      9 *
     10 *   http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing,
     13 * software distributed under the License is distributed on an
     14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     15 * KIND, either express or implied.  See the License for the
     16 * specific language governing permissions and limitations
     17 * under the License.
     18 */
     19
     20#include "config.h"
     21
     22#include "auth.h"
     23#include "client.h"
     24#include "clipboard.h"
     25#include "common/clipboard.h"
     26#include "common/cursor.h"
     27#include "common/display.h"
     28#include "cursor.h"
     29#include "display.h"
     30#include "log.h"
     31#include "settings.h"
     32#include "vnc.h"
     33
     34#ifdef ENABLE_PULSE
     35#include "pulse/pulse.h"
     36#endif
     37
     38#ifdef ENABLE_COMMON_SSH
     39#include "common-ssh/sftp.h"
     40#include "common-ssh/ssh.h"
     41#include "sftp.h"
     42#endif
     43
     44#include <guacamole/client.h>
     45#include <guacamole/protocol.h>
     46#include <guacamole/recording.h>
     47#include <guacamole/socket.h>
     48#include <guacamole/timestamp.h>
     49#include <guacamole/wol.h>
     50#include <rfb/rfbclient.h>
     51#include <rfb/rfbconfig.h>
     52#include <rfb/rfbproto.h>
     53
     54#ifdef LIBVNCSERVER_WITH_CLIENT_GCRYPT
     55#include <errno.h>
     56#include <gcrypt.h>
     57#endif
     58
     59#include <stdlib.h>
     60#include <string.h>
     61#include <time.h>
     62
     63#ifdef LIBVNCSERVER_WITH_CLIENT_GCRYPT
     64GCRY_THREAD_OPTION_PTHREAD_IMPL;
     65#endif
     66
     67char* GUAC_VNC_CLIENT_KEY = "GUAC_VNC";
     68
     69#ifdef ENABLE_VNC_TLS_LOCKING
     70/**
     71 * A callback function that is called by the VNC library prior to writing
     72 * data to a TLS-encrypted socket.  This returns the rfbBool FALSE value
     73 * if there's an error locking the mutex, or rfbBool TRUE otherwise.
     74 * 
     75 * @param rfb_client
     76 *     The rfbClient for which to lock the TLS mutex.
     77 *
     78 * @returns
     79 *     rfbBool FALSE if an error occurs locking the mutex, otherwise
     80 *     TRUE.
     81 */
     82static rfbBool guac_vnc_lock_write_to_tls(rfbClient* rfb_client) {
     83
     84    /* Retrieve the Guacamole data structures */
     85    guac_client* gc = rfbClientGetClientData(rfb_client, GUAC_VNC_CLIENT_KEY);
     86    guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data;
     87
     88    /* Lock write access */
     89    int retval = pthread_mutex_lock(&(vnc_client->tls_lock));
     90    if (retval) {
     91        guac_client_log(gc, GUAC_LOG_ERROR, "Error locking TLS write mutex: %s",
     92                strerror(retval));
     93        return FALSE;
     94    }
     95    return TRUE;
     96
     97}
     98
     99/**
    100 * A callback function for use by the VNC library that is called once
    101 * the client is finished writing to a TLS-encrypted socket. A rfbBool
    102 * FALSE value is returned if an error occurs unlocking the mutex,
    103 * otherwise TRUE is returned.
    104 *
    105 * @param rfb_client
    106 *     The rfbClient for which to unlock the TLS mutex.
    107 *
    108 * @returns
    109 *     rfbBool FALSE if an error occurs unlocking the mutex, otherwise
    110 *     TRUE.
    111 */
    112static rfbBool guac_vnc_unlock_write_to_tls(rfbClient* rfb_client) {
    113
    114    /* Retrieve the Guacamole data structures */
    115    guac_client* gc = rfbClientGetClientData(rfb_client, GUAC_VNC_CLIENT_KEY);
    116    guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data;
    117
    118    /* Unlock write access */
    119    int retval = pthread_mutex_unlock(&(vnc_client->tls_lock));
    120    if (retval) {
    121        guac_client_log(gc, GUAC_LOG_ERROR, "Error unlocking TLS write mutex: %s",
    122                strerror(retval));
    123        return FALSE;
    124    }
    125    return TRUE;
    126}
    127#endif
    128
    129rfbClient* guac_vnc_get_client(guac_client* client) {
    130
    131    rfbClient* rfb_client = rfbGetClient(8, 3, 4); /* 32-bpp client */
    132    guac_vnc_client* vnc_client = (guac_vnc_client*) client->data;
    133    guac_vnc_settings* vnc_settings = vnc_client->settings;
    134
    135    /* Store Guac client in rfb client */
    136    rfbClientSetClientData(rfb_client, GUAC_VNC_CLIENT_KEY, client);
    137
    138    /* Framebuffer update handler */
    139    rfb_client->GotFrameBufferUpdate = guac_vnc_update;
    140    rfb_client->GotCopyRect = guac_vnc_copyrect;
    141
    142#ifdef ENABLE_VNC_TLS_LOCKING
    143    /* TLS Locking and Unlocking */
    144    rfb_client->LockWriteToTLS = guac_vnc_lock_write_to_tls;
    145    rfb_client->UnlockWriteToTLS = guac_vnc_unlock_write_to_tls;
    146#endif
    147    
    148#ifdef LIBVNCSERVER_WITH_CLIENT_GCRYPT
    149    
    150    /* Check if GCrypt is initialized, do it if not. */
    151    if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
    152    
    153        guac_client_log(client, GUAC_LOG_DEBUG, "GCrypt initialization started.");
    154
    155        /* Initialize thread control. */
    156        gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread);
    157
    158        /* Basic GCrypt library initialization. */
    159        gcry_check_version(NULL);
    160
    161        /* Mark initialization as completed. */
    162        gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
    163        guac_client_log(client, GUAC_LOG_DEBUG, "GCrypt initialization completed.");
    164    
    165    }
    166    
    167#endif
    168
    169    /* Do not handle clipboard and local cursor if read-only */
    170    if (vnc_settings->read_only == 0) {
    171
    172        /* Clipboard */
    173        rfb_client->GotXCutText = guac_vnc_cut_text;
    174
    175        /* Set remote cursor */
    176        if (vnc_settings->remote_cursor) {
    177            rfb_client->appData.useRemoteCursor = FALSE;
    178        }
    179
    180        else {
    181            /* Enable client-side cursor */
    182            rfb_client->appData.useRemoteCursor = TRUE;
    183            rfb_client->GotCursorShape = guac_vnc_cursor;
    184        }
    185
    186    }
    187
    188#ifdef ENABLE_VNC_GENERIC_CREDENTIALS
    189    /* Authentication */
    190    rfb_client->GetCredential = guac_vnc_get_credentials;
    191#endif
    192    
    193    /* Password */
    194    rfb_client->GetPassword = guac_vnc_get_password;
    195
    196    /* Depth */
    197    guac_vnc_set_pixel_format(rfb_client, vnc_settings->color_depth);
    198
    199    /* Hook into allocation so we can handle resize. */
    200    vnc_client->rfb_MallocFrameBuffer = rfb_client->MallocFrameBuffer;
    201    rfb_client->MallocFrameBuffer = guac_vnc_malloc_framebuffer;
    202    rfb_client->canHandleNewFBSize = 1;
    203
    204    /* Set hostname and port */
    205    rfb_client->serverHost = strdup(vnc_settings->hostname);
    206    rfb_client->serverPort = vnc_settings->port;
    207
    208#ifdef ENABLE_VNC_REPEATER
    209    /* Set repeater parameters if specified */
    210    if (vnc_settings->dest_host) {
    211        rfb_client->destHost = strdup(vnc_settings->dest_host);
    212        rfb_client->destPort = vnc_settings->dest_port;
    213    }
    214#endif
    215
    216#ifdef ENABLE_VNC_LISTEN
    217    /* If reverse connection enabled, start listening */
    218    if (vnc_settings->reverse_connect) {
    219
    220        guac_client_log(client, GUAC_LOG_INFO, "Listening for connections on port %i", vnc_settings->port);
    221
    222        /* Listen for connection from server */
    223        rfb_client->listenPort = vnc_settings->port;
    224        if (listenForIncomingConnectionsNoFork(rfb_client, vnc_settings->listen_timeout*1000) <= 0)
    225            return NULL;
    226
    227    }
    228#endif
    229
    230    /* Set encodings if provided */
    231    if (vnc_settings->encodings)
    232        rfb_client->appData.encodingsString = strdup(vnc_settings->encodings);
    233
    234    /* Connect */
    235    if (rfbInitClient(rfb_client, NULL, NULL))
    236        return rfb_client;
    237
    238    /* If connection fails, return NULL */
    239    return NULL;
    240
    241}
    242
    243/**
    244 * Waits until data is available to be read from the given rfbClient, and thus
    245 * a call to HandleRFBServerMessages() should not block. If the timeout elapses
    246 * before data is available, zero is returned.
    247 *
    248 * @param rfb_client
    249 *     The rfbClient to wait for.
    250 *
    251 * @param timeout
    252 *     The maximum amount of time to wait, in microseconds.
    253 *
    254 * @returns
    255 *     A positive value if data is available, zero if the timeout elapses
    256 *     before data becomes available, or a negative value on error.
    257 */
    258static int guac_vnc_wait_for_messages(rfbClient* rfb_client, int timeout) {
    259
    260    /* Do not explicitly wait while data is on the buffer */
    261    if (rfb_client->buffered)
    262        return 1;
    263
    264    /* If no data on buffer, wait for data on socket */
    265    return WaitForMessage(rfb_client, timeout);
    266
    267}
    268
    269void* guac_vnc_client_thread(void* data) {
    270
    271    guac_client* client = (guac_client*) data;
    272    guac_vnc_client* vnc_client = (guac_vnc_client*) client->data;
    273    guac_vnc_settings* settings = vnc_client->settings;
    274
    275    /* If Wake-on-LAN is enabled, attempt to wake. */
    276    if (settings->wol_send_packet) {
    277        guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, "
    278                "and pausing for %d seconds.", settings->wol_wait_time);
    279        
    280        /* Send the Wake-on-LAN request. */
    281        if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr,
    282                settings->wol_udp_port))
    283            return NULL;
    284        
    285        /* If wait time is specified, sleep for that amount of time. */
    286        if (settings->wol_wait_time > 0)
    287            guac_timestamp_msleep(settings->wol_wait_time * 1000);
    288    }
    289    
    290    /* Configure clipboard encoding */
    291    if (guac_vnc_set_clipboard_encoding(client, settings->clipboard_encoding)) {
    292        guac_client_log(client, GUAC_LOG_INFO, "Using non-standard VNC "
    293                "clipboard encoding: '%s'.", settings->clipboard_encoding);
    294    }
    295
    296    /* Set up libvncclient logging */
    297    rfbClientLog = guac_vnc_client_log_info;
    298    rfbClientErr = guac_vnc_client_log_error;
    299
    300    /* Attempt connection */
    301    rfbClient* rfb_client = guac_vnc_get_client(client);
    302    int retries_remaining = settings->retries;
    303
    304    /* If unsuccessful, retry as many times as specified */
    305    while (!rfb_client && retries_remaining > 0) {
    306
    307        guac_client_log(client, GUAC_LOG_INFO,
    308                "Connect failed. Waiting %ims before retrying...",
    309                GUAC_VNC_CONNECT_INTERVAL);
    310
    311        /* Wait for given interval then retry */
    312        guac_timestamp_msleep(GUAC_VNC_CONNECT_INTERVAL);
    313        rfb_client = guac_vnc_get_client(client);
    314        retries_remaining--;
    315
    316    }
    317
    318    /* If the final connect attempt fails, return error */
    319    if (!rfb_client) {
    320        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
    321                "Unable to connect to VNC server.");
    322        return NULL;
    323    }
    324
    325#ifdef ENABLE_PULSE
    326    /* If audio is enabled, start streaming via PulseAudio */
    327    if (settings->audio_enabled)
    328        vnc_client->audio = guac_pa_stream_alloc(client, 
    329                settings->pa_servername);
    330#endif
    331
    332#ifdef ENABLE_COMMON_SSH
    333    guac_common_ssh_init(client);
    334
    335    /* Connect via SSH if SFTP is enabled */
    336    if (settings->enable_sftp) {
    337
    338        /* Abort if username is missing */
    339        if (settings->sftp_username == NULL) {
    340            guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    341                    "SFTP username is required if SFTP is enabled.");
    342            return NULL;
    343        }
    344
    345        guac_client_log(client, GUAC_LOG_DEBUG,
    346                "Connecting via SSH for SFTP filesystem access.");
    347
    348        vnc_client->sftp_user =
    349            guac_common_ssh_create_user(settings->sftp_username);
    350
    351        /* Import private key, if given */
    352        if (settings->sftp_private_key != NULL) {
    353
    354            guac_client_log(client, GUAC_LOG_DEBUG,
    355                    "Authenticating with private key.");
    356
    357            /* Abort if private key cannot be read */
    358            if (guac_common_ssh_user_import_key(vnc_client->sftp_user,
    359                        settings->sftp_private_key,
    360                        settings->sftp_passphrase)) {
    361                guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    362                        "Private key unreadable.");
    363                return NULL;
    364            }
    365
    366        }
    367
    368        /* Otherwise, use specified password */
    369        else {
    370            guac_client_log(client, GUAC_LOG_DEBUG,
    371                    "Authenticating with password.");
    372            guac_common_ssh_user_set_password(vnc_client->sftp_user,
    373                    settings->sftp_password);
    374        }
    375
    376        /* Attempt SSH connection */
    377        vnc_client->sftp_session =
    378            guac_common_ssh_create_session(client, settings->sftp_hostname,
    379                    settings->sftp_port, vnc_client->sftp_user, settings->sftp_server_alive_interval,
    380                    settings->sftp_host_key, NULL);
    381
    382        /* Fail if SSH connection does not succeed */
    383        if (vnc_client->sftp_session == NULL) {
    384            /* Already aborted within guac_common_ssh_create_session() */
    385            return NULL;
    386        }
    387
    388        /* Load filesystem */
    389        vnc_client->sftp_filesystem =
    390            guac_common_ssh_create_sftp_filesystem(vnc_client->sftp_session,
    391                    settings->sftp_root_directory, NULL,
    392                    settings->sftp_disable_download,
    393                    settings->sftp_disable_upload);
    394
    395        /* Expose filesystem to connection owner */
    396        guac_client_for_owner(client,
    397                guac_common_ssh_expose_sftp_filesystem,
    398                vnc_client->sftp_filesystem);
    399
    400        /* Abort if SFTP connection fails */
    401        if (vnc_client->sftp_filesystem == NULL) {
    402            guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
    403                    "SFTP connection failed.");
    404            return NULL;
    405        }
    406
    407        /* Configure destination for basic uploads, if specified */
    408        if (settings->sftp_directory != NULL)
    409            guac_common_ssh_sftp_set_upload_path(
    410                    vnc_client->sftp_filesystem,
    411                    settings->sftp_directory);
    412
    413        guac_client_log(client, GUAC_LOG_DEBUG,
    414                "SFTP connection succeeded.");
    415
    416    }
    417#endif
    418
    419    /* Set remaining client data */
    420    vnc_client->rfb_client = rfb_client;
    421
    422    /* Set up screen recording, if requested */
    423    if (settings->recording_path != NULL) {
    424        vnc_client->recording = guac_recording_create(client,
    425                settings->recording_path,
    426                settings->recording_name,
    427                settings->create_recording_path,
    428                !settings->recording_exclude_output,
    429                !settings->recording_exclude_mouse,
    430                0, /* Touch events not supported */
    431                settings->recording_include_keys);
    432    }
    433
    434    /* Create display */
    435    vnc_client->display = guac_common_display_alloc(client,
    436            rfb_client->width, rfb_client->height);
    437
    438    /* Use lossless compression only if requested (otherwise, use default
    439     * heuristics) */
    440    guac_common_display_set_lossless(vnc_client->display, settings->lossless);
    441
    442    /* If not read-only, set an appropriate cursor */
    443    if (settings->read_only == 0) {
    444        if (settings->remote_cursor)
    445            guac_common_cursor_set_dot(vnc_client->display->cursor);
    446        else
    447            guac_common_cursor_set_pointer(vnc_client->display->cursor);
    448
    449    }
    450
    451    guac_socket_flush(client->socket);
    452
    453    guac_timestamp last_frame_end = guac_timestamp_current();
    454
    455    /* Handle messages from VNC server while client is running */
    456    while (client->state == GUAC_CLIENT_RUNNING) {
    457
    458        /* Wait for start of frame */
    459        int wait_result = guac_vnc_wait_for_messages(rfb_client,
    460                GUAC_VNC_FRAME_START_TIMEOUT);
    461        if (wait_result > 0) {
    462
    463            int processing_lag = guac_client_get_processing_lag(client);
    464            guac_timestamp frame_start = guac_timestamp_current();
    465
    466            /* Read server messages until frame is built */
    467            do {
    468
    469                guac_timestamp frame_end;
    470                int frame_remaining;
    471
    472                /* Handle any message received */
    473                if (!HandleRFBServerMessage(rfb_client)) {
    474                    guac_client_abort(client,
    475                            GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
    476                            "Error handling message from VNC server.");
    477                    break;
    478                }
    479
    480                /* Calculate time remaining in frame */
    481                frame_end = guac_timestamp_current();
    482                frame_remaining = frame_start + GUAC_VNC_FRAME_DURATION
    483                                - frame_end;
    484
    485                /* Calculate time that client needs to catch up */
    486                int time_elapsed = frame_end - last_frame_end;
    487                int required_wait = processing_lag - time_elapsed;
    488
    489                /* Increase the duration of this frame if client is lagging */
    490                if (required_wait > GUAC_VNC_FRAME_TIMEOUT)
    491                    wait_result = guac_vnc_wait_for_messages(rfb_client,
    492                            required_wait*1000);
    493
    494                /* Wait again if frame remaining */
    495                else if (frame_remaining > 0)
    496                    wait_result = guac_vnc_wait_for_messages(rfb_client,
    497                            GUAC_VNC_FRAME_TIMEOUT*1000);
    498                else
    499                    break;
    500
    501            } while (wait_result > 0);
    502
    503            /* Record end of frame, excluding server-side rendering time (we
    504             * assume server-side rendering time will be consistent between any
    505             * two subsequent frames, and that this time should thus be
    506             * excluded from the required wait period of the next frame). */
    507            last_frame_end = frame_start;
    508
    509        }
    510
    511        /* If an error occurs, log it and fail */
    512        if (wait_result < 0)
    513            guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Connection closed.");
    514
    515        /* Flush frame */
    516        guac_common_surface_flush(vnc_client->display->default_surface);
    517        guac_client_end_frame(client);
    518        guac_socket_flush(client->socket);
    519
    520    }
    521
    522    /* Kill client and finish connection */
    523    guac_client_stop(client);
    524    guac_client_log(client, GUAC_LOG_INFO, "Internal VNC client disconnected");
    525    return NULL;
    526
    527}