cscg24-guacamole

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

telnet.c (21948B)


      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 "telnet.h"
     24#include "terminal/terminal.h"
     25
     26#include <guacamole/client.h>
     27#include <guacamole/mem.h>
     28#include <guacamole/protocol.h>
     29#include <guacamole/recording.h>
     30#include <guacamole/timestamp.h>
     31#include <guacamole/wol.h>
     32#include <libtelnet.h>
     33
     34#include <errno.h>
     35#include <netdb.h>
     36#include <netinet/in.h>
     37#include <poll.h>
     38#include <pthread.h>
     39#include <stdbool.h>
     40#include <stdlib.h>
     41#include <string.h>
     42#include <sys/socket.h>
     43#include <sys/time.h>
     44#include <unistd.h>
     45
     46/**
     47 * Support levels for various telnet options, required for connection
     48 * negotiation by telnet_init(), part of libtelnet.
     49 */
     50static const telnet_telopt_t __telnet_options[] = {
     51    { TELNET_TELOPT_ECHO,        TELNET_WONT, TELNET_DO   },
     52    { TELNET_TELOPT_TTYPE,       TELNET_WILL, TELNET_DONT },
     53    { TELNET_TELOPT_COMPRESS2,   TELNET_WONT, TELNET_DO   },
     54    { TELNET_TELOPT_MSSP,        TELNET_WONT, TELNET_DO   },
     55    { TELNET_TELOPT_NAWS,        TELNET_WILL, TELNET_DONT },
     56    { TELNET_TELOPT_NEW_ENVIRON, TELNET_WILL, TELNET_DONT },
     57    { -1, 0, 0 }
     58};
     59
     60/**
     61 * Write the entire buffer given to the specified file descriptor, retrying
     62 * the write automatically if necessary. This function will return a value
     63 * not equal to the buffer's size iff an error occurs which prevents all
     64 * future writes.
     65 *
     66 * @param fd The file descriptor to write to.
     67 * @param buffer The buffer to write.
     68 * @param size The number of bytes from the buffer to write.
     69 */
     70static int __guac_telnet_write_all(int fd, const char* buffer, int size) {
     71
     72    int remaining = size;
     73    while (remaining > 0) {
     74
     75        /* Attempt to write data */
     76        int ret_val = write(fd, buffer, remaining);
     77        if (ret_val <= 0)
     78            return -1;
     79
     80        /* If successful, contine with what data remains (if any) */
     81        remaining -= ret_val;
     82        buffer += ret_val;
     83
     84    }
     85
     86    return size;
     87
     88}
     89
     90/**
     91 * Matches the given line against the given regex, returning true and sending
     92 * the given value if a match is found. An enter keypress is automatically
     93 * sent after the value is sent.
     94 *
     95 * @param client
     96 *     The guac_client associated with the telnet session.
     97 *
     98 * @param regex
     99 *     The regex to search for within the given line buffer.
    100 *
    101 * @param value
    102 *     The string value to send through STDIN of the telnet session if a
    103 *     match is found, or NULL if no value should be sent.
    104 *
    105 * @param line_buffer
    106 *     The line of character data to test.
    107 *
    108 * @return
    109 *     true if a match is found, false otherwise.
    110 */
    111static bool guac_telnet_regex_exec(guac_client* client, regex_t* regex,
    112        const char* value, const char* line_buffer) {
    113
    114    guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
    115
    116    /* Send value upon match */
    117    if (regexec(regex, line_buffer, 0, NULL, 0) == 0) {
    118
    119        /* Send value */
    120        if (value != NULL) {
    121            guac_terminal_send_string(telnet_client->term, value);
    122            guac_terminal_send_string(telnet_client->term, "\x0D");
    123        }
    124
    125        /* Stop searching for prompt */
    126        return true;
    127
    128    }
    129
    130    return false;
    131
    132}
    133
    134/**
    135 * Matches the given line against the various stored regexes, automatically
    136 * sending the configured username, password, or reporting login
    137 * success/failure depending on context. If no search is in progress, either
    138 * because no regexes have been defined or because all applicable searches have
    139 * completed, this function has no effect.
    140 *
    141 * @param client
    142 *     The guac_client associated with the telnet session.
    143 *
    144 * @param line_buffer
    145 *     The line of character data to test.
    146 */
    147static void guac_telnet_search_line(guac_client* client, const char* line_buffer) {
    148
    149    guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
    150    guac_telnet_settings* settings = telnet_client->settings;
    151
    152    /* Continue search for username prompt */
    153    if (settings->username_regex != NULL) {
    154        if (guac_telnet_regex_exec(client, settings->username_regex,
    155                    settings->username, line_buffer)) {
    156            guac_client_log(client, GUAC_LOG_DEBUG, "Username sent");
    157            guac_telnet_regex_free(&settings->username_regex);
    158        }
    159    }
    160
    161    /* Continue search for password prompt */
    162    if (settings->password_regex != NULL) {
    163        if (guac_telnet_regex_exec(client, settings->password_regex,
    164                    settings->password, line_buffer)) {
    165
    166            guac_client_log(client, GUAC_LOG_DEBUG, "Password sent");
    167
    168            /* Do not continue searching for username/password once password is sent */
    169            guac_telnet_regex_free(&settings->username_regex);
    170            guac_telnet_regex_free(&settings->password_regex);
    171
    172        }
    173    }
    174
    175    /* Continue search for login success */
    176    if (settings->login_success_regex != NULL) {
    177        if (guac_telnet_regex_exec(client, settings->login_success_regex,
    178                    NULL, line_buffer)) {
    179
    180            /* Allow terminal to render now that login has been deemed successful */
    181            guac_client_log(client, GUAC_LOG_DEBUG, "Login successful");
    182            guac_terminal_start(telnet_client->term);
    183
    184            /* Stop all searches */
    185            guac_telnet_regex_free(&settings->username_regex);
    186            guac_telnet_regex_free(&settings->password_regex);
    187            guac_telnet_regex_free(&settings->login_success_regex);
    188            guac_telnet_regex_free(&settings->login_failure_regex);
    189
    190        }
    191    }
    192
    193    /* Continue search for login failure */
    194    if (settings->login_failure_regex != NULL) {
    195        if (guac_telnet_regex_exec(client, settings->login_failure_regex,
    196                    NULL, line_buffer)) {
    197
    198            /* Advise that login has failed and connection should be closed */
    199            guac_client_abort(client,
    200                    GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    201                    "Login failed");
    202
    203            /* Stop all searches */
    204            guac_telnet_regex_free(&settings->username_regex);
    205            guac_telnet_regex_free(&settings->password_regex);
    206            guac_telnet_regex_free(&settings->login_success_regex);
    207            guac_telnet_regex_free(&settings->login_failure_regex);
    208
    209        }
    210    }
    211
    212}
    213
    214/**
    215 * Searches for a line matching the various stored regexes, automatically
    216 * sending the configured username, password, or reporting login
    217 * success/failure depending on context. If no search is in progress, either
    218 * because no regexes have been defined or because all applicable searches
    219 * have completed, this function has no effect.
    220 *
    221 * @param client
    222 *     The guac_client associated with the telnet session.
    223 *
    224 * @param buffer
    225 *     The buffer of received data to search through.
    226 *
    227 * @param size
    228 *     The size of the given buffer, in bytes.
    229 */
    230static void guac_telnet_search(guac_client* client, const char* buffer, int size) {
    231
    232    static char line_buffer[1024] = {0};
    233    static int length = 0;
    234
    235    /* Append all characters in buffer to current line */
    236    const char* current = buffer;
    237    for (int i = 0; i < size; i++) {
    238
    239        char c = *(current++);
    240
    241        /* Attempt pattern match and clear buffer upon reading newline */
    242        if (c == '\n') {
    243            if (length > 0) {
    244                line_buffer[length] = '\0';
    245                guac_telnet_search_line(client, line_buffer);
    246                length = 0;
    247            }
    248        }
    249
    250        /* Append all non-newline characters to line buffer as long as space
    251         * remains */
    252        else if (length < sizeof(line_buffer) - 1)
    253            line_buffer[length++] = c;
    254
    255    }
    256
    257    /* Attempt pattern match if an unfinished line remains (may be a prompt) */
    258    if (length > 0) {
    259        line_buffer[length] = '\0';
    260        guac_telnet_search_line(client, line_buffer);
    261    }
    262
    263}
    264
    265/**
    266 * Event handler, as defined by libtelnet. This function is passed to
    267 * telnet_init() and will be called for every event fired by libtelnet,
    268 * including feature enable/disable and receipt/transmission of data.
    269 */
    270static void __guac_telnet_event_handler(telnet_t* telnet, telnet_event_t* event, void* data) {
    271
    272    guac_client* client = (guac_client*) data;
    273    guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
    274    guac_telnet_settings* settings = telnet_client->settings;
    275
    276    switch (event->type) {
    277
    278        /* Terminal output received */
    279        case TELNET_EV_DATA:
    280            guac_terminal_write(telnet_client->term, event->data.buffer, event->data.size);
    281            guac_telnet_search(client, event->data.buffer, event->data.size);
    282            break;
    283
    284        /* Data destined for remote end */
    285        case TELNET_EV_SEND:
    286            if (__guac_telnet_write_all(telnet_client->socket_fd, event->data.buffer, event->data.size)
    287                    != event->data.size)
    288                guac_client_stop(client);
    289            break;
    290
    291        /* Remote feature enabled */
    292        case TELNET_EV_WILL:
    293            if (event->neg.telopt == TELNET_TELOPT_ECHO)
    294                telnet_client->echo_enabled = 0; /* Disable local echo, as remote will echo */
    295            break;
    296
    297        /* Remote feature disabled */
    298        case TELNET_EV_WONT:
    299            if (event->neg.telopt == TELNET_TELOPT_ECHO)
    300                telnet_client->echo_enabled = 1; /* Enable local echo, as remote won't echo */
    301            break;
    302
    303        /* Local feature enable */
    304        case TELNET_EV_DO:
    305            if (event->neg.telopt == TELNET_TELOPT_NAWS) {
    306                telnet_client->naws_enabled = 1;
    307                guac_telnet_send_naws(telnet,
    308                        guac_terminal_get_columns(telnet_client->term),
    309                        guac_terminal_get_rows(telnet_client->term));
    310            }
    311            break;
    312
    313        /* Terminal type request */
    314        case TELNET_EV_TTYPE:
    315            if (event->ttype.cmd == TELNET_TTYPE_SEND)
    316                telnet_ttype_is(telnet_client->telnet, settings->terminal_type);
    317            break;
    318
    319        /* Environment request */
    320        case TELNET_EV_ENVIRON:
    321
    322            /* Only send USER if entire environment was requested */
    323            if (event->environ.size == 0)
    324                guac_telnet_send_user(telnet, settings->username);
    325
    326            break;
    327
    328        /* Connection warnings */
    329        case TELNET_EV_WARNING:
    330            guac_client_log(client, GUAC_LOG_WARNING, "%s", event->error.msg);
    331            break;
    332
    333        /* Connection errors */
    334        case TELNET_EV_ERROR:
    335            guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
    336                    "Telnet connection closing with error: %s", event->error.msg);
    337            break;
    338
    339        /* Ignore other events */
    340        default:
    341            break;
    342
    343    }
    344
    345}
    346
    347/**
    348 * Input thread, started by the main telnet client thread. This thread
    349 * continuously reads from the terminal's STDIN and transfers all read
    350 * data to the telnet connection.
    351 *
    352 * @param data The current guac_client instance.
    353 * @return Always NULL.
    354 */
    355static void* __guac_telnet_input_thread(void* data) {
    356
    357    guac_client* client = (guac_client*) data;
    358    guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
    359
    360    char buffer[8192];
    361    int bytes_read;
    362
    363    /* Write all data read */
    364    while ((bytes_read = guac_terminal_read_stdin(telnet_client->term, buffer, sizeof(buffer))) > 0) {
    365        telnet_send(telnet_client->telnet, buffer, bytes_read);
    366        if (telnet_client->echo_enabled)
    367            guac_terminal_write(telnet_client->term, buffer, bytes_read);
    368    }
    369
    370    return NULL;
    371
    372}
    373
    374/**
    375 * Connects to the telnet server specified within the data associated
    376 * with the given guac_client, which will have been populated by
    377 * guac_client_init.
    378 *
    379 * @return The connected telnet instance, if successful, or NULL if the
    380 *         connection fails for any reason.
    381 */
    382static telnet_t* __guac_telnet_create_session(guac_client* client) {
    383
    384    int retval;
    385
    386    int fd;
    387    struct addrinfo* addresses;
    388    struct addrinfo* current_address;
    389
    390    char connected_address[1024];
    391    char connected_port[64];
    392
    393    guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
    394    guac_telnet_settings* settings = telnet_client->settings;
    395
    396    struct addrinfo hints = {
    397        .ai_family   = AF_UNSPEC,
    398        .ai_socktype = SOCK_STREAM,
    399        .ai_protocol = IPPROTO_TCP
    400    };
    401
    402    /* Get socket */
    403    fd = socket(AF_INET, SOCK_STREAM, 0);
    404
    405    /* Get addresses connection */
    406    if ((retval = getaddrinfo(settings->hostname, settings->port,
    407                    &hints, &addresses))) {
    408        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error parsing given address or port: %s",
    409                gai_strerror(retval));
    410        return NULL;
    411
    412    }
    413
    414    /* Attempt connection to each address until success */
    415    current_address = addresses;
    416    while (current_address != NULL) {
    417
    418        int retval;
    419
    420        /* Resolve hostname */
    421        if ((retval = getnameinfo(current_address->ai_addr,
    422                current_address->ai_addrlen,
    423                connected_address, sizeof(connected_address),
    424                connected_port, sizeof(connected_port),
    425                NI_NUMERICHOST | NI_NUMERICSERV)))
    426            guac_client_log(client, GUAC_LOG_DEBUG, "Unable to resolve host: %s", gai_strerror(retval));
    427
    428        /* Connect */
    429        if (connect(fd, current_address->ai_addr,
    430                        current_address->ai_addrlen) == 0) {
    431
    432            guac_client_log(client, GUAC_LOG_DEBUG, "Successfully connected to "
    433                    "host %s, port %s", connected_address, connected_port);
    434
    435            /* Done if successful connect */
    436            break;
    437
    438        }
    439
    440        /* Otherwise log information regarding bind failure */
    441        else
    442            guac_client_log(client, GUAC_LOG_DEBUG, "Unable to connect to "
    443                    "host %s, port %s: %s",
    444                    connected_address, connected_port, strerror(errno));
    445
    446        current_address = current_address->ai_next;
    447
    448    }
    449
    450    /* If unable to connect to anything, fail */
    451    if (current_address == NULL) {
    452        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
    453                "Unable to connect to any addresses.");
    454        return NULL;
    455    }
    456
    457    /* Free addrinfo */
    458    freeaddrinfo(addresses);
    459
    460    /* Open telnet session */
    461    telnet_t* telnet = telnet_init(__telnet_options, __guac_telnet_event_handler, 0, client);
    462    if (telnet == NULL) {
    463        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Telnet client allocation failed.");
    464        return NULL;
    465    }
    466
    467    /* Save file descriptor */
    468    telnet_client->socket_fd = fd;
    469
    470    return telnet;
    471
    472}
    473
    474/**
    475 * Sends a 16-bit value over the given telnet connection with the byte order
    476 * required by the telnet protocol.
    477 *
    478 * @param telnet The telnet connection to use.
    479 * @param value The value to send.
    480 */
    481static void __guac_telnet_send_uint16(telnet_t* telnet, uint16_t value) {
    482
    483    unsigned char buffer[2];
    484    buffer[0] = (value >> 8) & 0xFF;
    485    buffer[1] =  value       & 0xFF;
    486
    487    telnet_send(telnet, (char*) buffer, 2);
    488
    489}
    490
    491/**
    492 * Sends an 8-bit value over the given telnet connection.
    493 *
    494 * @param telnet The telnet connection to use.
    495 * @param value The value to send.
    496 */
    497static void __guac_telnet_send_uint8(telnet_t* telnet, uint8_t value) {
    498    telnet_send(telnet, (char*) (&value), 1);
    499}
    500
    501void guac_telnet_send_naws(telnet_t* telnet, uint16_t width, uint16_t height) {
    502    telnet_begin_sb(telnet, TELNET_TELOPT_NAWS);
    503    __guac_telnet_send_uint16(telnet, width);
    504    __guac_telnet_send_uint16(telnet, height);
    505    telnet_finish_sb(telnet);
    506}
    507
    508void guac_telnet_send_user(telnet_t* telnet, const char* username) {
    509
    510    /* IAC SB NEW-ENVIRON IS */
    511    telnet_begin_sb(telnet, TELNET_TELOPT_NEW_ENVIRON);
    512    __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_IS);
    513
    514    /* Only send username if defined */
    515    if (username != NULL) {
    516
    517        /* VAR "USER" */
    518        __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_VAR);
    519        telnet_send(telnet, "USER", 4);
    520
    521        /* VALUE username */
    522        __guac_telnet_send_uint8(telnet, TELNET_ENVIRON_VALUE);
    523        telnet_send(telnet, username, strlen(username));
    524
    525    }
    526
    527    /* IAC SE */
    528    telnet_finish_sb(telnet);
    529
    530}
    531
    532/**
    533 * Waits for data on the given file descriptor for up to one second. The
    534 * return value is identical to that of select(): 0 on timeout, < 0 on
    535 * error, and > 0 on success.
    536 *
    537 * @param socket_fd The file descriptor to wait for.
    538 * @return A value greater than zero on success, zero on timeout, and
    539 *         less than zero on error.
    540 */
    541static int __guac_telnet_wait(int socket_fd) {
    542
    543    /* Build array of file descriptors */
    544    struct pollfd fds[] = {{
    545        .fd      = socket_fd,
    546        .events  = POLLIN,
    547        .revents = 0,
    548    }};
    549
    550    /* Wait for one second */
    551    return poll(fds, 1, 1000);
    552
    553}
    554
    555void* guac_telnet_client_thread(void* data) {
    556
    557    guac_client* client = (guac_client*) data;
    558    guac_telnet_client* telnet_client = (guac_telnet_client*) client->data;
    559    guac_telnet_settings* settings = telnet_client->settings;
    560
    561    pthread_t input_thread;
    562    char buffer[8192];
    563    int wait_result;
    564
    565    /* If Wake-on-LAN is enabled, attempt to wake. */
    566    if (settings->wol_send_packet) {
    567        guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, "
    568                "and pausing for %d seconds.", settings->wol_wait_time);
    569
    570        /* Send the Wake-on-LAN request. */
    571        if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr,
    572                settings->wol_udp_port))
    573            return NULL;
    574
    575        /* If wait time is specified, sleep for that amount of time. */
    576        if (settings->wol_wait_time > 0)
    577            guac_timestamp_msleep(settings->wol_wait_time * 1000);
    578    }
    579
    580    /* Set up screen recording, if requested */
    581    if (settings->recording_path != NULL) {
    582        telnet_client->recording = guac_recording_create(client,
    583                settings->recording_path,
    584                settings->recording_name,
    585                settings->create_recording_path,
    586                !settings->recording_exclude_output,
    587                !settings->recording_exclude_mouse,
    588                0, /* Touch events not supported */
    589                settings->recording_include_keys);
    590    }
    591
    592    /* Create terminal options with required parameters */
    593    guac_terminal_options* options = guac_terminal_options_create(
    594            settings->width, settings->height, settings->resolution);
    595
    596    /* Set optional parameters */
    597    options->disable_copy = settings->disable_copy;
    598    options->max_scrollback = settings->max_scrollback;
    599    options->font_name = settings->font_name;
    600    options->font_size = settings->font_size;
    601    options->color_scheme = settings->color_scheme;
    602    options->backspace = settings->backspace;
    603
    604    /* Create terminal */
    605    telnet_client->term = guac_terminal_create(client, options);
    606
    607    /* Free options struct now that it's been used */
    608    guac_mem_free(options);
    609
    610    /* Fail if terminal init failed */
    611    if (telnet_client->term == NULL) {
    612        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    613                "Terminal initialization failed");
    614        return NULL;
    615    }
    616
    617    /* Send current values of exposed arguments to owner only */
    618    guac_client_for_owner(client, guac_telnet_send_current_argv,
    619            telnet_client);
    620
    621    /* Set up typescript, if requested */
    622    if (settings->typescript_path != NULL) {
    623        guac_terminal_create_typescript(telnet_client->term,
    624                settings->typescript_path,
    625                settings->typescript_name,
    626                settings->create_typescript_path);
    627    }
    628
    629    /* Open telnet session */
    630    telnet_client->telnet = __guac_telnet_create_session(client);
    631    if (telnet_client->telnet == NULL) {
    632        /* Already aborted within __guac_telnet_create_session() */
    633        return NULL;
    634    }
    635
    636    /* Logged in */
    637    guac_client_log(client, GUAC_LOG_INFO, "Telnet connection successful.");
    638
    639    /* Allow terminal to render if login success/failure detection is not
    640     * enabled */
    641    if (settings->login_success_regex == NULL
    642            && settings->login_failure_regex == NULL)
    643        guac_terminal_start(telnet_client->term);
    644
    645    /* Start input thread */
    646    if (pthread_create(&(input_thread), NULL, __guac_telnet_input_thread, (void*) client)) {
    647        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread");
    648        return NULL;
    649    }
    650
    651    /* While data available, write to terminal */
    652    while ((wait_result = __guac_telnet_wait(telnet_client->socket_fd)) >= 0) {
    653
    654        /* Resume waiting of no data available */
    655        if (wait_result == 0)
    656            continue;
    657
    658        int bytes_read = read(telnet_client->socket_fd, buffer, sizeof(buffer));
    659        if (bytes_read <= 0)
    660            break;
    661
    662        telnet_recv(telnet_client->telnet, buffer, bytes_read);
    663
    664    }
    665
    666    /* Kill client and Wait for input thread to die */
    667    guac_client_stop(client);
    668    pthread_join(input_thread, NULL);
    669
    670    guac_client_log(client, GUAC_LOG_INFO, "Telnet connection ended.");
    671    return NULL;
    672
    673}
    674