cscg24-guacamole

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

ssh.c (18019B)


      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 "argv.h"
     23#include "common-ssh/sftp.h"
     24#include "common-ssh/ssh.h"
     25#include "settings.h"
     26#include "sftp.h"
     27#include "ssh.h"
     28#include "terminal/terminal.h"
     29#include "ttymode.h"
     30
     31#ifdef ENABLE_SSH_AGENT
     32#include "ssh_agent.h"
     33#endif
     34
     35#include <libssh2.h>
     36#include <libssh2_sftp.h>
     37#include <guacamole/client.h>
     38#include <guacamole/mem.h>
     39#include <guacamole/recording.h>
     40#include <guacamole/socket.h>
     41#include <guacamole/timestamp.h>
     42#include <guacamole/wol.h>
     43#include <openssl/err.h>
     44#include <openssl/ssl.h>
     45
     46#ifdef LIBSSH2_USES_GCRYPT
     47#include <gcrypt.h>
     48#endif
     49
     50#include <errno.h>
     51#include <netdb.h>
     52#include <netinet/in.h>
     53#include <poll.h>
     54#include <pthread.h>
     55#include <stdbool.h>
     56#include <stddef.h>
     57#include <stdio.h>
     58#include <stdlib.h>
     59#include <string.h>
     60#include <sys/socket.h>
     61#include <sys/time.h>
     62
     63/**
     64 * Produces a new user object containing a username and password or private
     65 * key, prompting the user as necessary to obtain that information.
     66 *
     67 * @param client
     68 *     The Guacamole client containing any existing user data, as well as the
     69 *     terminal to use when prompting the user.
     70 *
     71 * @return
     72 *     A new user object containing the user's username and other credentials,
     73 *     or NULL if fails to import key.
     74 */
     75static guac_common_ssh_user* guac_ssh_get_user(guac_client* client) {
     76
     77    guac_ssh_client* ssh_client = (guac_ssh_client*) client->data;
     78    guac_ssh_settings* settings = ssh_client->settings;
     79
     80    guac_common_ssh_user* user;
     81
     82    /* Get username */
     83    if (settings->username == NULL)
     84        settings->username = guac_terminal_prompt(ssh_client->term,
     85                "Login as: ", true);
     86
     87    /* Create user object from username */
     88    user = guac_common_ssh_create_user(settings->username);
     89
     90    /* If key specified, import */
     91    if (settings->key_base64 != NULL) {
     92
     93        guac_client_log(client, GUAC_LOG_DEBUG,
     94                "Attempting private key import (WITHOUT passphrase)");
     95
     96        /* Attempt to read key without passphrase */
     97        if (guac_common_ssh_user_import_key(user,
     98                    settings->key_base64, NULL)) {
     99
    100            /* Log failure of initial attempt */
    101            guac_client_log(client, GUAC_LOG_DEBUG,
    102                    "Initial import failed: %s",
    103                    guac_common_ssh_key_error());
    104
    105            guac_client_log(client, GUAC_LOG_DEBUG,
    106                    "Re-attempting private key import (WITH passphrase)");
    107
    108            /* Prompt for passphrase if missing */
    109            if (settings->key_passphrase == NULL)
    110                settings->key_passphrase =
    111                    guac_terminal_prompt(ssh_client->term,
    112                            "Key passphrase: ", false);
    113
    114            /* Reattempt import with passphrase */
    115            if (guac_common_ssh_user_import_key(user,
    116                        settings->key_base64,
    117                        settings->key_passphrase)) {
    118
    119                /* If still failing, give up */
    120                guac_client_abort(client,
    121                        GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    122                        "Auth key import failed: %s",
    123                        guac_common_ssh_key_error());
    124
    125                guac_common_ssh_destroy_user(user);
    126                return NULL;
    127
    128            }
    129
    130        } /* end decrypt key with passphrase */
    131
    132        /* Success */
    133        guac_client_log(client, GUAC_LOG_INFO,
    134                "Auth key successfully imported.");
    135
    136    } /* end if key given */
    137
    138    /* If available, get password from settings */
    139    else if (settings->password != NULL) {
    140        guac_common_ssh_user_set_password(user, settings->password);
    141    }
    142
    143    /* Clear screen of any prompts */
    144    guac_terminal_printf(ssh_client->term, "\x1B[H\x1B[J");
    145
    146    return user;
    147
    148}
    149
    150/**
    151 * A function used to generate a terminal prompt to gather additional
    152 * credentials from the guac_client during a connection, and using
    153 * the specified string to generate the prompt for the user.
    154 *
    155 * @param client
    156 *     The guac_client object associated with the current connection
    157 *     where additional credentials are required.
    158 *
    159 * @param cred_name
    160 *     The prompt text to display to the screen when prompting for the
    161 *     additional credentials.
    162 *
    163 * @return
    164 *     The string of credentials gathered from the user.
    165 */
    166static char* guac_ssh_get_credential(guac_client *client, char* cred_name) {
    167
    168    guac_ssh_client* ssh_client = (guac_ssh_client*) client->data;
    169    return guac_terminal_prompt(ssh_client->term, cred_name, false);
    170
    171}
    172
    173void* ssh_input_thread(void* data) {
    174
    175    guac_client* client = (guac_client*) data;
    176    guac_ssh_client* ssh_client = (guac_ssh_client*) client->data;
    177
    178    char buffer[8192];
    179    int bytes_read;
    180
    181    /* Write all data read */
    182    while ((bytes_read = guac_terminal_read_stdin(ssh_client->term, buffer, sizeof(buffer))) > 0) {
    183        pthread_mutex_lock(&(ssh_client->term_channel_lock));
    184        libssh2_channel_write(ssh_client->term_channel, buffer, bytes_read);
    185        pthread_mutex_unlock(&(ssh_client->term_channel_lock));
    186
    187        /* Make sure ssh_input_thread can be terminated anyway */
    188        if (client->state == GUAC_CLIENT_STOPPING)
    189            break;
    190    }
    191
    192    /* Stop the client so that ssh_client_thread can be terminated */
    193    guac_client_stop(client);
    194    return NULL;
    195
    196}
    197
    198void* ssh_client_thread(void* data) {
    199
    200    guac_client* client = (guac_client*) data;
    201    guac_ssh_client* ssh_client = (guac_ssh_client*) client->data;
    202    guac_ssh_settings* settings = ssh_client->settings;
    203
    204    char buffer[8192];
    205
    206    pthread_t input_thread;
    207
    208    /* If Wake-on-LAN is enabled, attempt to wake. */
    209    if (settings->wol_send_packet) {
    210        guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, "
    211                "and pausing for %d seconds.", settings->wol_wait_time);
    212
    213        /* Send the Wake-on-LAN request. */
    214        if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr,
    215                settings->wol_udp_port))
    216            return NULL;
    217
    218        /* If wait time is specified, sleep for that amount of time. */
    219        if (settings->wol_wait_time > 0)
    220            guac_timestamp_msleep(settings->wol_wait_time * 1000);
    221    }
    222
    223    /* Init SSH base libraries */
    224    if (guac_common_ssh_init(client)) {
    225        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    226                "SSH library initialization failed");
    227        return NULL;
    228    }
    229
    230    char ssh_ttymodes[GUAC_SSH_TTYMODES_SIZE(1)];
    231
    232    /* Set up screen recording, if requested */
    233    if (settings->recording_path != NULL) {
    234        ssh_client->recording = guac_recording_create(client,
    235                settings->recording_path,
    236                settings->recording_name,
    237                settings->create_recording_path,
    238                !settings->recording_exclude_output,
    239                !settings->recording_exclude_mouse,
    240                0, /* Touch events not supported */
    241                settings->recording_include_keys);
    242    }
    243
    244    /* Create terminal options with required parameters */
    245    guac_terminal_options* options = guac_terminal_options_create(
    246            settings->width, settings->height, settings->resolution);
    247
    248    /* Set optional parameters */
    249    options->disable_copy = settings->disable_copy;
    250    options->max_scrollback = settings->max_scrollback;
    251    options->font_name = settings->font_name;
    252    options->font_size = settings->font_size;
    253    options->color_scheme = settings->color_scheme;
    254    options->backspace = settings->backspace;
    255
    256    /* Create terminal */
    257    ssh_client->term = guac_terminal_create(client, options);
    258
    259    /* Free options struct now that it's been used */
    260    guac_mem_free(options);
    261
    262    /* Fail if terminal init failed */
    263    if (ssh_client->term == NULL) {
    264        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    265                "Terminal initialization failed");
    266        return NULL;
    267    }
    268
    269    /* Send current values of exposed arguments to owner only */
    270    guac_client_for_owner(client, guac_ssh_send_current_argv, ssh_client);
    271
    272    /* Set up typescript, if requested */
    273    if (settings->typescript_path != NULL) {
    274        guac_terminal_create_typescript(ssh_client->term,
    275                settings->typescript_path,
    276                settings->typescript_name,
    277                settings->create_typescript_path);
    278    }
    279
    280    /* Get user and credentials */
    281    ssh_client->user = guac_ssh_get_user(client);
    282    if (ssh_client->user == NULL) {
    283        /* Already aborted within guac_ssh_get_user() */
    284        return NULL;
    285    }
    286
    287    /* Ensure connection is kept alive during lengthy connects */
    288    guac_socket_require_keep_alive(client->socket);
    289
    290    /* Open SSH session */
    291    ssh_client->session = guac_common_ssh_create_session(client,
    292            settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval,
    293            settings->host_key, guac_ssh_get_credential);
    294    if (ssh_client->session == NULL) {
    295        /* Already aborted within guac_common_ssh_create_session() */
    296        return NULL;
    297    }
    298
    299    pthread_mutex_init(&ssh_client->term_channel_lock, NULL);
    300
    301    /* Open channel for terminal */
    302    ssh_client->term_channel =
    303        libssh2_channel_open_session(ssh_client->session->session);
    304    if (ssh_client->term_channel == NULL) {
    305        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
    306                "Unable to open terminal channel.");
    307        return NULL;
    308    }
    309
    310    /* Set the client timezone */
    311    if (settings->timezone != NULL) {
    312        if (libssh2_channel_setenv(ssh_client->term_channel, "TZ",
    313                    settings->timezone)) {
    314            guac_client_log(client, GUAC_LOG_WARNING,
    315                    "Unable to set the timezone: SSH server "
    316                    "refused to set \"TZ\" variable.");
    317        }
    318    }
    319
    320
    321#ifdef ENABLE_SSH_AGENT
    322    /* Start SSH agent forwarding, if enabled */
    323    if (ssh_client->enable_agent) {
    324        libssh2_session_callback_set(ssh_client->session,
    325                LIBSSH2_CALLBACK_AUTH_AGENT, (void*) ssh_auth_agent_callback);
    326
    327        /* Request agent forwarding */
    328        if (libssh2_channel_request_auth_agent(ssh_client->term_channel))
    329            guac_client_log(client, GUAC_LOG_ERROR, "Agent forwarding request failed");
    330        else
    331            guac_client_log(client, GUAC_LOG_INFO, "Agent forwarding enabled.");
    332    }
    333
    334    ssh_client->auth_agent = NULL;
    335#endif
    336
    337    /* Start SFTP session as well, if enabled */
    338    if (settings->enable_sftp) {
    339
    340        /* Create SSH session specific for SFTP */
    341        guac_client_log(client, GUAC_LOG_DEBUG, "Reconnecting for SFTP...");
    342        ssh_client->sftp_session =
    343            guac_common_ssh_create_session(client, settings->hostname,
    344                    settings->port, ssh_client->user, settings->server_alive_interval,
    345                    settings->host_key, NULL);
    346        if (ssh_client->sftp_session == NULL) {
    347            /* Already aborted within guac_common_ssh_create_session() */
    348            return NULL;
    349        }
    350
    351        /* Request SFTP */
    352        ssh_client->sftp_filesystem = guac_common_ssh_create_sftp_filesystem(
    353                    ssh_client->sftp_session, settings->sftp_root_directory,
    354                    NULL, settings->sftp_disable_download,
    355                    settings->sftp_disable_upload);
    356
    357        /* Expose filesystem to connection owner */
    358        guac_client_for_owner(client,
    359                guac_common_ssh_expose_sftp_filesystem,
    360                ssh_client->sftp_filesystem);
    361
    362        /* Init handlers for Guacamole-specific console codes */
    363        if (!settings->sftp_disable_upload)
    364            guac_terminal_set_upload_path_handler(ssh_client->term,
    365                    guac_sftp_set_upload_path);
    366
    367        if (!settings->sftp_disable_download)
    368            guac_terminal_set_file_download_handler(ssh_client->term,
    369                    guac_sftp_download_file);
    370
    371        guac_client_log(client, GUAC_LOG_DEBUG, "SFTP session initialized");
    372
    373    }
    374
    375    /* Set up the ttymode array prior to requesting the PTY */
    376    int ttymodeBytes = guac_ssh_ttymodes_init(ssh_ttymodes,
    377            GUAC_SSH_TTY_OP_VERASE, settings->backspace, GUAC_SSH_TTY_OP_END);
    378    if (ttymodeBytes < 1)
    379        guac_client_log(client, GUAC_LOG_WARNING, "Unable to set TTY modes."
    380                "  Backspace may not work as expected.");
    381
    382    /* Request PTY */
    383    int term_height = guac_terminal_get_rows(ssh_client->term);
    384    int term_width = guac_terminal_get_columns(ssh_client->term);
    385    if (libssh2_channel_request_pty_ex(ssh_client->term_channel,
    386            settings->terminal_type, strlen(settings->terminal_type),
    387            ssh_ttymodes, ttymodeBytes, term_width, term_height, 0, 0)) {
    388        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Unable to allocate PTY.");
    389        return NULL;
    390    }
    391
    392    /* Forward specified locale */
    393    if (settings->locale != NULL) {
    394        if (libssh2_channel_setenv(ssh_client->term_channel, "LANG",
    395                    settings->locale)) {
    396            guac_client_log(client, GUAC_LOG_WARNING,
    397                    "Unable to forward locale: SSH server refused to set "
    398                    "\"LANG\" environment variable.");
    399        }
    400    }
    401
    402    /* If a command is specified, run that instead of a shell */
    403    if (settings->command != NULL) {
    404        if (libssh2_channel_exec(ssh_client->term_channel, settings->command)) {
    405            guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
    406                    "Unable to execute command.");
    407            return NULL;
    408        }
    409    }
    410
    411    /* Otherwise, request a shell */
    412    else if (libssh2_channel_shell(ssh_client->term_channel)) {
    413        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
    414                "Unable to associate shell with PTY.");
    415        return NULL;
    416    }
    417
    418    /* Logged in */
    419    guac_client_log(client, GUAC_LOG_INFO, "SSH connection successful.");
    420    guac_terminal_start(ssh_client->term);
    421
    422    /* Start input thread */
    423    if (pthread_create(&(input_thread), NULL, ssh_input_thread, (void*) client)) {
    424        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread");
    425        return NULL;
    426    }
    427
    428    /* Set non-blocking */
    429    libssh2_session_set_blocking(ssh_client->session->session, 0);
    430
    431    /* While data available, write to terminal */
    432    int bytes_read = 0;
    433    for (;;) {
    434
    435        /* Track total amount of data read */
    436        int total_read = 0;
    437
    438        /* Timeout for polling socket activity */
    439        int timeout;
    440
    441        pthread_mutex_lock(&(ssh_client->term_channel_lock));
    442
    443        /* Stop reading at EOF */
    444        if (libssh2_channel_eof(ssh_client->term_channel)) {
    445            pthread_mutex_unlock(&(ssh_client->term_channel_lock));
    446            break;
    447        }
    448
    449        /* Client is stopping, break the loop */
    450        if (client->state == GUAC_CLIENT_STOPPING) {
    451            pthread_mutex_unlock(&(ssh_client->term_channel_lock));
    452            break;
    453        }
    454
    455        /* Send keepalive at configured interval */
    456        if (settings->server_alive_interval > 0) {
    457            timeout = 0;
    458            if (libssh2_keepalive_send(ssh_client->session->session, &timeout) > 0) {
    459                pthread_mutex_unlock(&(ssh_client->term_channel_lock));
    460                break;
    461            }
    462            timeout *= 1000;
    463        }
    464        /* If keepalive is not configured, sleep for the default of 1 second */
    465        else
    466            timeout = GUAC_SSH_DEFAULT_POLL_TIMEOUT;
    467
    468        /* Read terminal data */
    469        bytes_read = libssh2_channel_read(ssh_client->term_channel,
    470                buffer, sizeof(buffer));
    471
    472        pthread_mutex_unlock(&(ssh_client->term_channel_lock));
    473
    474        /* Attempt to write data received. Exit on failure. */
    475        if (bytes_read > 0) {
    476            int written = guac_terminal_write(ssh_client->term, buffer, bytes_read);
    477            if (written < 0)
    478                break;
    479
    480            total_read += bytes_read;
    481        }
    482
    483        else if (bytes_read < 0 && bytes_read != LIBSSH2_ERROR_EAGAIN)
    484            break;
    485
    486#ifdef ENABLE_SSH_AGENT
    487        /* If agent open, handle any agent packets */
    488        if (ssh_client->auth_agent != NULL) {
    489            bytes_read = ssh_auth_agent_read(ssh_client->auth_agent);
    490            if (bytes_read > 0)
    491                total_read += bytes_read;
    492            else if (bytes_read < 0 && bytes_read != LIBSSH2_ERROR_EAGAIN)
    493                ssh_client->auth_agent = NULL;
    494        }
    495#endif
    496
    497        /* Wait for more data if reads turn up empty */
    498        if (total_read == 0) {
    499
    500            /* Wait on the SSH session file descriptor only */
    501            struct pollfd fds[] = {{
    502                .fd      = ssh_client->session->fd,
    503                .events  = POLLIN,
    504                .revents = 0,
    505            }};
    506
    507            /* Wait up to computed timeout */
    508            if (poll(fds, 1, timeout) < 0)
    509                break;
    510
    511        }
    512
    513    }
    514
    515    /* Kill client and Wait for input thread to die */
    516    guac_client_stop(client);
    517    pthread_join(input_thread, NULL);
    518
    519    pthread_mutex_destroy(&ssh_client->term_channel_lock);
    520
    521    guac_client_log(client, GUAC_LOG_INFO, "SSH connection ended.");
    522    return NULL;
    523
    524}
    525