| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| #include "config.h" |
| #include "client.h" |
| #include "common/recording.h" |
| #include "io.h" |
| #include "kubernetes.h" |
| #include "ssl.h" |
| #include "terminal/terminal.h" |
| #include "url.h" |
| |
| #include <guacamole/client.h> |
| #include <guacamole/protocol.h> |
| #include <libwebsockets.h> |
| |
| #include <pthread.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| |
| /** |
| * Callback invoked by libwebsockets for events related to a WebSocket being |
| * used for communicating with an attached Kubernetes pod. |
| * |
| * @param wsi |
| * The libwebsockets handle for the WebSocket connection. |
| * |
| * @param reason |
| * The reason (event) that this callback was invoked. |
| * |
| * @param user |
| * Arbitrary data assocated with the WebSocket session. In some cases, |
| * this is actually event-specific data (such as the |
| * LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERT event). |
| * |
| * @param in |
| * A pointer to arbitrary, reason-specific data. |
| * |
| * @param length |
| * An arbitrary, reason-specific length value. |
| * |
| * @return |
| * An undocumented integer value related the success of handling the |
| * event, or -1 if the WebSocket connection should be closed. |
| */ |
| static int guac_kubernetes_lws_callback(struct lws* wsi, |
| enum lws_callback_reasons reason, void* user, |
| void* in, size_t length) { |
| |
| guac_client* client = guac_kubernetes_lws_current_client; |
| guac_kubernetes_client* kubernetes_client = |
| (guac_kubernetes_client*) client->data; |
| |
| /* Do not handle any further events if connection is closing */ |
| if (client->state != GUAC_CLIENT_RUNNING) { |
| #ifdef HAVE_LWS_CALLBACK_HTTP_DUMMY |
| return lws_callback_http_dummy(wsi, reason, user, in, length); |
| #else |
| return 0; |
| #endif |
| } |
| |
| switch (reason) { |
| |
| /* Complete initialization of SSL */ |
| case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS: |
| guac_kubernetes_init_ssl(client, (SSL_CTX*) user); |
| break; |
| |
| /* Failed to connect */ |
| case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: |
| guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, |
| "Error connecting to Kubernetes server: %s", |
| in != NULL ? (char*) in : "(no error description " |
| "available)"); |
| break; |
| |
| /* Connected / logged in */ |
| case LWS_CALLBACK_CLIENT_ESTABLISHED: |
| guac_client_log(client, GUAC_LOG_INFO, |
| "Kubernetes connection successful."); |
| |
| /* Allow terminal to render */ |
| guac_terminal_start(kubernetes_client->term); |
| |
| /* Schedule check for pending messages in case messages were added |
| * to the outbound message buffer prior to the connection being |
| * fully established */ |
| lws_callback_on_writable(wsi); |
| break; |
| |
| /* Data received via WebSocket */ |
| case LWS_CALLBACK_CLIENT_RECEIVE: |
| guac_kubernetes_receive_data(client, (const char*) in, length); |
| break; |
| |
| /* WebSocket is ready for writing */ |
| case LWS_CALLBACK_CLIENT_WRITEABLE: |
| |
| /* Send any pending messages, requesting another callback if |
| * yet more messages remain */ |
| if (guac_kubernetes_write_pending_message(client)) |
| lws_callback_on_writable(wsi); |
| break; |
| |
| #ifdef HAVE_LWS_CALLBACK_CLIENT_CLOSED |
| /* Connection closed (client-specific) */ |
| case LWS_CALLBACK_CLIENT_CLOSED: |
| #endif |
| |
| /* Connection closed */ |
| case LWS_CALLBACK_WSI_DESTROY: |
| case LWS_CALLBACK_CLOSED: |
| guac_client_stop(client); |
| guac_client_log(client, GUAC_LOG_DEBUG, "WebSocket connection to " |
| "Kubernetes server closed."); |
| break; |
| |
| /* No other event types are applicable */ |
| default: |
| break; |
| |
| } |
| |
| #ifdef HAVE_LWS_CALLBACK_HTTP_DUMMY |
| return lws_callback_http_dummy(wsi, reason, user, in, length); |
| #else |
| return 0; |
| #endif |
| |
| } |
| |
| /** |
| * List of all WebSocket protocols which should be declared as supported by |
| * libwebsockets during the initial WebSocket handshake, along with |
| * corresponding event-handling callbacks. |
| */ |
| struct lws_protocols guac_kubernetes_lws_protocols[] = { |
| { |
| .name = GUAC_KUBERNETES_LWS_PROTOCOL, |
| .callback = guac_kubernetes_lws_callback |
| }, |
| { 0 } |
| }; |
| |
| /** |
| * Input thread, started by the main Kubernetes client thread. This thread |
| * continuously reads from the terminal's STDIN and transfers all read |
| * data to the Kubernetes connection. |
| * |
| * @param data |
| * The current guac_client instance. |
| * |
| * @return |
| * Always NULL. |
| */ |
| static void* guac_kubernetes_input_thread(void* data) { |
| |
| guac_client* client = (guac_client*) data; |
| guac_kubernetes_client* kubernetes_client = |
| (guac_kubernetes_client*) client->data; |
| |
| char buffer[GUAC_KUBERNETES_MAX_MESSAGE_SIZE]; |
| int bytes_read; |
| |
| /* Write all data read */ |
| while ((bytes_read = guac_terminal_read_stdin(kubernetes_client->term, buffer, sizeof(buffer))) > 0) { |
| |
| /* Send received data to Kubernetes along STDIN channel */ |
| guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_STDIN, |
| buffer, bytes_read); |
| |
| } |
| |
| return NULL; |
| |
| } |
| |
| void* guac_kubernetes_client_thread(void* data) { |
| |
| guac_client* client = (guac_client*) data; |
| guac_kubernetes_client* kubernetes_client = |
| (guac_kubernetes_client*) client->data; |
| |
| guac_kubernetes_settings* settings = kubernetes_client->settings; |
| |
| pthread_t input_thread; |
| char endpoint_path[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; |
| |
| /* Verify that the pod name was specified (it's always required) */ |
| if (settings->kubernetes_pod == NULL) { |
| guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, |
| "The name of the Kubernetes pod is a required parameter."); |
| goto fail; |
| } |
| |
| /* Generate endpoint for attachment URL */ |
| if (guac_kubernetes_endpoint_attach(endpoint_path, sizeof(endpoint_path), |
| settings->kubernetes_namespace, |
| settings->kubernetes_pod, |
| settings->kubernetes_container)) { |
| guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, |
| "Unable to generate path for Kubernetes API endpoint: " |
| "Resulting path too long"); |
| goto fail; |
| } |
| |
| guac_client_log(client, GUAC_LOG_DEBUG, "The endpoint for attaching to " |
| "the requested Kubernetes pod is \"%s\".", endpoint_path); |
| |
| /* Set up screen recording, if requested */ |
| if (settings->recording_path != NULL) { |
| kubernetes_client->recording = guac_common_recording_create(client, |
| settings->recording_path, |
| settings->recording_name, |
| settings->create_recording_path, |
| !settings->recording_exclude_output, |
| !settings->recording_exclude_mouse, |
| settings->recording_include_keys); |
| } |
| |
| /* Create terminal */ |
| kubernetes_client->term = guac_terminal_create(client, |
| kubernetes_client->clipboard, settings->disable_copy, |
| settings->max_scrollback, settings->font_name, settings->font_size, |
| settings->resolution, settings->width, settings->height, |
| settings->color_scheme, settings->backspace); |
| |
| /* Fail if terminal init failed */ |
| if (kubernetes_client->term == NULL) { |
| guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, |
| "Terminal initialization failed"); |
| goto fail; |
| } |
| |
| /* Set up typescript, if requested */ |
| if (settings->typescript_path != NULL) { |
| guac_terminal_create_typescript(kubernetes_client->term, |
| settings->typescript_path, |
| settings->typescript_name, |
| settings->create_typescript_path); |
| } |
| |
| /* Init libwebsockets context creation parameters */ |
| struct lws_context_creation_info context_info = { |
| .port = CONTEXT_PORT_NO_LISTEN, /* We are not a WebSocket server */ |
| .uid = -1, |
| .gid = -1, |
| .protocols = guac_kubernetes_lws_protocols, |
| .user = client |
| }; |
| |
| /* Init WebSocket connection parameters which do not vary by Guacmaole |
| * connection parameters or creation of future libwebsockets objects */ |
| struct lws_client_connect_info connection_info = { |
| .host = settings->hostname, |
| .address = settings->hostname, |
| .origin = settings->hostname, |
| .port = settings->port, |
| .protocol = GUAC_KUBERNETES_LWS_PROTOCOL, |
| .userdata = client |
| }; |
| |
| /* If requested, use an SSL/TLS connection for communication with |
| * Kubernetes. Note that we disable hostname checks here because we |
| * do our own validation - libwebsockets does not validate properly if |
| * IP addresses are used. */ |
| if (settings->use_ssl) { |
| #ifdef HAVE_LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
| context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; |
| #endif |
| #ifdef HAVE_LCCSCF_USE_SSL |
| connection_info.ssl_connection = LCCSCF_USE_SSL |
| | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; |
| #else |
| connection_info.ssl_connection = 2; /* SSL + no hostname check */ |
| #endif |
| } |
| |
| /* Create libwebsockets context */ |
| kubernetes_client->context = lws_create_context(&context_info); |
| if (!kubernetes_client->context) { |
| guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, |
| "Initialization of libwebsockets failed"); |
| goto fail; |
| } |
| |
| /* Generate path dynamically */ |
| connection_info.context = kubernetes_client->context; |
| connection_info.path = endpoint_path; |
| |
| /* Open WebSocket connection to Kubernetes */ |
| kubernetes_client->wsi = lws_client_connect_via_info(&connection_info); |
| if (kubernetes_client->wsi == NULL) { |
| guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, |
| "Connection via libwebsockets failed"); |
| goto fail; |
| } |
| |
| /* Init outbound message buffer */ |
| pthread_mutex_init(&(kubernetes_client->outbound_message_lock), NULL); |
| |
| /* Start input thread */ |
| if (pthread_create(&(input_thread), NULL, guac_kubernetes_input_thread, (void*) client)) { |
| guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread"); |
| goto fail; |
| } |
| |
| /* Force a redraw of the attached display (there will be no content |
| * otherwise, given the stream nature of attaching to a running |
| * container) */ |
| guac_kubernetes_force_redraw(client); |
| |
| /* As long as client is connected, continue polling libwebsockets */ |
| while (client->state == GUAC_CLIENT_RUNNING) { |
| |
| /* Cease polling libwebsockets if an error condition is signalled */ |
| if (lws_service(kubernetes_client->context, |
| GUAC_KUBERNETES_SERVICE_INTERVAL) < 0) |
| break; |
| |
| } |
| |
| /* Kill client and Wait for input thread to die */ |
| guac_terminal_stop(kubernetes_client->term); |
| guac_client_stop(client); |
| pthread_join(input_thread, NULL); |
| |
| fail: |
| |
| /* Kill and free terminal, if allocated */ |
| if (kubernetes_client->term != NULL) |
| guac_terminal_free(kubernetes_client->term); |
| |
| /* Clean up recording, if in progress */ |
| if (kubernetes_client->recording != NULL) |
| guac_common_recording_free(kubernetes_client->recording); |
| |
| /* Free WebSocket context if successfully allocated */ |
| if (kubernetes_client->context != NULL) |
| lws_context_destroy(kubernetes_client->context); |
| |
| guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended."); |
| return NULL; |
| |
| } |
| |
| void guac_kubernetes_resize(guac_client* client, int rows, int columns) { |
| |
| char buffer[64]; |
| |
| guac_kubernetes_client* kubernetes_client = |
| (guac_kubernetes_client*) client->data; |
| |
| /* Send request only if different from last request */ |
| if (kubernetes_client->rows != rows || |
| kubernetes_client->columns != columns) { |
| |
| kubernetes_client->rows = rows; |
| kubernetes_client->columns = columns; |
| |
| /* Construct terminal resize message for Kubernetes */ |
| int length = snprintf(buffer, sizeof(buffer), |
| "{\"Width\":%i,\"Height\":%i}", columns, rows); |
| |
| /* Schedule message for sending */ |
| guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_RESIZE, |
| buffer, length); |
| |
| } |
| |
| } |
| |
| void guac_kubernetes_force_redraw(guac_client* client) { |
| |
| guac_kubernetes_client* kubernetes_client = |
| (guac_kubernetes_client*) client->data; |
| |
| /* Get current terminal dimensions */ |
| guac_terminal* term = kubernetes_client->term; |
| int rows = term->term_height; |
| int columns = term->term_width; |
| |
| /* Force a redraw by increasing the terminal size by one character in |
| * each dimension and then resizing it back to normal (the same technique |
| * used by kubectl */ |
| guac_kubernetes_resize(client, rows + 1, columns + 1); |
| guac_kubernetes_resize(client, rows, columns); |
| |
| } |
| |