/* * 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 "argv.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 #include #include #include #include #include /** * 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_uri(endpoint_path, sizeof(endpoint_path), settings->kubernetes_namespace, settings->kubernetes_pod, settings->kubernetes_container, settings->exec_command)) { 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, 0, /* Touch events not supported */ 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; } /* Send current values of exposed arguments to owner only */ guac_client_for_owner(client, guac_kubernetes_send_current_argv, kubernetes_client); /* 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); }