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