cscg24-guacamole

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

kubernetes.c (14545B)


      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 "client.h"
     24#include "io.h"
     25#include "kubernetes.h"
     26#include "ssl.h"
     27#include "terminal/terminal.h"
     28#include "url.h"
     29
     30#include <guacamole/client.h>
     31#include <guacamole/mem.h>
     32#include <guacamole/protocol.h>
     33#include <guacamole/recording.h>
     34#include <libwebsockets.h>
     35
     36#include <pthread.h>
     37#include <stdio.h>
     38#include <stdlib.h>
     39
     40/**
     41 * Callback invoked by libwebsockets for events related to a WebSocket being
     42 * used for communicating with an attached Kubernetes pod.
     43 *
     44 * @param wsi
     45 *     The libwebsockets handle for the WebSocket connection.
     46 *
     47 * @param reason
     48 *     The reason (event) that this callback was invoked.
     49 *
     50 * @param user
     51 *     Arbitrary data assocated with the WebSocket session. In some cases,
     52 *     this is actually event-specific data (such as the
     53 *     LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERT event).
     54 *
     55 * @param in
     56 *     A pointer to arbitrary, reason-specific data.
     57 *
     58 * @param length
     59 *     An arbitrary, reason-specific length value.
     60 *
     61 * @return
     62 *     An undocumented integer value related the success of handling the
     63 *     event, or -1 if the WebSocket connection should be closed.
     64 */
     65static int guac_kubernetes_lws_callback(struct lws* wsi,
     66        enum lws_callback_reasons reason, void* user,
     67        void* in, size_t length) {
     68
     69    guac_client* client = guac_kubernetes_lws_current_client;
     70    guac_kubernetes_client* kubernetes_client =
     71        (guac_kubernetes_client*) client->data;
     72
     73    /* Do not handle any further events if connection is closing */
     74    if (client->state != GUAC_CLIENT_RUNNING) {
     75#ifdef HAVE_LWS_CALLBACK_HTTP_DUMMY
     76        return lws_callback_http_dummy(wsi, reason, user, in, length);
     77#else
     78        return 0;
     79#endif
     80    }
     81
     82    switch (reason) {
     83
     84        /* Complete initialization of SSL */
     85        case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
     86            guac_kubernetes_init_ssl(client, (SSL_CTX*) user);
     87            break;
     88
     89        /* Failed to connect */
     90        case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
     91            guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
     92                    "Error connecting to Kubernetes server: %s",
     93                    in != NULL ? (char*) in : "(no error description "
     94                    "available)");
     95            break;
     96
     97        /* Connected / logged in */
     98        case LWS_CALLBACK_CLIENT_ESTABLISHED:
     99            guac_client_log(client, GUAC_LOG_INFO,
    100                    "Kubernetes connection successful.");
    101
    102            /* Allow terminal to render */
    103            guac_terminal_start(kubernetes_client->term);
    104
    105            /* Schedule check for pending messages in case messages were added
    106             * to the outbound message buffer prior to the connection being
    107             * fully established */
    108            lws_callback_on_writable(wsi);
    109            break;
    110
    111        /* Data received via WebSocket */
    112        case LWS_CALLBACK_CLIENT_RECEIVE:
    113            guac_kubernetes_receive_data(client, (const char*) in, length);
    114            break;
    115
    116        /* WebSocket is ready for writing */
    117        case LWS_CALLBACK_CLIENT_WRITEABLE:
    118
    119            /* Send any pending messages, requesting another callback if
    120             * yet more messages remain */
    121            if (guac_kubernetes_write_pending_message(client))
    122                lws_callback_on_writable(wsi);
    123            break;
    124
    125#ifdef HAVE_LWS_CALLBACK_CLIENT_CLOSED
    126        /* Connection closed (client-specific) */
    127        case LWS_CALLBACK_CLIENT_CLOSED:
    128#endif
    129
    130        /* Connection closed */
    131        case LWS_CALLBACK_WSI_DESTROY:
    132        case LWS_CALLBACK_CLOSED:
    133            guac_client_stop(client);
    134            guac_client_log(client, GUAC_LOG_DEBUG, "WebSocket connection to "
    135                    "Kubernetes server closed.");
    136            break;
    137
    138        /* No other event types are applicable */
    139        default:
    140            break;
    141
    142    }
    143
    144#ifdef HAVE_LWS_CALLBACK_HTTP_DUMMY
    145    return lws_callback_http_dummy(wsi, reason, user, in, length);
    146#else
    147    return 0;
    148#endif
    149
    150}
    151
    152/**
    153 * List of all WebSocket protocols which should be declared as supported by
    154 * libwebsockets during the initial WebSocket handshake, along with
    155 * corresponding event-handling callbacks.
    156 */
    157struct lws_protocols guac_kubernetes_lws_protocols[] = {
    158    {
    159        .name = GUAC_KUBERNETES_LWS_PROTOCOL,
    160        .callback = guac_kubernetes_lws_callback
    161    },
    162    { 0 }
    163};
    164
    165/**
    166 * Input thread, started by the main Kubernetes client thread. This thread
    167 * continuously reads from the terminal's STDIN and transfers all read
    168 * data to the Kubernetes connection.
    169 *
    170 * @param data
    171 *     The current guac_client instance.
    172 *
    173 * @return
    174 *     Always NULL.
    175 */
    176static void* guac_kubernetes_input_thread(void* data) {
    177
    178    guac_client* client = (guac_client*) data;
    179    guac_kubernetes_client* kubernetes_client =
    180        (guac_kubernetes_client*) client->data;
    181
    182    char buffer[GUAC_KUBERNETES_MAX_MESSAGE_SIZE];
    183    int bytes_read;
    184
    185    /* Write all data read */
    186    while ((bytes_read = guac_terminal_read_stdin(kubernetes_client->term, buffer, sizeof(buffer))) > 0) {
    187
    188        /* Send received data to Kubernetes along STDIN channel */
    189        guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_STDIN,
    190                buffer, bytes_read);
    191
    192    }
    193
    194    return NULL;
    195
    196}
    197
    198void* guac_kubernetes_client_thread(void* data) {
    199
    200    guac_client* client = (guac_client*) data;
    201    guac_kubernetes_client* kubernetes_client =
    202        (guac_kubernetes_client*) client->data;
    203
    204    guac_kubernetes_settings* settings = kubernetes_client->settings;
    205
    206    pthread_t input_thread;
    207    char endpoint_path[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
    208
    209    /* Verify that the pod name was specified (it's always required) */
    210    if (settings->kubernetes_pod == NULL) {
    211        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    212                "The name of the Kubernetes pod is a required parameter.");
    213        goto fail;
    214    }
    215
    216    /* Generate endpoint for attachment URL */
    217    if (guac_kubernetes_endpoint_uri(endpoint_path, sizeof(endpoint_path),
    218                settings->kubernetes_namespace,
    219                settings->kubernetes_pod,
    220                settings->kubernetes_container,
    221                settings->exec_command)) {
    222        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    223                "Unable to generate path for Kubernetes API endpoint: "
    224                "Resulting path too long");
    225        goto fail;
    226    }
    227
    228    guac_client_log(client, GUAC_LOG_DEBUG, "The endpoint for attaching to "
    229            "the requested Kubernetes pod is \"%s\".", endpoint_path);
    230
    231    /* Set up screen recording, if requested */
    232    if (settings->recording_path != NULL) {
    233        kubernetes_client->recording = guac_recording_create(client,
    234                settings->recording_path,
    235                settings->recording_name,
    236                settings->create_recording_path,
    237                !settings->recording_exclude_output,
    238                !settings->recording_exclude_mouse,
    239                0, /* Touch events not supported */
    240                settings->recording_include_keys);
    241    }
    242
    243    /* Create terminal options with required parameters */
    244    guac_terminal_options* options = guac_terminal_options_create(
    245            settings->width, settings->height, settings->resolution);
    246
    247    /* Set optional parameters */
    248    options->disable_copy = settings->disable_copy;
    249    options->max_scrollback = settings->max_scrollback;
    250    options->font_name = settings->font_name;
    251    options->font_size = settings->font_size;
    252    options->color_scheme = settings->color_scheme;
    253    options->backspace = settings->backspace;
    254
    255    /* Create terminal */
    256    kubernetes_client->term = guac_terminal_create(client, options);
    257
    258    /* Free options struct now that it's been used */
    259    guac_mem_free(options);
    260
    261    /* Fail if terminal init failed */
    262    if (kubernetes_client->term == NULL) {
    263        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    264                "Terminal initialization failed");
    265        goto fail;
    266    }
    267
    268    /* Send current values of exposed arguments to owner only */
    269    guac_client_for_owner(client, guac_kubernetes_send_current_argv,
    270            kubernetes_client);
    271
    272    /* Set up typescript, if requested */
    273    if (settings->typescript_path != NULL) {
    274        guac_terminal_create_typescript(kubernetes_client->term,
    275                settings->typescript_path,
    276                settings->typescript_name,
    277                settings->create_typescript_path);
    278    }
    279
    280    /* Init libwebsockets context creation parameters */
    281    struct lws_context_creation_info context_info = {
    282        .port = CONTEXT_PORT_NO_LISTEN, /* We are not a WebSocket server */
    283        .uid = -1,
    284        .gid = -1,
    285        .protocols = guac_kubernetes_lws_protocols,
    286        .user = client
    287    };
    288
    289    /* Init WebSocket connection parameters which do not vary by Guacmaole
    290     * connection parameters or creation of future libwebsockets objects */
    291    struct lws_client_connect_info connection_info = {
    292        .host = settings->hostname,
    293        .address = settings->hostname,
    294        .origin = settings->hostname,
    295        .port = settings->port,
    296        .protocol = GUAC_KUBERNETES_LWS_PROTOCOL,
    297        .userdata = client
    298    };
    299
    300    /* If requested, use an SSL/TLS connection for communication with
    301     * Kubernetes. Note that we disable hostname checks here because we
    302     * do our own validation - libwebsockets does not validate properly if
    303     * IP addresses are used. */
    304    if (settings->use_ssl) {
    305#ifdef HAVE_LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT
    306        context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
    307#endif
    308#ifdef HAVE_LCCSCF_USE_SSL
    309        connection_info.ssl_connection = LCCSCF_USE_SSL
    310            | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
    311#else
    312        connection_info.ssl_connection = 2; /* SSL + no hostname check */
    313#endif
    314    }
    315
    316    /* Create libwebsockets context */
    317    kubernetes_client->context = lws_create_context(&context_info);
    318    if (!kubernetes_client->context) {
    319        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    320                "Initialization of libwebsockets failed");
    321        goto fail;
    322    }
    323
    324    /* Generate path dynamically */
    325    connection_info.context = kubernetes_client->context;
    326    connection_info.path = endpoint_path;
    327
    328    /* Open WebSocket connection to Kubernetes */
    329    kubernetes_client->wsi = lws_client_connect_via_info(&connection_info);
    330    if (kubernetes_client->wsi == NULL) {
    331        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    332                "Connection via libwebsockets failed");
    333        goto fail;
    334    }
    335
    336    /* Init outbound message buffer */
    337    pthread_mutex_init(&(kubernetes_client->outbound_message_lock), NULL);
    338
    339    /* Start input thread */
    340    if (pthread_create(&(input_thread), NULL, guac_kubernetes_input_thread, (void*) client)) {
    341        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread");
    342        goto fail;
    343    }
    344
    345    /* Force a redraw of the attached display (there will be no content
    346     * otherwise, given the stream nature of attaching to a running
    347     * container) */
    348    guac_kubernetes_force_redraw(client);
    349
    350    /* As long as client is connected, continue polling libwebsockets */
    351    while (client->state == GUAC_CLIENT_RUNNING) {
    352
    353        /* Cease polling libwebsockets if an error condition is signalled */
    354        if (lws_service(kubernetes_client->context,
    355                    GUAC_KUBERNETES_SERVICE_INTERVAL) < 0)
    356            break;
    357
    358    }
    359
    360    /* Kill client and Wait for input thread to die */
    361    guac_terminal_stop(kubernetes_client->term);
    362    guac_client_stop(client);
    363    pthread_join(input_thread, NULL);
    364
    365fail:
    366
    367    /* Kill and free terminal, if allocated */
    368    if (kubernetes_client->term != NULL)
    369        guac_terminal_free(kubernetes_client->term);
    370
    371    /* Clean up recording, if in progress */
    372    if (kubernetes_client->recording != NULL)
    373        guac_recording_free(kubernetes_client->recording);
    374
    375    /* Free WebSocket context if successfully allocated */
    376    if (kubernetes_client->context != NULL)
    377        lws_context_destroy(kubernetes_client->context);
    378
    379    guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended.");
    380    return NULL;
    381
    382}
    383
    384void guac_kubernetes_resize(guac_client* client, int rows, int columns) {
    385
    386    char buffer[64];
    387
    388    guac_kubernetes_client* kubernetes_client =
    389        (guac_kubernetes_client*) client->data;
    390
    391    /* Send request only if different from last request */
    392    if (kubernetes_client->rows != rows ||
    393            kubernetes_client->columns != columns) {
    394
    395        kubernetes_client->rows = rows;
    396        kubernetes_client->columns = columns;
    397
    398        /* Construct terminal resize message for Kubernetes */
    399        int length = snprintf(buffer, sizeof(buffer),
    400                "{\"Width\":%i,\"Height\":%i}", columns, rows);
    401
    402        /* Schedule message for sending */
    403        guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_RESIZE,
    404                buffer, length);
    405
    406    }
    407
    408}
    409
    410void guac_kubernetes_force_redraw(guac_client* client) {
    411
    412    guac_kubernetes_client* kubernetes_client =
    413        (guac_kubernetes_client*) client->data;
    414
    415    /* Get current terminal dimensions */
    416    guac_terminal* term = kubernetes_client->term;
    417    int rows = guac_terminal_get_rows(term);
    418    int columns = guac_terminal_get_columns(term);
    419
    420    /* Force a redraw by increasing the terminal size by one character in
    421     * each dimension and then resizing it back to normal (the same technique
    422     * used by kubectl */
    423    guac_kubernetes_resize(client, rows + 1, columns + 1);
    424    guac_kubernetes_resize(client, rows, columns);
    425
    426}
    427