From b8bd0e4c6a63995c18050fa4f88fa09b97f7a90c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 9 Sep 2018 20:03:40 -0700 Subject: [PATCH 01/18] GUACAMOLE-623: Add base skeleton for Kubernetes protocol support. --- Makefile.am | 4 + configure.ac | 60 ++++- src/protocols/kubernetes/Makefile.am | 57 ++++ src/protocols/kubernetes/client.c | 91 +++++++ src/protocols/kubernetes/client.h | 41 +++ src/protocols/kubernetes/clipboard.c | 67 +++++ src/protocols/kubernetes/clipboard.h | 43 +++ src/protocols/kubernetes/input.c | 95 +++++++ src/protocols/kubernetes/input.h | 46 ++++ src/protocols/kubernetes/kubernetes.c | 136 ++++++++++ src/protocols/kubernetes/kubernetes.h | 71 +++++ src/protocols/kubernetes/pipe.c | 51 ++++ src/protocols/kubernetes/pipe.h | 42 +++ src/protocols/kubernetes/settings.c | 366 ++++++++++++++++++++++++++ src/protocols/kubernetes/settings.h | 256 ++++++++++++++++++ src/protocols/kubernetes/user.c | 116 ++++++++ src/protocols/kubernetes/user.h | 38 +++ 17 files changed, 1572 insertions(+), 8 deletions(-) create mode 100644 src/protocols/kubernetes/Makefile.am create mode 100644 src/protocols/kubernetes/client.c create mode 100644 src/protocols/kubernetes/client.h create mode 100644 src/protocols/kubernetes/clipboard.c create mode 100644 src/protocols/kubernetes/clipboard.h create mode 100644 src/protocols/kubernetes/input.c create mode 100644 src/protocols/kubernetes/input.h create mode 100644 src/protocols/kubernetes/kubernetes.c create mode 100644 src/protocols/kubernetes/kubernetes.h create mode 100644 src/protocols/kubernetes/pipe.c create mode 100644 src/protocols/kubernetes/pipe.h create mode 100644 src/protocols/kubernetes/settings.c create mode 100644 src/protocols/kubernetes/settings.h create mode 100644 src/protocols/kubernetes/user.c create mode 100644 src/protocols/kubernetes/user.h diff --git a/Makefile.am b/Makefile.am index e9233760..91c8abec 100644 --- a/Makefile.am +++ b/Makefile.am @@ -52,6 +52,10 @@ if ENABLE_PULSE SUBDIRS += src/pulse endif +if ENABLE_KUBERNETES +SUBDIRS += src/protocols/kubernetes +endif + if ENABLE_RDP SUBDIRS += src/protocols/rdp endif diff --git a/configure.ac b/configure.ac index 6b20c974..ae783244 100644 --- a/configure.ac +++ b/configure.ac @@ -1172,6 +1172,46 @@ fi AM_CONDITIONAL([ENABLE_WEBP], [test "x${have_webp}" = "xyes"]) AC_SUBST(WEBP_LIBS) +# +# libwebsockets +# + +have_libwebsockets=disabled +WEBSOCKETS_LIBS= +AC_ARG_WITH([websockets], + [AS_HELP_STRING([--with-websockets], + [support WebSockets @<:@default=check@:>@])], + [], + [with_websockets=check]) + +if test "x$with_websockets" != "xno" +then + have_libwebsockets=yes + AC_CHECK_LIB([websockets], + [lws_create_context], + [WEBSOCKETS_LIBS="$WEBSOCKETS_LIBS -lwebsockets"], + [have_libwebsockets=no]) +fi + +AM_CONDITIONAL([ENABLE_WEBSOCKETS], + [test "x${have_libwebsockets}" = "xyes"]) + +AC_SUBST(WEBSOCKETS_LIBS) + +# +# Kubernetes +# + +AC_ARG_ENABLE([kubernetes], + [AS_HELP_STRING([--disable-kubernetes], + [do not build support for attaching to Kubernetes pods])], + [], + [enable_kubernetes=yes]) + +AM_CONDITIONAL([ENABLE_KUBERNETES], [test "x${enable_kubernetes}" = "xyes" \ + -a "x${have_libwebsockets}" = "xyes" \ + -a "x${have_terminal}" = "xyes"]) + # # guacd # @@ -1230,6 +1270,7 @@ AC_CONFIG_FILES([Makefile src/guaclog/Makefile src/guaclog/man/guaclog.1 src/pulse/Makefile + src/protocols/kubernetes/Makefile src/protocols/rdp/Makefile src/protocols/ssh/Makefile src/protocols/telnet/Makefile @@ -1240,10 +1281,11 @@ AC_OUTPUT # Protocol build status # -AM_COND_IF([ENABLE_RDP], [build_rdp=yes], [build_rdp=no]) -AM_COND_IF([ENABLE_SSH], [build_ssh=yes], [build_ssh=no]) -AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no]) -AM_COND_IF([ENABLE_VNC], [build_vnc=yes], [build_vnc=no]) +AM_COND_IF([ENABLE_KUBERNETES], [build_kubernetes=yes], [build_kubernetes=no]) +AM_COND_IF([ENABLE_RDP], [build_rdp=yes], [build_rdp=no]) +AM_COND_IF([ENABLE_SSH], [build_ssh=yes], [build_ssh=no]) +AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no]) +AM_COND_IF([ENABLE_VNC], [build_vnc=yes], [build_vnc=no]) # # Service / tool build status @@ -1287,15 +1329,17 @@ $PACKAGE_NAME version $PACKAGE_VERSION libVNCServer ........ ${have_libvncserver} libvorbis ........... ${have_vorbis} libpulse ............ ${have_pulse} + libwebsockets ....... ${have_websockets} libwebp ............. ${have_webp} wsock32 ............. ${have_winsock} Protocol support: - RDP ....... ${build_rdp} - SSH ....... ${build_ssh} - Telnet .... ${build_telnet} - VNC ....... ${build_vnc} + Kubernetes .... ${build_kubernetes} + RDP ........... ${build_rdp} + SSH ........... ${build_ssh} + Telnet ........ ${build_telnet} + VNC ........... ${build_vnc} Services / tools: diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am new file mode 100644 index 00000000..d864967f --- /dev/null +++ b/src/protocols/kubernetes/Makefile.am @@ -0,0 +1,57 @@ +# +# 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. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +lib_LTLIBRARIES = libguac-client-kubernetes.la + +libguac_client_kubernetes_la_SOURCES = \ + client.c \ + clipboard.c \ + input.c \ + pipe.c \ + settings.c \ + kubernetes.c \ + user.c + +noinst_HEADERS = \ + client.h \ + clipboard.h \ + input.h \ + pipe.h \ + settings.h \ + kubernetes.h \ + user.h + +libguac_client_kubernetes_la_CFLAGS = \ + -Werror -Wall -Iinclude \ + @LIBGUAC_INCLUDE@ \ + @TERMINAL_INCLUDE@ + +libguac_client_kubernetes_la_LIBADD = \ + @COMMON_LTLIB@ \ + @LIBGUAC_LTLIB@ \ + @TERMINAL_LTLIB@ + +libguac_client_kubernetes_la_LDFLAGS = \ + -version-info 0:0:0 \ + @PTHREAD_LIBS@ \ + @WEBSOCKETS_LIBS@ + diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c new file mode 100644 index 00000000..1b5d175d --- /dev/null +++ b/src/protocols/kubernetes/client.c @@ -0,0 +1,91 @@ +/* + * 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 "kubernetes.h" +#include "settings.h" +#include "terminal/terminal.h" +#include "user.h" + +#include +#include +#include +#include +#include + +#include + +int guac_client_init(guac_client* client) { + + /* Set client args */ + client->args = GUAC_KUBERNETES_CLIENT_ARGS; + + /* Allocate client instance data */ + guac_kubernetes_client* kubernetes_client = calloc(1, sizeof(guac_kubernetes_client)); + client->data = kubernetes_client; + + /* Init clipboard */ + kubernetes_client->clipboard = guac_common_clipboard_alloc(GUAC_KUBERNETES_CLIPBOARD_MAX_LENGTH); + + /* Set handlers */ + client->join_handler = guac_kubernetes_user_join_handler; + client->free_handler = guac_kubernetes_client_free_handler; + + /* Set locale and warn if not UTF-8 */ + setlocale(LC_CTYPE, ""); + if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) { + guac_client_log(client, GUAC_LOG_INFO, + "Current locale does not use UTF-8. Some characters may " + "not render correctly."); + } + + /* Success */ + return 0; + +} + +int guac_kubernetes_client_free_handler(guac_client* client) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Clean up recording, if in progress */ + if (kubernetes_client->recording != NULL) + guac_common_recording_free(kubernetes_client->recording); + + /* Kill terminal */ + guac_terminal_free(kubernetes_client->term); + + /* TODO: Wait for and free WebSocket session, if connected */ + /*if (kubernetes_client->websocket != NULL) { + pthread_join(kubernetes_client->client_thread, NULL); + }*/ + + /* Free settings */ + if (kubernetes_client->settings != NULL) + guac_kubernetes_settings_free(kubernetes_client->settings); + + guac_common_clipboard_free(kubernetes_client->clipboard); + free(kubernetes_client); + return 0; + +} + diff --git a/src/protocols/kubernetes/client.h b/src/protocols/kubernetes/client.h new file mode 100644 index 00000000..2e96d109 --- /dev/null +++ b/src/protocols/kubernetes/client.h @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_CLIENT_H +#define GUAC_KUBERNETES_CLIENT_H + +#include "config.h" +#include "terminal/terminal.h" + +#include +#include + +/** + * The maximum number of bytes to allow within the clipboard. + */ +#define GUAC_KUBERNETES_CLIPBOARD_MAX_LENGTH 262144 + +/** + * Free handler. Required by libguac and called when the guac_client is + * disconnected and must be cleaned up. + */ +guac_client_free_handler guac_kubernetes_client_free_handler; + +#endif + diff --git a/src/protocols/kubernetes/clipboard.c b/src/protocols/kubernetes/clipboard.c new file mode 100644 index 00000000..87a34b04 --- /dev/null +++ b/src/protocols/kubernetes/clipboard.c @@ -0,0 +1,67 @@ +/* + * 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 "clipboard.h" +#include "common/clipboard.h" +#include "kubernetes.h" +#include "terminal/terminal.h" + +#include +#include +#include + +int guac_kubernetes_clipboard_handler(guac_user* user, guac_stream* stream, + char* mimetype) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Clear clipboard and prepare for new data */ + guac_common_clipboard_reset(kubernetes_client->clipboard, mimetype); + + /* Set handlers for clipboard stream */ + stream->blob_handler = guac_kubernetes_clipboard_blob_handler; + stream->end_handler = guac_kubernetes_clipboard_end_handler; + + return 0; +} + +int guac_kubernetes_clipboard_blob_handler(guac_user* user, + guac_stream* stream, void* data, int length) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Append new data */ + guac_common_clipboard_append(kubernetes_client->clipboard, data, length); + + return 0; +} + +int guac_kubernetes_clipboard_end_handler(guac_user* user, + guac_stream* stream) { + + /* Nothing to do - clipboard is implemented within client */ + + return 0; +} + diff --git a/src/protocols/kubernetes/clipboard.h b/src/protocols/kubernetes/clipboard.h new file mode 100644 index 00000000..009219cf --- /dev/null +++ b/src/protocols/kubernetes/clipboard.h @@ -0,0 +1,43 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_CLIPBOARD_H +#define GUAC_KUBERNETES_CLIPBOARD_H + +#include "config.h" + +#include + +/** + * Handler for inbound clipboard streams. + */ +guac_user_clipboard_handler guac_kubernetes_clipboard_handler; + +/** + * Handler for data received along clipboard streams. + */ +guac_user_blob_handler guac_kubernetes_clipboard_blob_handler; + +/** + * Handler for end-of-stream related to clipboard. + */ +guac_user_end_handler guac_kubernetes_clipboard_end_handler; + +#endif + diff --git a/src/protocols/kubernetes/input.c b/src/protocols/kubernetes/input.c new file mode 100644 index 00000000..9bf5b717 --- /dev/null +++ b/src/protocols/kubernetes/input.c @@ -0,0 +1,95 @@ +/* + * 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 "common/recording.h" +#include "kubernetes.h" +#include "input.h" +#include "terminal/terminal.h" + +#include +#include + +#include +#include +#include + +int guac_kubernetes_user_mouse_handler(guac_user* user, + int x, int y, int mask) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Skip if terminal not yet ready */ + guac_terminal* term = kubernetes_client->term; + if (term == NULL) + return 0; + + /* Report mouse position within recording */ + if (kubernetes_client->recording != NULL) + guac_common_recording_report_mouse(kubernetes_client->recording, x, y, + mask); + + guac_terminal_send_mouse(term, user, x, y, mask); + return 0; + +} + +int guac_kubernetes_user_key_handler(guac_user* user, int keysym, int pressed) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Report key state within recording */ + if (kubernetes_client->recording != NULL) + guac_common_recording_report_key(kubernetes_client->recording, + keysym, pressed); + + /* Skip if terminal not yet ready */ + guac_terminal* term = kubernetes_client->term; + if (term == NULL) + return 0; + + guac_terminal_send_key(term, keysym, pressed); + return 0; + +} + +int guac_kubernetes_user_size_handler(guac_user* user, int width, int height) { + + /* Get terminal */ + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Skip if terminal not yet ready */ + guac_terminal* terminal = kubernetes_client->term; + if (terminal == NULL) + return 0; + + /* Resize terminal */ + guac_terminal_resize(terminal, width, height); + + /* TODO: Update Kubernetes terminal window size if connected */ + + return 0; +} + diff --git a/src/protocols/kubernetes/input.h b/src/protocols/kubernetes/input.h new file mode 100644 index 00000000..ac65835c --- /dev/null +++ b/src/protocols/kubernetes/input.h @@ -0,0 +1,46 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_INPUT_H +#define GUAC_KUBERNETES_INPUT_H + +#include "config.h" + +#include + +/** + * Handler for key events. Required by libguac and called whenever key events + * are received. + */ +guac_user_key_handler guac_kubernetes_user_key_handler; + +/** + * Handler for mouse events. Required by libguac and called whenever mouse + * events are received. + */ +guac_user_mouse_handler guac_kubernetes_user_mouse_handler; + +/** + * Handler for size events. Required by libguac and called whenever the remote + * display (window) is resized. + */ +guac_user_size_handler guac_kubernetes_user_size_handler; + +#endif + diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c new file mode 100644 index 00000000..231d78d3 --- /dev/null +++ b/src/protocols/kubernetes/kubernetes.c @@ -0,0 +1,136 @@ +/* + * 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 "common/recording.h" +#include "kubernetes.h" +#include "terminal/terminal.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * 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[8192]; + int bytes_read; + + /* Write all data read */ + while ((bytes_read = guac_terminal_read_stdin(kubernetes_client->term, buffer, sizeof(buffer))) > 0) { + + /* TODO: Send to Kubernetes */ + guac_terminal_write(kubernetes_client->term, 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; + + /* 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->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"); + return NULL; + } + + /* 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); + } + + /* TODO: Open WebSocket connection to Kubernetes */ + + /* Logged in */ + guac_client_log(client, GUAC_LOG_INFO, + "Kubernetes connection successful."); + + /* 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"); + return NULL; + } + + /* TODO: While data available, write to terminal */ + + /* Kill client and Wait for input thread to die */ + guac_client_stop(client); + pthread_join(input_thread, NULL); + + guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended."); + return NULL; + +} + diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h new file mode 100644 index 00000000..f8035ae5 --- /dev/null +++ b/src/protocols/kubernetes/kubernetes.h @@ -0,0 +1,71 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_H +#define GUAC_KUBERNETES_H + +#include "config.h" +#include "common/clipboard.h" +#include "common/recording.h" +#include "settings.h" +#include "terminal/terminal.h" + +#include + +/** + * Kubernetes-specific client data. + */ +typedef struct guac_kubernetes_client { + + /** + * Kubernetes connection settings. + */ + guac_kubernetes_settings* settings; + + /** + * The Kubernetes client thread. + */ + pthread_t client_thread; + + /** + * The current clipboard contents. + */ + guac_common_clipboard* clipboard; + + /** + * The terminal which will render all output from the Kubernetes pod. + */ + guac_terminal* term; + + /** + * The in-progress session recording, or NULL if no recording is in + * progress. + */ + guac_common_recording* recording; + +} guac_kubernetes_client; + +/** + * Main Kubernetes client thread, handling transfer of STDOUT/STDERR of an + * attached Kubernetes pod to STDOUT of the terminal. + */ +void* guac_kubernetes_client_thread(void* data); + +#endif + diff --git a/src/protocols/kubernetes/pipe.c b/src/protocols/kubernetes/pipe.c new file mode 100644 index 00000000..242105b1 --- /dev/null +++ b/src/protocols/kubernetes/pipe.c @@ -0,0 +1,51 @@ +/* + * 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 "kubernetes.h" +#include "pipe.h" +#include "terminal/terminal.h" + +#include +#include +#include + +#include + +int guac_kubernetes_pipe_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* name) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Redirect STDIN if pipe has required name */ + if (strcmp(name, GUAC_KUBERNETES_STDIN_PIPE_NAME) == 0) { + guac_terminal_send_stream(kubernetes_client->term, user, stream); + return 0; + } + + /* No other inbound pipe streams are supported */ + guac_protocol_send_ack(user->socket, stream, "No such input stream.", + GUAC_PROTOCOL_STATUS_RESOURCE_NOT_FOUND); + guac_socket_flush(user->socket); + return 0; + +} + diff --git a/src/protocols/kubernetes/pipe.h b/src/protocols/kubernetes/pipe.h new file mode 100644 index 00000000..7acae3cb --- /dev/null +++ b/src/protocols/kubernetes/pipe.h @@ -0,0 +1,42 @@ +/* + * 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. + */ + + +#ifndef GUAC_KUBERNETES_PIPE_H +#define GUAC_KUBERNETES_PIPE_H + +#include "config.h" + +#include + +/** + * The name reserved for the inbound pipe stream which forces the terminal + * emulator's STDIN to be received from the pipe. + */ +#define GUAC_KUBERNETES_STDIN_PIPE_NAME "STDIN" + +/** + * Handles an incoming stream from a Guacamole "pipe" instruction. If the pipe + * is named "STDIN", the the contents of the pipe stream are redirected to + * STDIN of the terminal emulator for as long as the pipe is open. + */ +guac_user_pipe_handler guac_kubernetes_pipe_handler; + +#endif + diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c new file mode 100644 index 00000000..1f04b401 --- /dev/null +++ b/src/protocols/kubernetes/settings.c @@ -0,0 +1,366 @@ +/* + * 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 "settings.h" + +#include + +#include +#include +#include +#include + +/* Client plugin arguments */ +const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { + "hostname", + "port", + "use-ssl", + "client-cert-file", + "client-key-file", + "ca-cert-file", + "ignore-cert", + "font-name", + "font-size", + "color-scheme", + "typescript-path", + "typescript-name", + "create-typescript-path", + "recording-path", + "recording-name", + "recording-exclude-output", + "recording-exclude-mouse", + "recording-include-keys", + "create-recording-path", + "read-only", + "backspace", + "scrollback", + NULL +}; + +enum KUBERNETES_ARGS_IDX { + + /** + * The hostname to connect to. Required. + */ + IDX_HOSTNAME, + + /** + * The port to connect to. Optional. + */ + IDX_PORT, + + /** + * Whether SSL/TLS should be used. SSL is used by default. + */ + IDX_USE_SSL, + + /** + * The filename of the certificate to use if performing SSL/TLS client + * authentication to authenticate with the Kubernetes server. If omitted, + * SSL client authentication will not be performed. + */ + IDX_CLIENT_CERT_FILE, + + /** + * The filename of the key to use if performing SSL/TLS client + * authentication to authenticate with the Kubernetes server. If omitted, + * SSL client authentication will not be performed. + */ + IDX_CLIENT_KEY_FILE, + + /** + * The filename of the certificate of the certificate authority that signed + * the certificate of the Kubernetes server. + */ + IDX_CA_CERT_FILE, + + /** + * Whether the certificate used by the Kubernetes server for SSL/TLS should + * be ignored if it cannot be validated. + */ + IDX_IGNORE_CERT, + + /** + * The name of the font to use within the terminal. + */ + IDX_FONT_NAME, + + /** + * The size of the font to use within the terminal, in points. + */ + IDX_FONT_SIZE, + + /** + * The color scheme to use, as a series of semicolon-separated color-value + * pairs: "background: ", "foreground: ", or + * "color: ", where is a number from 0 to 255, and is + * "color" or an X11 color code (e.g. "aqua" or "rgb:12/34/56"). + * The color scheme can also be one of the special values: "black-white", + * "white-black", "gray-black", or "green-black". + */ + IDX_COLOR_SCHEME, + + /** + * The full absolute path to the directory in which typescripts should be + * written. + */ + IDX_TYPESCRIPT_PATH, + + /** + * The name that should be given to typescripts which are written in the + * given path. Each typescript will consist of two files: "NAME" and + * "NAME.timing". + */ + IDX_TYPESCRIPT_NAME, + + /** + * Whether the specified typescript path should automatically be created + * if it does not yet exist. + */ + IDX_CREATE_TYPESCRIPT_PATH, + + /** + * The full absolute path to the directory in which screen recordings + * should be written. + */ + IDX_RECORDING_PATH, + + /** + * The name that should be given to screen recordings which are written in + * the given path. + */ + IDX_RECORDING_NAME, + + /** + * Whether output which is broadcast to each connected client (graphics, + * streams, etc.) should NOT be included in the session recording. Output + * is included by default, as it is necessary for any recording which must + * later be viewable as video. + */ + IDX_RECORDING_EXCLUDE_OUTPUT, + + /** + * Whether changes to mouse state, such as position and buttons pressed or + * released, should NOT be included in the session recording. Mouse state + * is included by default, as it is necessary for the mouse cursor to be + * rendered in any resulting video. + */ + IDX_RECORDING_EXCLUDE_MOUSE, + + /** + * Whether keys pressed and released should be included in the session + * recording. Key events are NOT included by default within the recording, + * as doing so has privacy and security implications. Including key events + * may be necessary in certain auditing contexts, but should only be done + * with caution. Key events can easily contain sensitive information, such + * as passwords, credit card numbers, etc. + */ + IDX_RECORDING_INCLUDE_KEYS, + + /** + * Whether the specified screen recording path should automatically be + * created if it does not yet exist. + */ + IDX_CREATE_RECORDING_PATH, + + /** + * "true" if this connection should be read-only (user input should be + * dropped), "false" or blank otherwise. + */ + IDX_READ_ONLY, + + /** + * ASCII code, as an integer to use for the backspace key, or 127 + * if not specified. + */ + IDX_BACKSPACE, + + /** + * The maximum size of the scrollback buffer in rows. + */ + IDX_SCROLLBACK, + + KUBERNETES_ARGS_COUNT +}; + +guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, + int argc, const char** argv) { + + /* Validate arg count */ + if (argc != KUBERNETES_ARGS_COUNT) { + guac_user_log(user, GUAC_LOG_WARNING, "Incorrect number of connection " + "parameters provided: expected %i, got %i.", + KUBERNETES_ARGS_COUNT, argc); + return NULL; + } + + guac_kubernetes_settings* settings = + calloc(1, sizeof(guac_kubernetes_settings)); + + /* Read parameters */ + settings->hostname = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_HOSTNAME, ""); + + /* Parse whether SSL should be used */ + settings->use_ssl = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_USE_SSL, true); + + /* Read SSL/TLS connection details only if enabled */ + if (settings->use_ssl) { + + settings->client_cert_file = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_CLIENT_CERT_FILE, NULL); + + settings->client_key_file = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_CLIENT_KEY_FILE, NULL); + + settings->ca_cert_file = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_CA_CERT_FILE, NULL); + + settings->ignore_cert = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, + argv, IDX_IGNORE_CERT, false); + + } + + /* Read-only mode */ + settings->read_only = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_READ_ONLY, false); + + /* Read maximum scrollback size */ + settings->max_scrollback = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_SCROLLBACK, GUAC_KUBERNETES_DEFAULT_MAX_SCROLLBACK); + + /* Read font name */ + settings->font_name = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_FONT_NAME, GUAC_KUBERNETES_DEFAULT_FONT_NAME); + + /* Read font size */ + settings->font_size = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_FONT_SIZE, GUAC_KUBERNETES_DEFAULT_FONT_SIZE); + + /* Copy requested color scheme */ + settings->color_scheme = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_COLOR_SCHEME, ""); + + /* Pull width/height/resolution directly from user */ + settings->width = user->info.optimal_width; + settings->height = user->info.optimal_height; + settings->resolution = user->info.optimal_resolution; + + /* Read port */ + settings->port = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT); + + /* Read typescript path */ + settings->typescript_path = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_PATH, NULL); + + /* Read typescript name */ + settings->typescript_name = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_NAME, GUAC_KUBERNETES_DEFAULT_TYPESCRIPT_NAME); + + /* Parse path creation flag */ + settings->create_typescript_path = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_CREATE_TYPESCRIPT_PATH, false); + + /* Read recording path */ + settings->recording_path = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_PATH, NULL); + + /* Read recording name */ + settings->recording_name = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_NAME, GUAC_KUBERNETES_DEFAULT_RECORDING_NAME); + + /* Parse output exclusion flag */ + settings->recording_exclude_output = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_OUTPUT, false); + + /* Parse mouse exclusion flag */ + settings->recording_exclude_mouse = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_MOUSE, false); + + /* Parse key event inclusion flag */ + settings->recording_include_keys = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_INCLUDE_KEYS, false); + + /* Parse path creation flag */ + settings->create_recording_path = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_CREATE_RECORDING_PATH, false); + + /* Parse backspace key code */ + settings->backspace = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_BACKSPACE, 127); + + /* Parsing was successful */ + return settings; + +} + +void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) { + + /* Free network connection information */ + free(settings->hostname); + free(settings->port); + + /* Free SSL/TLS details */ + free(settings->client_cert_file); + free(settings->client_key_file); + free(settings->ca_cert_file); + + /* Free display preferences */ + free(settings->font_name); + free(settings->color_scheme); + + /* Free typescript settings */ + free(settings->typescript_name); + free(settings->typescript_path); + + /* Free screen recording settings */ + free(settings->recording_name); + free(settings->recording_path); + + /* Free overall structure */ + free(settings); + +} + diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h new file mode 100644 index 00000000..3e89ce53 --- /dev/null +++ b/src/protocols/kubernetes/settings.h @@ -0,0 +1,256 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_SETTINGS_H +#define GUAC_KUBERNETES_SETTINGS_H + +#include "config.h" + +#include + +#include +#include + +/** + * The name of the font to use for the terminal if no name is specified. + */ +#define GUAC_KUBERNETES_DEFAULT_FONT_NAME "monospace" + +/** + * The size of the font to use for the terminal if no font size is specified, + * in points. + */ +#define GUAC_KUBERNETES_DEFAULT_FONT_SIZE 12 + +/** + * The port to connect to when initiating any Kubernetes connection, if no + * other port is specified. + */ +#define GUAC_KUBERNETES_DEFAULT_PORT "8443" + +/** + * The filename to use for the typescript, if not specified. + */ +#define GUAC_KUBERNETES_DEFAULT_TYPESCRIPT_NAME "typescript" + +/** + * The filename to use for the screen recording, if not specified. + */ +#define GUAC_KUBERNETES_DEFAULT_RECORDING_NAME "recording" + +/** + * The default maximum scrollback size in rows. + */ +#define GUAC_KUBERNETES_DEFAULT_MAX_SCROLLBACK 1000 + +/** + * Settings for the Kubernetes connection. The values for this structure are + * parsed from the arguments given during the Guacamole protocol handshake + * using the guac_kubernetes_parse_args() function. + */ +typedef struct guac_kubernetes_settings { + + /** + * The hostname of the Kubernetes server to connect to. + */ + char* hostname; + + /** + * The port of the Kubernetes server to connect to. + */ + char* port; + + /** + * Whether SSL/TLS should be used. + */ + bool use_ssl; + + /** + * The filename of the certificate to use if performing SSL/TLS client + * authentication to authenticate with the Kubernetes server. If omitted, + * SSL client authentication will not be performed. + */ + char* client_cert_file; + + /** + * The filename of the key to use if performing SSL/TLS client + * authentication to authenticate with the Kubernetes server. If omitted, + * SSL client authentication will not be performed. + */ + char* client_key_file; + + /** + * The filename of the certificate of the certificate authority that signed + * the certificate of the Kubernetes server. + */ + char* ca_cert_file; + + /** + * Whether the certificate used by the Kubernetes server for SSL/TLS should + * be ignored if it cannot be validated. + */ + bool ignore_cert; + + /** + * Whether this connection is read-only, and user input should be dropped. + */ + bool read_only; + + /** + * The maximum size of the scrollback buffer in rows. + */ + int max_scrollback; + + /** + * The name of the font to use for display rendering. + */ + char* font_name; + + /** + * The size of the font to use, in points. + */ + int font_size; + + /** + * The name of the color scheme to use. + */ + char* color_scheme; + + /** + * The desired width of the terminal display, in pixels. + */ + int width; + + /** + * The desired height of the terminal display, in pixels. + */ + int height; + + /** + * The desired screen resolution, in DPI. + */ + int resolution; + + /** + * The path in which the typescript should be saved, if enabled. If no + * typescript should be saved, this will be NULL. + */ + char* typescript_path; + + /** + * The filename to use for the typescript, if enabled. + */ + char* typescript_name; + + /** + * Whether the typescript path should be automatically created if it does + * not already exist. + */ + bool create_typescript_path; + + /** + * The path in which the screen recording should be saved, if enabled. If + * no screen recording should be saved, this will be NULL. + */ + char* recording_path; + + /** + * The filename to use for the screen recording, if enabled. + */ + char* recording_name; + + /** + * Whether the screen recording path should be automatically created if it + * does not already exist. + */ + bool create_recording_path; + + /** + * Whether output which is broadcast to each connected client (graphics, + * streams, etc.) should NOT be included in the session recording. Output + * is included by default, as it is necessary for any recording which must + * later be viewable as video. + */ + bool recording_exclude_output; + + /** + * Whether changes to mouse state, such as position and buttons pressed or + * released, should NOT be included in the session recording. Mouse state + * is included by default, as it is necessary for the mouse cursor to be + * rendered in any resulting video. + */ + bool recording_exclude_mouse; + + /** + * Whether keys pressed and released should be included in the session + * recording. Key events are NOT included by default within the recording, + * as doing so has privacy and security implications. Including key events + * may be necessary in certain auditing contexts, but should only be done + * with caution. Key events can easily contain sensitive information, such + * as passwords, credit card numbers, etc. + */ + bool recording_include_keys; + + /** + * The ASCII code, as an integer, that the Kubernetes client will use when + * the backspace key is pressed. By default, this is 127, ASCII delete, if + * not specified in the client settings. + */ + int backspace; + +} guac_kubernetes_settings; + +/** + * Parses all given args, storing them in a newly-allocated settings object. If + * the args fail to parse, NULL is returned. + * + * @param user + * The user who submitted the given arguments while joining the + * connection. + * + * @param argc + * The number of arguments within the argv array. + * + * @param argv + * The values of all arguments provided by the user. + * + * @return + * A newly-allocated settings object which must be freed with + * guac_kubernetes_settings_free() when no longer needed. If the arguments + * fail to parse, NULL is returned. + */ +guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, + int argc, const char** argv); + +/** + * Frees the given guac_kubernetes_settings object, having been previously + * allocated via guac_kubernetes_parse_args(). + * + * @param settings + * The settings object to free. + */ +void guac_kubernetes_settings_free(guac_kubernetes_settings* settings); + +/** + * NULL-terminated array of accepted client args. + */ +extern const char* GUAC_KUBERNETES_CLIENT_ARGS[]; + +#endif + diff --git a/src/protocols/kubernetes/user.c b/src/protocols/kubernetes/user.c new file mode 100644 index 00000000..62666cb5 --- /dev/null +++ b/src/protocols/kubernetes/user.c @@ -0,0 +1,116 @@ +/* + * 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 "clipboard.h" +#include "input.h" +#include "kubernetes.h" +#include "pipe.h" +#include "settings.h" +#include "terminal/terminal.h" +#include "user.h" + +#include +#include +#include + +#include +#include + +int guac_kubernetes_user_join_handler(guac_user* user, int argc, char** argv) { + + guac_client* client = user->client; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Parse provided arguments */ + guac_kubernetes_settings* settings = guac_kubernetes_parse_args(user, + argc, (const char**) argv); + + /* Fail if settings cannot be parsed */ + if (settings == NULL) { + guac_user_log(user, GUAC_LOG_INFO, + "Badly formatted client arguments."); + return 1; + } + + /* Store settings at user level */ + user->data = settings; + + /* Connect to Kubernetes if owner */ + if (user->owner) { + + /* Store owner's settings at client level */ + kubernetes_client->settings = settings; + + /* Start client thread */ + if (pthread_create(&(kubernetes_client->client_thread), NULL, + guac_kubernetes_client_thread, (void*) client)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to start Kubernetes client thread"); + return 1; + } + + } + + /* If not owner, synchronize with current display */ + else { + guac_terminal_dup(kubernetes_client->term, user, user->socket); + guac_socket_flush(user->socket); + } + + /* Only handle events if not read-only */ + if (!settings->read_only) { + + /* General mouse/keyboard/clipboard events */ + user->key_handler = guac_kubernetes_user_key_handler; + user->mouse_handler = guac_kubernetes_user_mouse_handler; + user->clipboard_handler = guac_kubernetes_clipboard_handler; + + /* STDIN redirection */ + user->pipe_handler = guac_kubernetes_pipe_handler; + + /* Display size change events */ + user->size_handler = guac_kubernetes_user_size_handler; + + } + + return 0; + +} + +int guac_kubernetes_user_leave_handler(guac_user* user) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) user->client->data; + + /* Update shared cursor state */ + guac_common_cursor_remove_user(kubernetes_client->term->cursor, user); + + /* Free settings if not owner (owner settings will be freed with client) */ + if (!user->owner) { + guac_kubernetes_settings* settings = + (guac_kubernetes_settings*) user->data; + guac_kubernetes_settings_free(settings); + } + + return 0; +} + diff --git a/src/protocols/kubernetes/user.h b/src/protocols/kubernetes/user.h new file mode 100644 index 00000000..d235b2b7 --- /dev/null +++ b/src/protocols/kubernetes/user.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_USER_H +#define GUAC_KUBERNETES_USER_H + +#include "config.h" + +#include + +/** + * Handler for joining users. + */ +guac_user_join_handler guac_kubernetes_user_join_handler; + +/** + * Handler for leaving users. + */ +guac_user_leave_handler guac_kubernetes_user_leave_handler; + +#endif + From 5bae422b29939c2e08aff13b33bdb8c2b9ab2ed9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 9 Sep 2018 21:49:58 -0700 Subject: [PATCH 02/18] GUACAMOLE-623: libwebsockets requires an integer port number. --- src/protocols/kubernetes/settings.c | 3 +-- src/protocols/kubernetes/settings.h | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c index 1f04b401..5ee66716 100644 --- a/src/protocols/kubernetes/settings.c +++ b/src/protocols/kubernetes/settings.c @@ -278,7 +278,7 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, /* Read port */ settings->port = - guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT); /* Read typescript path */ @@ -340,7 +340,6 @@ void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) { /* Free network connection information */ free(settings->hostname); - free(settings->port); /* Free SSL/TLS details */ free(settings->client_cert_file); diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index 3e89ce53..fea6cf1a 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -42,7 +42,7 @@ * The port to connect to when initiating any Kubernetes connection, if no * other port is specified. */ -#define GUAC_KUBERNETES_DEFAULT_PORT "8443" +#define GUAC_KUBERNETES_DEFAULT_PORT 8443 /** * The filename to use for the typescript, if not specified. @@ -74,7 +74,7 @@ typedef struct guac_kubernetes_settings { /** * The port of the Kubernetes server to connect to. */ - char* port; + int port; /** * Whether SSL/TLS should be used. From 519c90a88760ae4db4ad4db8b0f4f02dcef599b4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 9 Sep 2018 22:54:50 -0700 Subject: [PATCH 03/18] GUACAMOLE-623: Default to unencrypted Kubernetes connections. --- src/protocols/kubernetes/settings.c | 4 ++-- src/protocols/kubernetes/settings.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c index 5ee66716..50daa6f9 100644 --- a/src/protocols/kubernetes/settings.c +++ b/src/protocols/kubernetes/settings.c @@ -68,7 +68,7 @@ enum KUBERNETES_ARGS_IDX { IDX_PORT, /** - * Whether SSL/TLS should be used. SSL is used by default. + * Whether SSL/TLS should be used. If omitted, SSL/TLS will not be used. */ IDX_USE_SSL, @@ -223,7 +223,7 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, /* Parse whether SSL should be used */ settings->use_ssl = guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, - IDX_USE_SSL, true); + IDX_USE_SSL, false); /* Read SSL/TLS connection details only if enabled */ if (settings->use_ssl) { diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index fea6cf1a..8f42f8ca 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -42,7 +42,7 @@ * The port to connect to when initiating any Kubernetes connection, if no * other port is specified. */ -#define GUAC_KUBERNETES_DEFAULT_PORT 8443 +#define GUAC_KUBERNETES_DEFAULT_PORT 8080 /** * The filename to use for the typescript, if not specified. From 7165fa949d21386d19cbda795fbc849e916461d0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 9 Sep 2018 22:55:38 -0700 Subject: [PATCH 04/18] GUACAMOLE-623: Stub out implementation of WebSocket client for Kubernetes. --- src/protocols/kubernetes/kubernetes.c | 177 +++++++++++++++++++++++++- src/protocols/kubernetes/kubernetes.h | 6 + 2 files changed, 178 insertions(+), 5 deletions(-) diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 231d78d3..98e49ecf 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -36,6 +37,104 @@ #include #include +/** + * The name of the WebSocket protocol specific to Kubernetes which should be + * sent to the Kubernetes server when attaching to a pod. + */ +#define GUAC_KUBERNETES_LWS_PROTOCOL "v4.channel.k8s.io" + +/** + * 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. This will always + * be a pointer to the guac_client instance. + * + * @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_client*) user; + /*guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data;*/ + + /* Request connection closure if client is stopped */ + if (client->state != GUAC_CLIENT_RUNNING) + return -1; + + switch (reason) { + + 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; + + /* Logged in */ + case LWS_CALLBACK_CLIENT_ESTABLISHED: + guac_client_log(client, GUAC_LOG_INFO, + "Kubernetes connection successful."); + break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + guac_client_log(client, GUAC_LOG_DEBUG, "Received: %s", + (const char*) in); + break; + + /* TODO: Only send data here. Request callback for writing via lws_callback_on_writable(some struct lws*) */ + case LWS_CALLBACK_CLIENT_WRITEABLE: + break; + + case LWS_CALLBACK_CLOSED: + /* TODO: case LWS_CALLBACK_CLIENT_CLOSED: <-- Needs test and #ifdef */ + 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: + guac_client_log(client, GUAC_LOG_DEBUG, "Unexpected libwebsockets " + "reason: %i", reason); + break; + + } + + return lws_callback_http_dummy(wsi, reason, user, in, length); + +} + +/** + * 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 @@ -111,11 +210,69 @@ void* guac_kubernetes_client_thread(void* data) { settings->create_typescript_path); } - /* TODO: Open WebSocket connection to Kubernetes */ + /* Init libwebsockets context creation parameters */ + struct lws_context_creation_info context_info = { + .port = CONTEXT_PORT_NO_LISTEN, /* We are not a WebSocket server */ + .protocols = guac_kubernetes_lws_protocols + }; - /* Logged in */ - guac_client_log(client, GUAC_LOG_INFO, - "Kubernetes connection successful."); + /* 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, + .pwsi = &kubernetes_client->wsi, + .userdata = client + }; + + /* If requested, use an SSL/TLS connection for communication with + * Kubernetes */ + if (settings->use_ssl) { + + /* Enable use of SSL/TLS */ + context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + connection_info.ssl_connection = LCCSCF_USE_SSL; + + /* Bypass certificate checks if requested */ + if (settings->ignore_cert) { + connection_info.ssl_connection |= + LCCSCF_ALLOW_SELFSIGNED + | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK + | LCCSCF_ALLOW_EXPIRED; + } + + /* Otherwise use the given CA certificate to validate (if any) */ + else + context_info.client_ssl_ca_filepath = settings->ca_cert_file; + + /* Certificate and key file for SSL/TLS client auth */ + context_info.client_ssl_cert_filepath = settings->client_cert_file; + context_info.client_ssl_private_key_filepath = settings->client_key_file; + + } + + /* Create libwebsockets context */ + struct lws_context* context = lws_create_context(&context_info); + if (!context) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Initialization of libwebsockets failed"); + return NULL; + } + + /* FIXME: Generate path dynamically */ + connection_info.context = context; + connection_info.path = "/api/v1/namespaces/default/pods/my-shell-68974bb7f7-rpjgr/attach?container=my-shell&stdin=true&stdout=true&tty=true"; + + /* 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"); + return NULL; + } /* Start input thread */ if (pthread_create(&(input_thread), NULL, guac_kubernetes_input_thread, (void*) client)) { @@ -123,12 +280,22 @@ void* guac_kubernetes_client_thread(void* data) { return NULL; } - /* TODO: While data available, write to terminal */ + /* 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(context, 1000) < 0) + break; + + } /* Kill client and Wait for input thread to die */ guac_client_stop(client); pthread_join(input_thread, NULL); + /* All done with libwebsockets */ + lws_context_destroy(context); + guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended."); return NULL; diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h index f8035ae5..89c172f7 100644 --- a/src/protocols/kubernetes/kubernetes.h +++ b/src/protocols/kubernetes/kubernetes.h @@ -26,6 +26,7 @@ #include "settings.h" #include "terminal/terminal.h" +#include #include /** @@ -38,6 +39,11 @@ typedef struct guac_kubernetes_client { */ guac_kubernetes_settings* settings; + /** + * The connected WebSocket. + */ + struct lws* wsi; + /** * The Kubernetes client thread. */ From f72877bf0d7c68fc2c08eb442d749ce096dc8118 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 9 Sep 2018 23:50:30 -0700 Subject: [PATCH 05/18] GUACAMOLE-623: Handle data received from Kubernetes. --- src/protocols/kubernetes/kubernetes.c | 70 +++++++++++++++++++++------ src/protocols/kubernetes/kubernetes.h | 26 ++++++++++ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 98e49ecf..ae2a8947 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -38,10 +38,44 @@ #include /** - * The name of the WebSocket protocol specific to Kubernetes which should be - * sent to the Kubernetes server when attaching to a pod. + * Handles data received from Kubernetes over WebSocket, decoding the channel + * index of the received data and forwarding that data accordingly. + * + * @param client + * The guac_client associated with the connection to Kubernetes. + * + * @param buffer + * The data received from Kubernetes. + * + * @param length + * The size of the data received from Kubernetes, in bytes. */ -#define GUAC_KUBERNETES_LWS_PROTOCOL "v4.channel.k8s.io" +static void guac_kubernetes_receive_data(guac_client* client, + const char* buffer, size_t length) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Strip channel index from beginning of buffer */ + int channel = *(buffer++); + length--; + + switch (channel) { + + /* Write STDOUT / STDERR directly to terminal as output */ + case GUAC_KUBERNETES_CHANNEL_STDOUT: + case GUAC_KUBERNETES_CHANNEL_STDERR: + guac_terminal_write(kubernetes_client->term, buffer, length); + break; + + /* Ignore data on other channels */ + default: + guac_client_log(client, GUAC_LOG_DEBUG, "Received %i bytes along " + "channel %i.", length, channel); + + } + +} /** * Callback invoked by libwebsockets for events related to a WebSocket being @@ -71,16 +105,15 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t length) { + /* Request connection closure if client is stopped (note that the user + * pointer passed by libwebsockets may be NULL for some events) */ guac_client* client = (guac_client*) user; - /*guac_kubernetes_client* kubernetes_client = - (guac_kubernetes_client*) client->data;*/ - - /* Request connection closure if client is stopped */ - if (client->state != GUAC_CLIENT_RUNNING) + if (client != NULL && client->state != GUAC_CLIENT_RUNNING) return -1; switch (reason) { + /* 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", @@ -88,23 +121,29 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, "available)"); break; - /* Logged in */ + /* Connected / logged in */ case LWS_CALLBACK_CLIENT_ESTABLISHED: guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection successful."); break; + /* Data received via WebSocket */ case LWS_CALLBACK_CLIENT_RECEIVE: - guac_client_log(client, GUAC_LOG_DEBUG, "Received: %s", - (const char*) in); + guac_kubernetes_receive_data(client, (const char*) in, length); break; /* TODO: Only send data here. Request callback for writing via lws_callback_on_writable(some struct lws*) */ case LWS_CALLBACK_CLIENT_WRITEABLE: break; + /* TODO: Add configure test */ +#ifdef HAVE_LWS_CALLBACK_CLIENT_CLOSED + /* Connection closed (client-specific) */ + case LWS_CALLBACK_CLIENT_CLOSED: +#endif + + /* Connection closed */ case LWS_CALLBACK_CLOSED: - /* TODO: case LWS_CALLBACK_CLIENT_CLOSED: <-- Needs test and #ifdef */ guac_client_stop(client); guac_client_log(client, GUAC_LOG_DEBUG, "WebSocket connection to " "Kubernetes server closed."); @@ -112,8 +151,6 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, /* No other event types are applicable */ default: - guac_client_log(client, GUAC_LOG_DEBUG, "Unexpected libwebsockets " - "reason: %i", reason); break; } @@ -213,7 +250,10 @@ void* guac_kubernetes_client_thread(void* data) { /* Init libwebsockets context creation parameters */ struct lws_context_creation_info context_info = { .port = CONTEXT_PORT_NO_LISTEN, /* We are not a WebSocket server */ - .protocols = guac_kubernetes_lws_protocols + .uid = -1, + .gid = -1, + .protocols = guac_kubernetes_lws_protocols, + .user = client }; /* Init WebSocket connection parameters which do not vary by Guacmaole diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h index 89c172f7..89b6678f 100644 --- a/src/protocols/kubernetes/kubernetes.h +++ b/src/protocols/kubernetes/kubernetes.h @@ -29,6 +29,32 @@ #include #include +/** + * The name of the WebSocket protocol specific to Kubernetes which should be + * sent to the Kubernetes server when attaching to a pod. + */ +#define GUAC_KUBERNETES_LWS_PROTOCOL "v4.channel.k8s.io" + +/** + * The index of the Kubernetes channel used for STDIN. + */ +#define GUAC_KUBERNETES_CHANNEL_STDIN 0 + +/** + * The index of the Kubernetes channel used for STDOUT. + */ +#define GUAC_KUBERNETES_CHANNEL_STDOUT 1 + +/** + * The index of the Kubernetes channel used for STDERR. + */ +#define GUAC_KUBERNETES_CHANNEL_STDERR 2 + +/** + * The index of the Kubernetes channel used for terminal resize messages. + */ +#define GUAC_KUBERNETES_CHANNEL_RESIZE 4 + /** * Kubernetes-specific client data. */ From cbe593503f4cf819af32abc0b661662aa3fadc9e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 00:25:49 -0700 Subject: [PATCH 06/18] GUACAMOLE-623: Do not return -1 from libwebsockets callback. Doing so results in automatic cleanup of part of the context, resulting in a segfault when lws_context_destroy() is invoked. --- src/protocols/kubernetes/kubernetes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index ae2a8947..e38d5e37 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -109,7 +109,7 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, * pointer passed by libwebsockets may be NULL for some events) */ guac_client* client = (guac_client*) user; if (client != NULL && client->state != GUAC_CLIENT_RUNNING) - return -1; + return lws_callback_http_dummy(wsi, reason, user, in, length); switch (reason) { From f35517b3ff42f268e20f84282bc93130fa86e98d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 01:26:13 -0700 Subject: [PATCH 07/18] GUACAMOLE-623: Add outbound message buffer. --- src/protocols/kubernetes/client.c | 13 +----- src/protocols/kubernetes/kubernetes.c | 31 ++++++++++---- src/protocols/kubernetes/kubernetes.h | 58 +++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c index 1b5d175d..77aa6473 100644 --- a/src/protocols/kubernetes/client.c +++ b/src/protocols/kubernetes/client.c @@ -67,17 +67,8 @@ int guac_kubernetes_client_free_handler(guac_client* client) { guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) client->data; - /* Clean up recording, if in progress */ - if (kubernetes_client->recording != NULL) - guac_common_recording_free(kubernetes_client->recording); - - /* Kill terminal */ - guac_terminal_free(kubernetes_client->term); - - /* TODO: Wait for and free WebSocket session, if connected */ - /*if (kubernetes_client->websocket != NULL) { - pthread_join(kubernetes_client->client_thread, NULL); - }*/ + /* Wait client thread to terminate */ + pthread_join(kubernetes_client->client_thread, NULL); /* Free settings */ if (kubernetes_client->settings != NULL) diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index e38d5e37..62fb6ee7 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -206,6 +206,8 @@ static void* guac_kubernetes_input_thread(void* data) { void* guac_kubernetes_client_thread(void* data) { + struct lws_context* context = NULL; + guac_client* client = (guac_client*) data; guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) client->data; @@ -236,7 +238,7 @@ void* guac_kubernetes_client_thread(void* data) { if (kubernetes_client->term == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Terminal initialization failed"); - return NULL; + goto fail; } /* Set up typescript, if requested */ @@ -295,11 +297,11 @@ void* guac_kubernetes_client_thread(void* data) { } /* Create libwebsockets context */ - struct lws_context* context = lws_create_context(&context_info); + context = lws_create_context(&context_info); if (!context) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Initialization of libwebsockets failed"); - return NULL; + goto fail; } /* FIXME: Generate path dynamically */ @@ -311,13 +313,16 @@ void* guac_kubernetes_client_thread(void* data) { if (kubernetes_client->wsi == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Connection via libwebsockets failed"); - return NULL; + 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"); - return NULL; + goto fail; } /* As long as client is connected, continue polling libwebsockets */ @@ -330,11 +335,23 @@ void* guac_kubernetes_client_thread(void* data) { } /* Kill client and Wait for input thread to die */ + guac_terminal_stop(kubernetes_client->term); guac_client_stop(client); pthread_join(input_thread, NULL); - /* All done with libwebsockets */ - lws_context_destroy(context); +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 (context != NULL) + lws_context_destroy(context); guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended."); return NULL; diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h index 89b6678f..8fb917db 100644 --- a/src/protocols/kubernetes/kubernetes.h +++ b/src/protocols/kubernetes/kubernetes.h @@ -55,6 +55,37 @@ */ #define GUAC_KUBERNETES_CHANNEL_RESIZE 4 +/** + * The maximum number of messages to allow within the outbound message buffer. + * If messages are sent despite the buffer being full, those messages will be + * dropped. + */ +#define GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES 8 + +/** + * An outbound message to be received by Kubernetes over WebSocket. + */ +typedef struct guac_kubernetes_message { + + /** + * The index of the channel receiving the data, such as + * GUAC_KUBERNETES_CHANNEL_STDIN. + */ + uint8_t channel; + + /** + * The data that should be sent to Kubernetes (along with the channel + * index). + */ + char data[1024]; + + /** + * The length of the data to be sent, excluding the channel index. + */ + int length; + +} guac_kubernetes_message; + /** * Kubernetes-specific client data. */ @@ -70,6 +101,33 @@ typedef struct guac_kubernetes_client { */ struct lws* wsi; + /** + * Outbound message ring buffer for outbound WebSocket messages. As + * libwebsockets uses an event loop for all operations, outbound messages + * may be sent only in context of a particular event received via a + * callback. Until that event is received, pending data must accumulate in + * a buffer. + */ + guac_kubernetes_message outbound_messages[GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES]; + + /** + * The number of messages currently waiting in the outbound message + * buffer. + */ + int outbound_messages_waiting; + + /** + * The index of the oldest entry in the outbound message buffer. Newer + * messages follow this entry. + */ + int outbound_messages_top; + + /** + * Lock which is acquired when the outbound message buffer is being read + * or manipulated. + */ + pthread_mutex_t outbound_message_lock; + /** * The Kubernetes client thread. */ From b7c938c239f0c61b6122c817c6ce44309d61bd1f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 02:16:36 -0700 Subject: [PATCH 08/18] GUACAMOLE-623: Send typed data to Kubernetes via the STDIN channel. --- src/protocols/kubernetes/kubernetes.c | 151 ++++++++++++++++++++++++-- src/protocols/kubernetes/kubernetes.h | 26 ++++- 2 files changed, 164 insertions(+), 13 deletions(-) diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 62fb6ee7..aadb448d 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -77,6 +77,127 @@ static void guac_kubernetes_receive_data(guac_client* client, } +/** + * Requests that the given data be sent along the given channel to the + * Kubernetes server when the WebSocket connection is next available for + * writing. If the WebSocket connection has not been available for writing for + * long enough that the outbound message buffer is full, the request to send + * this particular message will be dropped. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param channel + * The Kubernetes channel on which to send the message, + * such as GUAC_KUBERNETES_CHANNEL_STDIN. + * + * @param data + * A buffer containing the data to send. + * + * @param length + * The number of bytes to send. + */ +static void guac_kubernetes_send_message(guac_client* client, + int channel, const char* data, int length) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); + + /* Add message to buffer if space is available */ + if (kubernetes_client->outbound_messages_waiting + < GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES) { + + /* Calculate storage position of next message */ + int index = (kubernetes_client->outbound_messages_top + + kubernetes_client->outbound_messages_waiting) + % GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; + + /* Obtain pointer to message slot at calculated position */ + guac_kubernetes_message* message = + &(kubernetes_client->outbound_messages[index]); + + /* Copy details of message into buffer */ + message->channel = channel; + memcpy(message->data, data, length); + message->length = length; + + /* One more message is now waiting */ + kubernetes_client->outbound_messages_waiting++; + + /* Notify libwebsockets that we need a callback to send pending + * messages */ + lws_callback_on_writable(kubernetes_client->wsi); + lws_cancel_service(kubernetes_client->context); + + } + + /* Warn if data has to be dropped */ + else + guac_client_log(client, GUAC_LOG_WARNING, "Send buffer could not be " + "flushed in time to handle additional data. Outbound " + "message dropped."); + + pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); + +} + +/** + * Writes the oldest pending message within the outbound message queue, + * as scheduled with guac_kubernetes_send_message(), removing that message + * from the queue. This function MAY NOT be invoked outside the libwebsockets + * event callback and MUST only be invoked in the context of a + * LWS_CALLBACK_CLIENT_WRITEABLE event. If no messages are pending, this + * function has no effect. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @return + * true if messages still remain to be written within the outbound message + * queue, false otherwise. + */ +static bool guac_kubernetes_write_pending_message(guac_client* client) { + + bool messages_remain; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); + + /* Send one message from top of buffer */ + if (kubernetes_client->outbound_messages_waiting > 0) { + + /* Obtain pointer to message at top */ + int top = kubernetes_client->outbound_messages_top; + guac_kubernetes_message* message = + &(kubernetes_client->outbound_messages[top]); + + /* Write message including channel index */ + lws_write(kubernetes_client->wsi, + ((unsigned char*) message) + LWS_PRE, + message->length + 1, LWS_WRITE_BINARY); + + /* Advance top to next message */ + kubernetes_client->outbound_messages_top++; + kubernetes_client->outbound_messages_top %= + GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; + + /* One less message is waiting */ + kubernetes_client->outbound_messages_waiting--; + + } + + /* Record whether messages remained at time of completion */ + messages_remain = (kubernetes_client->outbound_messages_waiting > 0); + + pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); + + return messages_remain; + +} + /** * Callback invoked by libwebsockets for events related to a WebSocket being * used for communicating with an attached Kubernetes pod. @@ -132,8 +253,14 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, guac_kubernetes_receive_data(client, (const char*) in, length); break; - /* TODO: Only send data here. Request callback for writing via lws_callback_on_writable(some struct lws*) */ + /* 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; /* TODO: Add configure test */ @@ -189,14 +316,15 @@ static void* guac_kubernetes_input_thread(void* data) { guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) client->data; - char buffer[8192]; + 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) { - /* TODO: Send to Kubernetes */ - guac_terminal_write(kubernetes_client->term, buffer, bytes_read); + /* Send received data to Kubernetes along STDIN channel */ + guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_STDIN, + buffer, bytes_read); } @@ -206,8 +334,6 @@ static void* guac_kubernetes_input_thread(void* data) { void* guac_kubernetes_client_thread(void* data) { - struct lws_context* context = NULL; - guac_client* client = (guac_client*) data; guac_kubernetes_client* kubernetes_client = (guac_kubernetes_client*) client->data; @@ -297,15 +423,15 @@ void* guac_kubernetes_client_thread(void* data) { } /* Create libwebsockets context */ - context = lws_create_context(&context_info); - if (!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; } /* FIXME: Generate path dynamically */ - connection_info.context = context; + connection_info.context = kubernetes_client->context; connection_info.path = "/api/v1/namespaces/default/pods/my-shell-68974bb7f7-rpjgr/attach?container=my-shell&stdin=true&stdout=true&tty=true"; /* Open WebSocket connection to Kubernetes */ @@ -329,7 +455,8 @@ void* guac_kubernetes_client_thread(void* data) { while (client->state == GUAC_CLIENT_RUNNING) { /* Cease polling libwebsockets if an error condition is signalled */ - if (lws_service(context, 1000) < 0) + if (lws_service(kubernetes_client->context, + GUAC_KUBERNETES_SERVICE_INTERVAL) < 0) break; } @@ -350,8 +477,8 @@ fail: guac_common_recording_free(kubernetes_client->recording); /* Free WebSocket context if successfully allocated */ - if (context != NULL) - lws_context_destroy(context); + if (kubernetes_client->context != NULL) + lws_context_destroy(kubernetes_client->context); guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended."); return NULL; diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h index 8fb917db..761a897c 100644 --- a/src/protocols/kubernetes/kubernetes.h +++ b/src/protocols/kubernetes/kubernetes.h @@ -55,6 +55,13 @@ */ #define GUAC_KUBERNETES_CHANNEL_RESIZE 4 +/** + * The maximum amount of data to include in any particular WebSocket message + * to Kubernetes. This excludes the storage space required for the channel + * index. + */ +#define GUAC_KUBERNETES_MAX_MESSAGE_SIZE 1024 + /** * The maximum number of messages to allow within the outbound message buffer. * If messages are sent despite the buffer being full, those messages will be @@ -62,11 +69,23 @@ */ #define GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES 8 +/** + * The maximum number of milliseconds to wait for a libwebsockets event to + * occur before entering another iteration of the libwebsockets event loop. + */ +#define GUAC_KUBERNETES_SERVICE_INTERVAL 1000 + /** * An outbound message to be received by Kubernetes over WebSocket. */ typedef struct guac_kubernetes_message { + /** + * lws_write() requires leading padding of LWS_PRE bytes to provide + * scratch space for WebSocket framing. + */ + uint8_t _padding[LWS_PRE]; + /** * The index of the channel receiving the data, such as * GUAC_KUBERNETES_CHANNEL_STDIN. @@ -77,7 +96,7 @@ typedef struct guac_kubernetes_message { * The data that should be sent to Kubernetes (along with the channel * index). */ - char data[1024]; + char data[GUAC_KUBERNETES_MAX_MESSAGE_SIZE]; /** * The length of the data to be sent, excluding the channel index. @@ -96,6 +115,11 @@ typedef struct guac_kubernetes_client { */ guac_kubernetes_settings* settings; + /** + * The libwebsockets context associated with the connected WebSocket. + */ + struct lws_context* context; + /** * The connected WebSocket. */ From fe7edce5694a718ce233b4d4c85b386be4240262 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 02:50:15 -0700 Subject: [PATCH 09/18] GUACAMOLE-623: Add support for terminal resize. Redraw Kubernetes container upon connect. --- src/protocols/kubernetes/input.c | 4 +- src/protocols/kubernetes/kubernetes.c | 55 ++++++++++++++++++++++++++- src/protocols/kubernetes/kubernetes.h | 38 ++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/protocols/kubernetes/input.c b/src/protocols/kubernetes/input.c index 9bf5b717..e73772e5 100644 --- a/src/protocols/kubernetes/input.c +++ b/src/protocols/kubernetes/input.c @@ -88,7 +88,9 @@ int guac_kubernetes_user_size_handler(guac_user* user, int width, int height) { /* Resize terminal */ guac_terminal_resize(terminal, width, height); - /* TODO: Update Kubernetes terminal window size if connected */ + /* Update Kubernetes terminal window size if connected */ + guac_kubernetes_resize(client, terminal->term_height, + terminal->term_width); return 0; } diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index aadb448d..53a8580d 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -246,6 +246,11 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, case LWS_CALLBACK_CLIENT_ESTABLISHED: guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection successful."); + + /* 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 */ @@ -260,7 +265,6 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, * yet more messages remain */ if (guac_kubernetes_write_pending_message(client)) lws_callback_on_writable(wsi); - break; /* TODO: Add configure test */ @@ -451,6 +455,11 @@ void* guac_kubernetes_client_thread(void* data) { 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) { @@ -485,3 +494,47 @@ fail: } +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); + +} + diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h index 761a897c..fedf77a2 100644 --- a/src/protocols/kubernetes/kubernetes.h +++ b/src/protocols/kubernetes/kubernetes.h @@ -167,6 +167,18 @@ typedef struct guac_kubernetes_client { */ guac_terminal* term; + /** + * The number of rows last sent to Kubernetes in a terminal resize + * request. + */ + int rows; + + /** + * The number of columns last sent to Kubernetes in a terminal resize + * request. + */ + int columns; + /** * The in-progress session recording, or NULL if no recording is in * progress. @@ -181,5 +193,31 @@ typedef struct guac_kubernetes_client { */ void* guac_kubernetes_client_thread(void* data); +/** + * Sends a message to the Kubernetes server requesting that the terminal be + * resized to the given dimensions. This message may be queued until the + * underlying WebSocket connection is ready to send. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param rows + * The new terminal size in rows. + * + * @param columns + * The new terminal size in columns. + */ +void guac_kubernetes_resize(guac_client* client, int rows, int columns); + +/** + * Sends messages to the Kubernetes server such that the terminal is forced + * to redraw. This function should be invoked at the beginning of each + * session in order to restore expected display state. + * + * @param client + * The guac_client associated with the Kubernetes connection. + */ +void guac_kubernetes_force_redraw(guac_client* client); + #endif From 34f8f8b30d84e7622005f2ba0ab704758cc88767 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 15:01:48 -0700 Subject: [PATCH 10/18] GUACAMOLE-623: Redirect libwebsockets logging to guacd's debug level log. --- src/protocols/kubernetes/client.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c index 77aa6473..450e1e87 100644 --- a/src/protocols/kubernetes/client.c +++ b/src/protocols/kubernetes/client.c @@ -33,8 +33,39 @@ #include +/** + * Static reference to the guac_client associated with the active Kubernetes + * connection. As guacd guarantees that each main client connection is + * isolated within its own process, this is safe. + */ +static guac_client* guac_kubernetes_lws_log_client = NULL; + +/** + * Logging callback invoked by libwebsockets to log a single line of logging + * output. As libwebsockets messages are all generally low-level, the log + * level provided by libwebsockets is ignored here, with all messages logged + * instead at guacd's debug level. + * + * @param level + * The libwebsockets log level associated with the log message. This value + * is ignored by this implementation of the logging callback. + * + * @param line + * The line of logging output to log. + */ +static void guac_kubernetes_log(int level, const char* line) { + if (guac_kubernetes_lws_log_client != NULL) + guac_client_log(guac_kubernetes_lws_log_client, GUAC_LOG_DEBUG, + "libwebsockets: %s", line); +} + int guac_client_init(guac_client* client) { + /* Redirect libwebsockets logging */ + guac_kubernetes_lws_log_client = client; + lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO, + guac_kubernetes_log); + /* Set client args */ client->args = GUAC_KUBERNETES_CLIENT_ARGS; From ed560938886e92dbe656d610966585b8178c2d73 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 18:39:06 -0700 Subject: [PATCH 11/18] GUACAMOLE-623: Generate Kubernetes API endpoint dynamically. --- src/protocols/kubernetes/Makefile.am | 2 + src/protocols/kubernetes/kubernetes.c | 27 +++++- src/protocols/kubernetes/settings.c | 53 +++++++++-- src/protocols/kubernetes/settings.h | 24 +++++ src/protocols/kubernetes/url.c | 126 ++++++++++++++++++++++++++ src/protocols/kubernetes/url.h | 87 ++++++++++++++++++ 6 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 src/protocols/kubernetes/url.c create mode 100644 src/protocols/kubernetes/url.h diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am index d864967f..9e50feb6 100644 --- a/src/protocols/kubernetes/Makefile.am +++ b/src/protocols/kubernetes/Makefile.am @@ -29,6 +29,7 @@ libguac_client_kubernetes_la_SOURCES = \ pipe.c \ settings.c \ kubernetes.c \ + url.c \ user.c noinst_HEADERS = \ @@ -38,6 +39,7 @@ noinst_HEADERS = \ pipe.h \ settings.h \ kubernetes.h \ + url.h \ user.h libguac_client_kubernetes_la_CFLAGS = \ diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 53a8580d..380d1d3b 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -21,6 +21,7 @@ #include "common/recording.h" #include "kubernetes.h" #include "terminal/terminal.h" +#include "url.h" #include #include @@ -345,6 +346,28 @@ void* guac_kubernetes_client_thread(void* 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) { @@ -434,9 +457,9 @@ void* guac_kubernetes_client_thread(void* data) { goto fail; } - /* FIXME: Generate path dynamically */ + /* Generate path dynamically */ connection_info.context = kubernetes_client->context; - connection_info.path = "/api/v1/namespaces/default/pods/my-shell-68974bb7f7-rpjgr/attach?container=my-shell&stdin=true&stdout=true&tty=true"; + connection_info.path = endpoint_path; /* Open WebSocket connection to Kubernetes */ kubernetes_client->wsi = lws_client_connect_via_info(&connection_info); diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c index 50daa6f9..4f9f3f24 100644 --- a/src/protocols/kubernetes/settings.c +++ b/src/protocols/kubernetes/settings.c @@ -32,6 +32,9 @@ const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { "hostname", "port", + "namespace", + "pod", + "container", "use-ssl", "client-cert-file", "client-key-file", @@ -67,6 +70,24 @@ enum KUBERNETES_ARGS_IDX { */ IDX_PORT, + /** + * The name of the Kubernetes namespace of the pod containing the container + * being attached to. If omitted, the default namespace will be used. + */ + IDX_NAMESPACE, + + /** + * The name of the Kubernetes pod containing with the container being + * attached to. Required. + */ + IDX_POD, + + /** + * The name of the container to attach to. If omitted, the first container + * in the pod will be used. + */ + IDX_CONTAINER, + /** * Whether SSL/TLS should be used. If omitted, SSL/TLS will not be used. */ @@ -215,11 +236,31 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, guac_kubernetes_settings* settings = calloc(1, sizeof(guac_kubernetes_settings)); - /* Read parameters */ + /* Read hostname */ settings->hostname = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, IDX_HOSTNAME, ""); + /* Read port */ + settings->port = + guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT); + + /* Read Kubernetes namespace */ + settings->kubernetes_namespace = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_NAMESPACE, GUAC_KUBERNETES_DEFAULT_NAMESPACE); + + /* Read name of Kubernetes pod (required) */ + settings->kubernetes_pod = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_POD, NULL); + + /* Read container of pod (optional) */ + settings->kubernetes_container = + guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_CONTAINER, NULL); + /* Parse whether SSL should be used */ settings->use_ssl = guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, @@ -276,11 +317,6 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, settings->height = user->info.optimal_height; settings->resolution = user->info.optimal_resolution; - /* Read port */ - settings->port = - guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, - IDX_PORT, GUAC_KUBERNETES_DEFAULT_PORT); - /* Read typescript path */ settings->typescript_path = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, @@ -341,6 +377,11 @@ void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) { /* Free network connection information */ free(settings->hostname); + /* Free Kubernetes pod/container details */ + free(settings->kubernetes_namespace); + free(settings->kubernetes_pod); + free(settings->kubernetes_container); + /* Free SSL/TLS details */ free(settings->client_cert_file); free(settings->client_key_file); diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index 8f42f8ca..c2e479ea 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -44,6 +44,12 @@ */ #define GUAC_KUBERNETES_DEFAULT_PORT 8080 +/** + * The name of the Kubernetes namespace that should be used by default if no + * specific Kubernetes namespace is provided. + */ +#define GUAC_KUBERNETES_DEFAULT_NAMESPACE "default" + /** * The filename to use for the typescript, if not specified. */ @@ -76,6 +82,24 @@ typedef struct guac_kubernetes_settings { */ int port; + /** + * The name of the Kubernetes namespace of the pod containing the container + * being attached to. + */ + char* kubernetes_namespace; + + /** + * The name of the Kubernetes pod containing with the container being + * attached to. + */ + char* kubernetes_pod; + + /** + * The name of the container to attach to, or NULL to arbitrarily attach to + * the first container in the pod. + */ + char* kubernetes_container; + /** * Whether SSL/TLS should be used. */ diff --git a/src/protocols/kubernetes/url.c b/src/protocols/kubernetes/url.c new file mode 100644 index 00000000..cfd6f745 --- /dev/null +++ b/src/protocols/kubernetes/url.c @@ -0,0 +1,126 @@ +/* + * 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 "url.h" + +#include +#include + +static int guac_kubernetes_is_url_safe(char c) { + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || strchr("-_.!~*'()", c) != NULL; +} + +int guac_kubernetes_escape_url_component(char* output, int length, + const char* str) { + + char* current = output; + while (*str != '\0') { + + char c = *str; + + /* Store alphanumeric characters verbatim */ + if (guac_kubernetes_is_url_safe(c)) { + + /* Verify space exists for single character */ + if (length < 1) + return 1; + + *(current++) = c; + length--; + + } + + /* Escape EVERYTHING else as hex */ + else { + + /* Verify space exists for hex-encoded character */ + if (length < 4) + return 1; + + snprintf(current, 4, "%%%02X", (int) c); + + current += 3; + length -= 3; + } + + /* Next character */ + str++; + + } + + /* Verify space exists for null terminator */ + if (length < 1) + return 1; + + /* Append null terminator */ + *current = '\0'; + return 0; + +} + +int guac_kubernetes_endpoint_attach(char* buffer, int length, + const char* kubernetes_namespace, const char* kubernetes_pod, + const char* kubernetes_container) { + + int written; + + char escaped_namespace[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; + char escaped_pod[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; + char escaped_container[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH]; + + /* Escape Kubernetes namespace */ + if (guac_kubernetes_escape_url_component(escaped_namespace, + sizeof(escaped_namespace), kubernetes_namespace)) + return 1; + + /* Escape name of Kubernetes pod */ + if (guac_kubernetes_escape_url_component(escaped_pod, + sizeof(escaped_pod), kubernetes_pod)) + return 1; + + /* Generate attachment endpoint URL */ + if (kubernetes_container != NULL) { + + /* Escape container name */ + if (guac_kubernetes_escape_url_component(escaped_container, + sizeof(escaped_container), kubernetes_container)) + return 1; + + written = snprintf(buffer, length, + "/api/v1/namespaces/%s/pods/%s/attach" + "?container=%s&stdin=true&stdout=true&tty=true", + escaped_namespace, escaped_pod, escaped_container); + } + else { + written = snprintf(buffer, length, + "/api/v1/namespaces/%s/pods/%s/attach" + "?stdin=true&stdout=true&tty=true", + escaped_namespace, escaped_pod); + } + + /* Endpoint URL was successfully generated if it was written to the given + * buffer without truncation */ + return !(written < length - 1); + +} + diff --git a/src/protocols/kubernetes/url.h b/src/protocols/kubernetes/url.h new file mode 100644 index 00000000..19084ee0 --- /dev/null +++ b/src/protocols/kubernetes/url.h @@ -0,0 +1,87 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_URL_H +#define GUAC_KUBERNETES_URL_H + +#include "config.h" + +/** + * The maximum number of characters allowed in the full path for any Kubernetes + * endpoint. + */ +#define GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH 1024 + +/** + * Escapes the given string such that it can be included safely within a URL. + * This function duplicates the behavior of JavaScript's encodeURIComponent(), + * escaping all but the following characters: A-Z a-z 0-9 - _ . ! ~ * ' ( ) + * + * @param output + * The buffer which should receive the escaped string. This buffer may be + * touched even if escaping is unsuccessful. + * + * @param length + * The number of bytes available in the given output buffer. + * + * @param str + * The string to escape. + * + * @return + * Zero if the string was successfully escaped and written into the + * provided output buffer without being truncated, including null + * terminator, non-zero otherwise. + */ +int guac_kubernetes_escape_url_component(char* output, int length, + const char* str); + +/** + * Generates the full path to the Kubernetes API endpoint which handles + * attaching to running containers within specific pods. Values within the path + * will be URL-escaped as necessary. + * + * @param buffer + * The buffer which should receive the endpoint path. This buffer may be + * touched even if the endpoint path could not be generated. + * + * @param length + * The number of bytes available in the given buffer. + * + * @param kubernetes_namespace + * The name of the Kubernetes namespace of the pod containing the container + * being attached to. + * + * @param kubernetes_pod + * The name of the Kubernetes pod containing with the container being + * attached to. + * + * @param kubernetes_container + * The name of the container to attach to, or NULL to arbitrarily attach + * to the first container in the pod. + * + * @return + * Zero if the endpoint path was successfully written to the provided + * buffer, non-zero if insufficient space exists within the buffer. + */ +int guac_kubernetes_endpoint_attach(char* buffer, int length, + const char* kubernetes_namespace, const char* kubernetes_pod, + const char* kubernetes_container); + +#endif + From c5f67a31dc6c803da23f70662befc102a9187855 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 20:00:44 -0700 Subject: [PATCH 12/18] GUACAMOLE-623: Add configure test for LWS_CALLBACK_CLIENT_CLOSED (only defined in recent libwebsockets and required if present). --- configure.ac | 10 ++++++++++ src/protocols/kubernetes/kubernetes.c | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index ae783244..7c0bb73c 100644 --- a/configure.ac +++ b/configure.ac @@ -1193,6 +1193,16 @@ then [have_libwebsockets=no]) fi +# Check for client-specific closed event, which must be used in favor of the +# generic closed event if libwebsockets is recent enough to provide this +if test "x$with_websockets" != "xno" +then + AC_CHECK_DECL([LWS_CALLBACK_CLIENT_CLOSED], + [AC_DEFINE([HAVE_LWS_CALLBACK_CLIENT_CLOSED],, + [Whether LWS_CALLBACK_CLIENT_CLOSED is defined])],, + [#include ]) +fi + AM_CONDITIONAL([ENABLE_WEBSOCKETS], [test "x${have_libwebsockets}" = "xyes"]) diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 380d1d3b..9850c6d4 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -268,7 +268,6 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, lws_callback_on_writable(wsi); break; - /* TODO: Add configure test */ #ifdef HAVE_LWS_CALLBACK_CLIENT_CLOSED /* Connection closed (client-specific) */ case LWS_CALLBACK_CLIENT_CLOSED: From 77a866129b3b6592f122a5912c349a03f0c4b4e2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 20:09:36 -0700 Subject: [PATCH 13/18] GUACAMOLE-623: Add warning when Kubernetes support will not be built. Fix summary output from configure. --- configure.ac | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 7c0bb73c..df36eabe 100644 --- a/configure.ac +++ b/configure.ac @@ -1190,7 +1190,12 @@ then AC_CHECK_LIB([websockets], [lws_create_context], [WEBSOCKETS_LIBS="$WEBSOCKETS_LIBS -lwebsockets"], - [have_libwebsockets=no]) + [AC_MSG_WARN([ + -------------------------------------------- + Unable to find libwebsockets. + Support for Kubernetes will be disabled. + --------------------------------------------]) + have_libwebsockets=no]) fi # Check for client-specific closed event, which must be used in favor of the @@ -1339,7 +1344,7 @@ $PACKAGE_NAME version $PACKAGE_VERSION libVNCServer ........ ${have_libvncserver} libvorbis ........... ${have_vorbis} libpulse ............ ${have_pulse} - libwebsockets ....... ${have_websockets} + libwebsockets ....... ${have_libwebsockets} libwebp ............. ${have_webp} wsock32 ............. ${have_winsock} From 371eed1f93c353e0bf2159bcffd77cf6d8bd9716 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 21:05:23 -0700 Subject: [PATCH 14/18] GUACAMOLE-623: Add missin includes. Remove unnecessary includes. --- src/protocols/kubernetes/client.c | 9 ++++----- src/protocols/kubernetes/client.h | 6 +----- src/protocols/kubernetes/clipboard.c | 2 -- src/protocols/kubernetes/clipboard.h | 2 -- src/protocols/kubernetes/input.c | 5 +---- src/protocols/kubernetes/input.h | 2 -- src/protocols/kubernetes/kubernetes.c | 10 ++-------- src/protocols/kubernetes/kubernetes.h | 4 +++- src/protocols/kubernetes/pipe.c | 5 +++-- src/protocols/kubernetes/pipe.h | 2 -- src/protocols/kubernetes/settings.c | 5 ----- src/protocols/kubernetes/settings.h | 3 --- src/protocols/kubernetes/url.c | 2 +- src/protocols/kubernetes/url.h | 2 -- src/protocols/kubernetes/user.c | 6 +++--- src/protocols/kubernetes/user.h | 2 -- 16 files changed, 18 insertions(+), 49 deletions(-) diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c index 450e1e87..58f4728a 100644 --- a/src/protocols/kubernetes/client.c +++ b/src/protocols/kubernetes/client.c @@ -17,22 +17,21 @@ * under the License. */ -#include "config.h" #include "client.h" -#include "common/recording.h" +#include "common/clipboard.h" #include "kubernetes.h" #include "settings.h" -#include "terminal/terminal.h" #include "user.h" +#include +#include + #include #include #include #include #include -#include - /** * Static reference to the guac_client associated with the active Kubernetes * connection. As guacd guarantees that each main client connection is diff --git a/src/protocols/kubernetes/client.h b/src/protocols/kubernetes/client.h index 2e96d109..0b847da4 100644 --- a/src/protocols/kubernetes/client.h +++ b/src/protocols/kubernetes/client.h @@ -20,11 +20,7 @@ #ifndef GUAC_KUBERNETES_CLIENT_H #define GUAC_KUBERNETES_CLIENT_H -#include "config.h" -#include "terminal/terminal.h" - -#include -#include +#include /** * The maximum number of bytes to allow within the clipboard. diff --git a/src/protocols/kubernetes/clipboard.c b/src/protocols/kubernetes/clipboard.c index 87a34b04..f1682066 100644 --- a/src/protocols/kubernetes/clipboard.c +++ b/src/protocols/kubernetes/clipboard.c @@ -17,11 +17,9 @@ * under the License. */ -#include "config.h" #include "clipboard.h" #include "common/clipboard.h" #include "kubernetes.h" -#include "terminal/terminal.h" #include #include diff --git a/src/protocols/kubernetes/clipboard.h b/src/protocols/kubernetes/clipboard.h index 009219cf..87a393cf 100644 --- a/src/protocols/kubernetes/clipboard.h +++ b/src/protocols/kubernetes/clipboard.h @@ -20,8 +20,6 @@ #ifndef GUAC_KUBERNETES_CLIPBOARD_H #define GUAC_KUBERNETES_CLIPBOARD_H -#include "config.h" - #include /** diff --git a/src/protocols/kubernetes/input.c b/src/protocols/kubernetes/input.c index e73772e5..814578ef 100644 --- a/src/protocols/kubernetes/input.c +++ b/src/protocols/kubernetes/input.c @@ -17,18 +17,15 @@ * under the License. */ -#include "config.h" #include "common/recording.h" -#include "kubernetes.h" #include "input.h" +#include "kubernetes.h" #include "terminal/terminal.h" #include #include #include -#include -#include int guac_kubernetes_user_mouse_handler(guac_user* user, int x, int y, int mask) { diff --git a/src/protocols/kubernetes/input.h b/src/protocols/kubernetes/input.h index ac65835c..6f24cf20 100644 --- a/src/protocols/kubernetes/input.h +++ b/src/protocols/kubernetes/input.h @@ -20,8 +20,6 @@ #ifndef GUAC_KUBERNETES_INPUT_H #define GUAC_KUBERNETES_INPUT_H -#include "config.h" - #include /** diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 9850c6d4..3644d6ec 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -18,8 +18,8 @@ */ #include "config.h" -#include "common/recording.h" #include "kubernetes.h" +#include "common/recording.h" #include "terminal/terminal.h" #include "url.h" @@ -27,16 +27,10 @@ #include #include -#include -#include -#include -#include #include +#include #include #include -#include -#include -#include /** * Handles data received from Kubernetes over WebSocket, decoding the channel diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h index fedf77a2..8c9a25e7 100644 --- a/src/protocols/kubernetes/kubernetes.h +++ b/src/protocols/kubernetes/kubernetes.h @@ -20,13 +20,15 @@ #ifndef GUAC_KUBERNETES_H #define GUAC_KUBERNETES_H -#include "config.h" #include "common/clipboard.h" #include "common/recording.h" #include "settings.h" #include "terminal/terminal.h" +#include #include + +#include #include /** diff --git a/src/protocols/kubernetes/pipe.c b/src/protocols/kubernetes/pipe.c index 242105b1..8f18530a 100644 --- a/src/protocols/kubernetes/pipe.c +++ b/src/protocols/kubernetes/pipe.c @@ -17,12 +17,13 @@ * under the License. */ -#include "config.h" #include "kubernetes.h" -#include "pipe.h" #include "terminal/terminal.h" +#include "pipe.h" +#include #include +#include #include #include diff --git a/src/protocols/kubernetes/pipe.h b/src/protocols/kubernetes/pipe.h index 7acae3cb..47565bfd 100644 --- a/src/protocols/kubernetes/pipe.h +++ b/src/protocols/kubernetes/pipe.h @@ -21,8 +21,6 @@ #ifndef GUAC_KUBERNETES_PIPE_H #define GUAC_KUBERNETES_PIPE_H -#include "config.h" - #include /** diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c index 4f9f3f24..122a8584 100644 --- a/src/protocols/kubernetes/settings.c +++ b/src/protocols/kubernetes/settings.c @@ -17,16 +17,11 @@ * under the License. */ -#include "config.h" - #include "settings.h" #include -#include #include -#include -#include /* Client plugin arguments */ const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index c2e479ea..a86d14a5 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -20,11 +20,8 @@ #ifndef GUAC_KUBERNETES_SETTINGS_H #define GUAC_KUBERNETES_SETTINGS_H -#include "config.h" - #include -#include #include /** diff --git a/src/protocols/kubernetes/url.c b/src/protocols/kubernetes/url.c index cfd6f745..434cc87e 100644 --- a/src/protocols/kubernetes/url.c +++ b/src/protocols/kubernetes/url.c @@ -17,10 +17,10 @@ * under the License. */ -#include "config.h" #include "url.h" #include +#include #include static int guac_kubernetes_is_url_safe(char c) { diff --git a/src/protocols/kubernetes/url.h b/src/protocols/kubernetes/url.h index 19084ee0..285baa21 100644 --- a/src/protocols/kubernetes/url.h +++ b/src/protocols/kubernetes/url.h @@ -20,8 +20,6 @@ #ifndef GUAC_KUBERNETES_URL_H #define GUAC_KUBERNETES_URL_H -#include "config.h" - /** * The maximum number of characters allowed in the full path for any Kubernetes * endpoint. diff --git a/src/protocols/kubernetes/user.c b/src/protocols/kubernetes/user.c index 62666cb5..f90260e7 100644 --- a/src/protocols/kubernetes/user.c +++ b/src/protocols/kubernetes/user.c @@ -17,9 +17,8 @@ * under the License. */ -#include "config.h" - #include "clipboard.h" +#include "common/cursor.h" #include "input.h" #include "kubernetes.h" #include "pipe.h" @@ -28,11 +27,12 @@ #include "user.h" #include +#include #include #include #include -#include +#include int guac_kubernetes_user_join_handler(guac_user* user, int argc, char** argv) { diff --git a/src/protocols/kubernetes/user.h b/src/protocols/kubernetes/user.h index d235b2b7..55d49fdf 100644 --- a/src/protocols/kubernetes/user.h +++ b/src/protocols/kubernetes/user.h @@ -20,8 +20,6 @@ #ifndef GUAC_KUBERNETES_USER_H #define GUAC_KUBERNETES_USER_H -#include "config.h" - #include /** From 5e3aec6df2ec3578e47030665f7b12c3c3cadd51 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 21:08:19 -0700 Subject: [PATCH 15/18] GUACAMOLE-623: Add missing documentation for URL character test. --- src/protocols/kubernetes/url.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/protocols/kubernetes/url.c b/src/protocols/kubernetes/url.c index 434cc87e..78c116e5 100644 --- a/src/protocols/kubernetes/url.c +++ b/src/protocols/kubernetes/url.c @@ -23,6 +23,17 @@ #include #include +/** + * Returns whether the given character is a character that need not be + * escaped when included as part of a component of a URL. + * + * @param c + * The character to test. + * + * @return + * Zero if the character does not need to be escaped when included as + * part of a component of a URL, non-zero otherwise. + */ static int guac_kubernetes_is_url_safe(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') From 2e50573531411a49c98dcb44a29fe2ad8a983609 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 22:55:02 -0700 Subject: [PATCH 16/18] GUACAMOLE-623: Move I/O-related functions into separate files. --- src/protocols/kubernetes/Makefile.am | 2 + src/protocols/kubernetes/io.c | 143 ++++++++++++++++++++++ src/protocols/kubernetes/io.h | 144 ++++++++++++++++++++++ src/protocols/kubernetes/kubernetes.c | 165 +------------------------- src/protocols/kubernetes/kubernetes.h | 59 +-------- 5 files changed, 292 insertions(+), 221 deletions(-) create mode 100644 src/protocols/kubernetes/io.c create mode 100644 src/protocols/kubernetes/io.h diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am index 9e50feb6..e818ff72 100644 --- a/src/protocols/kubernetes/Makefile.am +++ b/src/protocols/kubernetes/Makefile.am @@ -26,6 +26,7 @@ libguac_client_kubernetes_la_SOURCES = \ client.c \ clipboard.c \ input.c \ + io.c \ pipe.c \ settings.c \ kubernetes.c \ @@ -36,6 +37,7 @@ noinst_HEADERS = \ client.h \ clipboard.h \ input.h \ + io.h \ pipe.h \ settings.h \ kubernetes.h \ diff --git a/src/protocols/kubernetes/io.c b/src/protocols/kubernetes/io.c new file mode 100644 index 00000000..bfa37b1d --- /dev/null +++ b/src/protocols/kubernetes/io.c @@ -0,0 +1,143 @@ +/* + * 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 "kubernetes.h" +#include "terminal/terminal.h" + +#include +#include + +#include +#include +#include + +void guac_kubernetes_receive_data(guac_client* client, + const char* buffer, size_t length) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + /* Strip channel index from beginning of buffer */ + int channel = *(buffer++); + length--; + + switch (channel) { + + /* Write STDOUT / STDERR directly to terminal as output */ + case GUAC_KUBERNETES_CHANNEL_STDOUT: + case GUAC_KUBERNETES_CHANNEL_STDERR: + guac_terminal_write(kubernetes_client->term, buffer, length); + break; + + /* Ignore data on other channels */ + default: + guac_client_log(client, GUAC_LOG_DEBUG, "Received %i bytes along " + "channel %i.", length, channel); + + } + +} + +void guac_kubernetes_send_message(guac_client* client, + int channel, const char* data, int length) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); + + /* Add message to buffer if space is available */ + if (kubernetes_client->outbound_messages_waiting + < GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES) { + + /* Calculate storage position of next message */ + int index = (kubernetes_client->outbound_messages_top + + kubernetes_client->outbound_messages_waiting) + % GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; + + /* Obtain pointer to message slot at calculated position */ + guac_kubernetes_message* message = + &(kubernetes_client->outbound_messages[index]); + + /* Copy details of message into buffer */ + message->channel = channel; + memcpy(message->data, data, length); + message->length = length; + + /* One more message is now waiting */ + kubernetes_client->outbound_messages_waiting++; + + /* Notify libwebsockets that we need a callback to send pending + * messages */ + lws_callback_on_writable(kubernetes_client->wsi); + lws_cancel_service(kubernetes_client->context); + + } + + /* Warn if data has to be dropped */ + else + guac_client_log(client, GUAC_LOG_WARNING, "Send buffer could not be " + "flushed in time to handle additional data. Outbound " + "message dropped."); + + pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); + +} + +bool guac_kubernetes_write_pending_message(guac_client* client) { + + bool messages_remain; + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); + + /* Send one message from top of buffer */ + if (kubernetes_client->outbound_messages_waiting > 0) { + + /* Obtain pointer to message at top */ + int top = kubernetes_client->outbound_messages_top; + guac_kubernetes_message* message = + &(kubernetes_client->outbound_messages[top]); + + /* Write message including channel index */ + lws_write(kubernetes_client->wsi, + ((unsigned char*) message) + LWS_PRE, + message->length + 1, LWS_WRITE_BINARY); + + /* Advance top to next message */ + kubernetes_client->outbound_messages_top++; + kubernetes_client->outbound_messages_top %= + GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; + + /* One less message is waiting */ + kubernetes_client->outbound_messages_waiting--; + + } + + /* Record whether messages remained at time of completion */ + messages_remain = (kubernetes_client->outbound_messages_waiting > 0); + + pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); + + return messages_remain; + +} + + diff --git a/src/protocols/kubernetes/io.h b/src/protocols/kubernetes/io.h new file mode 100644 index 00000000..40f2c69a --- /dev/null +++ b/src/protocols/kubernetes/io.h @@ -0,0 +1,144 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_IO_H +#define GUAC_KUBERNETES_IO_H + +#include +#include + +#include +#include + +/** + * The maximum amount of data to include in any particular WebSocket message + * to Kubernetes. This excludes the storage space required for the channel + * index. + */ +#define GUAC_KUBERNETES_MAX_MESSAGE_SIZE 1024 + +/** + * The index of the Kubernetes channel used for STDIN. + */ +#define GUAC_KUBERNETES_CHANNEL_STDIN 0 + +/** + * The index of the Kubernetes channel used for STDOUT. + */ +#define GUAC_KUBERNETES_CHANNEL_STDOUT 1 + +/** + * The index of the Kubernetes channel used for STDERR. + */ +#define GUAC_KUBERNETES_CHANNEL_STDERR 2 + +/** + * The index of the Kubernetes channel used for terminal resize messages. + */ +#define GUAC_KUBERNETES_CHANNEL_RESIZE 4 + +/** + * An outbound message to be received by Kubernetes over WebSocket. + */ +typedef struct guac_kubernetes_message { + + /** + * lws_write() requires leading padding of LWS_PRE bytes to provide + * scratch space for WebSocket framing. + */ + uint8_t _padding[LWS_PRE]; + + /** + * The index of the channel receiving the data, such as + * GUAC_KUBERNETES_CHANNEL_STDIN. + */ + uint8_t channel; + + /** + * The data that should be sent to Kubernetes (along with the channel + * index). + */ + char data[GUAC_KUBERNETES_MAX_MESSAGE_SIZE]; + + /** + * The length of the data to be sent, excluding the channel index. + */ + int length; + +} guac_kubernetes_message; + + +/** + * Handles data received from Kubernetes over WebSocket, decoding the channel + * index of the received data and forwarding that data accordingly. + * + * @param client + * The guac_client associated with the connection to Kubernetes. + * + * @param buffer + * The data received from Kubernetes. + * + * @param length + * The size of the data received from Kubernetes, in bytes. + */ +void guac_kubernetes_receive_data(guac_client* client, + const char* buffer, size_t length); + +/** + * Requests that the given data be sent along the given channel to the + * Kubernetes server when the WebSocket connection is next available for + * writing. If the WebSocket connection has not been available for writing for + * long enough that the outbound message buffer is full, the request to send + * this particular message will be dropped. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param channel + * The Kubernetes channel on which to send the message, + * such as GUAC_KUBERNETES_CHANNEL_STDIN. + * + * @param data + * A buffer containing the data to send. + * + * @param length + * The number of bytes to send. + */ +void guac_kubernetes_send_message(guac_client* client, + int channel, const char* data, int length); + +/** + * Writes the oldest pending message within the outbound message queue, + * as scheduled with guac_kubernetes_send_message(), removing that message + * from the queue. This function MAY NOT be invoked outside the libwebsockets + * event callback and MUST only be invoked in the context of a + * LWS_CALLBACK_CLIENT_WRITEABLE event. If no messages are pending, this + * function has no effect. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @return + * true if messages still remain to be written within the outbound message + * queue, false otherwise. + */ +bool guac_kubernetes_write_pending_message(guac_client* client); + +#endif + diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 3644d6ec..4e7928ed 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -18,8 +18,9 @@ */ #include "config.h" -#include "kubernetes.h" #include "common/recording.h" +#include "io.h" +#include "kubernetes.h" #include "terminal/terminal.h" #include "url.h" @@ -30,168 +31,6 @@ #include #include #include -#include - -/** - * Handles data received from Kubernetes over WebSocket, decoding the channel - * index of the received data and forwarding that data accordingly. - * - * @param client - * The guac_client associated with the connection to Kubernetes. - * - * @param buffer - * The data received from Kubernetes. - * - * @param length - * The size of the data received from Kubernetes, in bytes. - */ -static void guac_kubernetes_receive_data(guac_client* client, - const char* buffer, size_t length) { - - guac_kubernetes_client* kubernetes_client = - (guac_kubernetes_client*) client->data; - - /* Strip channel index from beginning of buffer */ - int channel = *(buffer++); - length--; - - switch (channel) { - - /* Write STDOUT / STDERR directly to terminal as output */ - case GUAC_KUBERNETES_CHANNEL_STDOUT: - case GUAC_KUBERNETES_CHANNEL_STDERR: - guac_terminal_write(kubernetes_client->term, buffer, length); - break; - - /* Ignore data on other channels */ - default: - guac_client_log(client, GUAC_LOG_DEBUG, "Received %i bytes along " - "channel %i.", length, channel); - - } - -} - -/** - * Requests that the given data be sent along the given channel to the - * Kubernetes server when the WebSocket connection is next available for - * writing. If the WebSocket connection has not been available for writing for - * long enough that the outbound message buffer is full, the request to send - * this particular message will be dropped. - * - * @param client - * The guac_client associated with the Kubernetes connection. - * - * @param channel - * The Kubernetes channel on which to send the message, - * such as GUAC_KUBERNETES_CHANNEL_STDIN. - * - * @param data - * A buffer containing the data to send. - * - * @param length - * The number of bytes to send. - */ -static void guac_kubernetes_send_message(guac_client* client, - int channel, const char* data, int length) { - - guac_kubernetes_client* kubernetes_client = - (guac_kubernetes_client*) client->data; - - pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); - - /* Add message to buffer if space is available */ - if (kubernetes_client->outbound_messages_waiting - < GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES) { - - /* Calculate storage position of next message */ - int index = (kubernetes_client->outbound_messages_top - + kubernetes_client->outbound_messages_waiting) - % GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; - - /* Obtain pointer to message slot at calculated position */ - guac_kubernetes_message* message = - &(kubernetes_client->outbound_messages[index]); - - /* Copy details of message into buffer */ - message->channel = channel; - memcpy(message->data, data, length); - message->length = length; - - /* One more message is now waiting */ - kubernetes_client->outbound_messages_waiting++; - - /* Notify libwebsockets that we need a callback to send pending - * messages */ - lws_callback_on_writable(kubernetes_client->wsi); - lws_cancel_service(kubernetes_client->context); - - } - - /* Warn if data has to be dropped */ - else - guac_client_log(client, GUAC_LOG_WARNING, "Send buffer could not be " - "flushed in time to handle additional data. Outbound " - "message dropped."); - - pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); - -} - -/** - * Writes the oldest pending message within the outbound message queue, - * as scheduled with guac_kubernetes_send_message(), removing that message - * from the queue. This function MAY NOT be invoked outside the libwebsockets - * event callback and MUST only be invoked in the context of a - * LWS_CALLBACK_CLIENT_WRITEABLE event. If no messages are pending, this - * function has no effect. - * - * @param client - * The guac_client associated with the Kubernetes connection. - * - * @return - * true if messages still remain to be written within the outbound message - * queue, false otherwise. - */ -static bool guac_kubernetes_write_pending_message(guac_client* client) { - - bool messages_remain; - guac_kubernetes_client* kubernetes_client = - (guac_kubernetes_client*) client->data; - - pthread_mutex_lock(&(kubernetes_client->outbound_message_lock)); - - /* Send one message from top of buffer */ - if (kubernetes_client->outbound_messages_waiting > 0) { - - /* Obtain pointer to message at top */ - int top = kubernetes_client->outbound_messages_top; - guac_kubernetes_message* message = - &(kubernetes_client->outbound_messages[top]); - - /* Write message including channel index */ - lws_write(kubernetes_client->wsi, - ((unsigned char*) message) + LWS_PRE, - message->length + 1, LWS_WRITE_BINARY); - - /* Advance top to next message */ - kubernetes_client->outbound_messages_top++; - kubernetes_client->outbound_messages_top %= - GUAC_KUBERNETES_MAX_OUTBOUND_MESSAGES; - - /* One less message is waiting */ - kubernetes_client->outbound_messages_waiting--; - - } - - /* Record whether messages remained at time of completion */ - messages_remain = (kubernetes_client->outbound_messages_waiting > 0); - - pthread_mutex_unlock(&(kubernetes_client->outbound_message_lock)); - - return messages_remain; - -} /** * Callback invoked by libwebsockets for events related to a WebSocket being diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h index 8c9a25e7..c37ca4cf 100644 --- a/src/protocols/kubernetes/kubernetes.h +++ b/src/protocols/kubernetes/kubernetes.h @@ -22,6 +22,7 @@ #include "common/clipboard.h" #include "common/recording.h" +#include "io.h" #include "settings.h" #include "terminal/terminal.h" @@ -29,7 +30,6 @@ #include #include -#include /** * The name of the WebSocket protocol specific to Kubernetes which should be @@ -37,33 +37,6 @@ */ #define GUAC_KUBERNETES_LWS_PROTOCOL "v4.channel.k8s.io" -/** - * The index of the Kubernetes channel used for STDIN. - */ -#define GUAC_KUBERNETES_CHANNEL_STDIN 0 - -/** - * The index of the Kubernetes channel used for STDOUT. - */ -#define GUAC_KUBERNETES_CHANNEL_STDOUT 1 - -/** - * The index of the Kubernetes channel used for STDERR. - */ -#define GUAC_KUBERNETES_CHANNEL_STDERR 2 - -/** - * The index of the Kubernetes channel used for terminal resize messages. - */ -#define GUAC_KUBERNETES_CHANNEL_RESIZE 4 - -/** - * The maximum amount of data to include in any particular WebSocket message - * to Kubernetes. This excludes the storage space required for the channel - * index. - */ -#define GUAC_KUBERNETES_MAX_MESSAGE_SIZE 1024 - /** * The maximum number of messages to allow within the outbound message buffer. * If messages are sent despite the buffer being full, those messages will be @@ -77,36 +50,6 @@ */ #define GUAC_KUBERNETES_SERVICE_INTERVAL 1000 -/** - * An outbound message to be received by Kubernetes over WebSocket. - */ -typedef struct guac_kubernetes_message { - - /** - * lws_write() requires leading padding of LWS_PRE bytes to provide - * scratch space for WebSocket framing. - */ - uint8_t _padding[LWS_PRE]; - - /** - * The index of the channel receiving the data, such as - * GUAC_KUBERNETES_CHANNEL_STDIN. - */ - uint8_t channel; - - /** - * The data that should be sent to Kubernetes (along with the channel - * index). - */ - char data[GUAC_KUBERNETES_MAX_MESSAGE_SIZE]; - - /** - * The length of the data to be sent, excluding the channel index. - */ - int length; - -} guac_kubernetes_message; - /** * Kubernetes-specific client data. */ From 83a531bc89a5c79371f42d1ec5142c484a08479a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 11 Sep 2018 03:03:17 -0700 Subject: [PATCH 17/18] GUACAMOLE-623: Add support for SSL. --- configure.ac | 1 + src/protocols/kubernetes/Makefile.am | 3 + src/protocols/kubernetes/client.c | 16 +- src/protocols/kubernetes/client.h | 7 + src/protocols/kubernetes/kubernetes.c | 46 +++--- src/protocols/kubernetes/settings.c | 48 +++--- src/protocols/kubernetes/settings.h | 24 +-- src/protocols/kubernetes/ssl.c | 210 ++++++++++++++++++++++++++ src/protocols/kubernetes/ssl.h | 41 +++++ 9 files changed, 326 insertions(+), 70 deletions(-) create mode 100644 src/protocols/kubernetes/ssl.c create mode 100644 src/protocols/kubernetes/ssl.h diff --git a/configure.ac b/configure.ac index df36eabe..d26db39f 100644 --- a/configure.ac +++ b/configure.ac @@ -1225,6 +1225,7 @@ AC_ARG_ENABLE([kubernetes], AM_CONDITIONAL([ENABLE_KUBERNETES], [test "x${enable_kubernetes}" = "xyes" \ -a "x${have_libwebsockets}" = "xyes" \ + -a "x${have_ssl}" = "xyes" \ -a "x${have_terminal}" = "xyes"]) # diff --git a/src/protocols/kubernetes/Makefile.am b/src/protocols/kubernetes/Makefile.am index e818ff72..56db4d64 100644 --- a/src/protocols/kubernetes/Makefile.am +++ b/src/protocols/kubernetes/Makefile.am @@ -29,6 +29,7 @@ libguac_client_kubernetes_la_SOURCES = \ io.c \ pipe.c \ settings.c \ + ssl.c \ kubernetes.c \ url.c \ user.c @@ -40,6 +41,7 @@ noinst_HEADERS = \ io.h \ pipe.h \ settings.h \ + ssl.h \ kubernetes.h \ url.h \ user.h @@ -57,5 +59,6 @@ libguac_client_kubernetes_la_LIBADD = \ libguac_client_kubernetes_la_LDFLAGS = \ -version-info 0:0:0 \ @PTHREAD_LIBS@ \ + @SSL_LIBS@ \ @WEBSOCKETS_LIBS@ diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c index 58f4728a..331e03d7 100644 --- a/src/protocols/kubernetes/client.c +++ b/src/protocols/kubernetes/client.c @@ -32,12 +32,7 @@ #include #include -/** - * Static reference to the guac_client associated with the active Kubernetes - * connection. As guacd guarantees that each main client connection is - * isolated within its own process, this is safe. - */ -static guac_client* guac_kubernetes_lws_log_client = NULL; +guac_client* guac_kubernetes_lws_current_client = NULL; /** * Logging callback invoked by libwebsockets to log a single line of logging @@ -53,15 +48,18 @@ static guac_client* guac_kubernetes_lws_log_client = NULL; * The line of logging output to log. */ static void guac_kubernetes_log(int level, const char* line) { - if (guac_kubernetes_lws_log_client != NULL) - guac_client_log(guac_kubernetes_lws_log_client, GUAC_LOG_DEBUG, + if (guac_kubernetes_lws_current_client != NULL) + guac_client_log(guac_kubernetes_lws_current_client, GUAC_LOG_DEBUG, "libwebsockets: %s", line); } int guac_client_init(guac_client* client) { + /* Ensure reference to main guac_client remains available in all + * libwebsockets contexts */ + guac_kubernetes_lws_current_client = client; + /* Redirect libwebsockets logging */ - guac_kubernetes_lws_log_client = client; lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO, guac_kubernetes_log); diff --git a/src/protocols/kubernetes/client.h b/src/protocols/kubernetes/client.h index 0b847da4..ec4ba326 100644 --- a/src/protocols/kubernetes/client.h +++ b/src/protocols/kubernetes/client.h @@ -27,6 +27,13 @@ */ #define GUAC_KUBERNETES_CLIPBOARD_MAX_LENGTH 262144 +/** + * Static reference to the guac_client associated with the active Kubernetes + * connection. While libwebsockets provides some means of storing and + * retrieving custom data in some structures, this is not always available. + */ +extern guac_client* guac_kubernetes_lws_current_client; + /** * Free handler. Required by libguac and called when the guac_client is * disconnected and must be cleaned up. diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 4e7928ed..f314c597 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -18,9 +18,11 @@ */ #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" @@ -43,8 +45,9 @@ * The reason (event) that this callback was invoked. * * @param user - * Arbitrary data assocated with the WebSocket session. This will always - * be a pointer to the guac_client instance. + * 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. @@ -60,14 +63,19 @@ static int guac_kubernetes_lws_callback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t length) { - /* Request connection closure if client is stopped (note that the user - * pointer passed by libwebsockets may be NULL for some events) */ - guac_client* client = (guac_client*) user; - if (client != NULL && client->state != GUAC_CLIENT_RUNNING) + guac_client* client = guac_kubernetes_lws_current_client; + + /* Do not handle any further events if connection is closing */ + if (client->state != GUAC_CLIENT_RUNNING) return lws_callback_http_dummy(wsi, reason, user, in, length); 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, @@ -256,29 +264,13 @@ void* guac_kubernetes_client_thread(void* data) { }; /* If requested, use an SSL/TLS connection for communication with - * Kubernetes */ + * 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) { - - /* Enable use of SSL/TLS */ context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; - connection_info.ssl_connection = LCCSCF_USE_SSL; - - /* Bypass certificate checks if requested */ - if (settings->ignore_cert) { - connection_info.ssl_connection |= - LCCSCF_ALLOW_SELFSIGNED - | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK - | LCCSCF_ALLOW_EXPIRED; - } - - /* Otherwise use the given CA certificate to validate (if any) */ - else - context_info.client_ssl_ca_filepath = settings->ca_cert_file; - - /* Certificate and key file for SSL/TLS client auth */ - context_info.client_ssl_cert_filepath = settings->client_cert_file; - context_info.client_ssl_private_key_filepath = settings->client_key_file; - + connection_info.ssl_connection = LCCSCF_USE_SSL + | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; } /* Create libwebsockets context */ diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c index 122a8584..4f00a445 100644 --- a/src/protocols/kubernetes/settings.c +++ b/src/protocols/kubernetes/settings.c @@ -31,9 +31,9 @@ const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { "pod", "container", "use-ssl", - "client-cert-file", - "client-key-file", - "ca-cert-file", + "client-cert", + "client-key", + "ca-cert", "ignore-cert", "font-name", "font-size", @@ -89,24 +89,26 @@ enum KUBERNETES_ARGS_IDX { IDX_USE_SSL, /** - * The filename of the certificate to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The certificate to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - IDX_CLIENT_CERT_FILE, + IDX_CLIENT_CERT, /** - * The filename of the key to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The key to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - IDX_CLIENT_KEY_FILE, + IDX_CLIENT_KEY, /** - * The filename of the certificate of the certificate authority that signed - * the certificate of the Kubernetes server. + * The certificate of the certificate authority that signed the certificate + * of the Kubernetes server, in PEM format. If omitted. verification of + * the Kubernetes server certificate will use the systemwide certificate + * authorities. */ - IDX_CA_CERT_FILE, + IDX_CA_CERT, /** * Whether the certificate used by the Kubernetes server for SSL/TLS should @@ -264,17 +266,17 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, /* Read SSL/TLS connection details only if enabled */ if (settings->use_ssl) { - settings->client_cert_file = + settings->client_cert = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, - argv, IDX_CLIENT_CERT_FILE, NULL); + argv, IDX_CLIENT_CERT, NULL); - settings->client_key_file = + settings->client_key = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, - argv, IDX_CLIENT_KEY_FILE, NULL); + argv, IDX_CLIENT_KEY, NULL); - settings->ca_cert_file = + settings->ca_cert = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, - argv, IDX_CA_CERT_FILE, NULL); + argv, IDX_CA_CERT, NULL); settings->ignore_cert = guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, @@ -378,9 +380,9 @@ void guac_kubernetes_settings_free(guac_kubernetes_settings* settings) { free(settings->kubernetes_container); /* Free SSL/TLS details */ - free(settings->client_cert_file); - free(settings->client_key_file); - free(settings->ca_cert_file); + free(settings->client_cert); + free(settings->client_key); + free(settings->ca_cert); /* Free display preferences */ free(settings->font_name); diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index a86d14a5..6267a18b 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -103,24 +103,26 @@ typedef struct guac_kubernetes_settings { bool use_ssl; /** - * The filename of the certificate to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The certificate to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - char* client_cert_file; + char* client_cert; /** - * The filename of the key to use if performing SSL/TLS client - * authentication to authenticate with the Kubernetes server. If omitted, - * SSL client authentication will not be performed. + * The key to use if performing SSL/TLS client authentication to + * authenticate with the Kubernetes server, in PEM format. If omitted, SSL + * client authentication will not be performed. */ - char* client_key_file; + char* client_key; /** - * The filename of the certificate of the certificate authority that signed - * the certificate of the Kubernetes server. + * The certificate of the certificate authority that signed the certificate + * of the Kubernetes server, in PEM format. If omitted. verification of + * the Kubernetes server certificate will use the systemwide certificate + * authorities. */ - char* ca_cert_file; + char* ca_cert; /** * Whether the certificate used by the Kubernetes server for SSL/TLS should diff --git a/src/protocols/kubernetes/ssl.c b/src/protocols/kubernetes/ssl.c new file mode 100644 index 00000000..6ebafc61 --- /dev/null +++ b/src/protocols/kubernetes/ssl.c @@ -0,0 +1,210 @@ +/* + * 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 "kubernetes.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include +#include + +/** + * Tests whether the given hostname is, in fact, an IP address. + * + * @param hostname + * The hostname to test. + * + * @return + * Non-zero if the given hostname is an IP address, zero otherwise. + */ +static int guac_kubernetes_is_address(const char* hostname) { + + /* Attempt to interpret the hostname as an IP address */ + ASN1_OCTET_STRING* ip = a2i_IPADDRESS(hostname); + + /* If unsuccessful, the hostname is not an IP address */ + if (ip == NULL) + return 0; + + /* Converted hostname must be freed */ + ASN1_OCTET_STRING_free(ip); + return 1; + +} + +/** + * Parses the given PEM certificate, returning a new OpenSSL X509 structure + * representing that certificate. + * + * @param pem + * The PEM certificate. + * + * @return + * An X509 structure representing the given certificate, or NULL if the + * certificate was unreadable. + */ +static X509* guac_kubernetes_read_cert(char* pem) { + + /* Prepare a BIO which provides access to the in-memory CA cert */ + BIO* bio = BIO_new_mem_buf(pem, -1); + if (bio == NULL) + return NULL; + + /* Read the CA cert as PEM */ + X509* certificate = PEM_read_bio_X509(bio, NULL, NULL, NULL); + if (certificate == NULL) { + BIO_free(bio); + return NULL; + } + + return certificate; + +} + +/** + * Parses the given PEM private key, returning a new OpenSSL EVP_PKEY structure + * representing that key. + * + * @param pem + * The PEM private key. + * + * @return + * An EVP_KEY representing the given private key, or NULL if the private + * key was unreadable. + */ +static EVP_PKEY* guac_kubernetes_read_key(char* pem) { + + /* Prepare a BIO which provides access to the in-memory key */ + BIO* bio = BIO_new_mem_buf(pem, -1); + if (bio == NULL) + return NULL; + + /* Read the private key as PEM */ + EVP_PKEY* key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + if (key == NULL) { + BIO_free(bio); + return NULL; + } + + return key; + +} + +void guac_kubernetes_init_ssl(guac_client* client, SSL_CTX* context) { + + guac_kubernetes_client* kubernetes_client = + (guac_kubernetes_client*) client->data; + + guac_kubernetes_settings* settings = kubernetes_client->settings; + + /* Bypass certificate checks if requested */ + if (settings->ignore_cert) + SSL_CTX_set_verify(context, SSL_VERIFY_NONE, NULL); + + /* Otherwise use the given CA certificate to validate (if any) */ + else if (settings->ca_cert != NULL) { + + /* Read CA certificate from configuration data */ + X509* ca_cert = guac_kubernetes_read_cert(settings->ca_cert); + if (ca_cert == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided CA certificate is unreadable"); + return; + } + + /* Add certificate to CA store */ + X509_STORE* ca_store = SSL_CTX_get_cert_store(context); + if (!X509_STORE_add_cert(ca_store, ca_cert)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to add CA certificate to certificate store of " + "SSL context"); + return; + } + + } + + /* Certificate for SSL/TLS client auth */ + if (settings->client_cert != NULL) { + + /* Read client certificate from configuration data */ + X509* client_cert = guac_kubernetes_read_cert(settings->client_cert); + if (client_cert == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided client certificate is unreadable"); + return; + } + + /* Use parsed certificate for authentication */ + if (!SSL_CTX_use_certificate(context, client_cert)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Client certificate could not be used for SSL/TLS " + "client authentication"); + return; + } + + } + + /* Private key for SSL/TLS client auth */ + if (settings->client_key != NULL) { + + /* Read client private key from configuration data */ + EVP_PKEY* client_key = guac_kubernetes_read_key(settings->client_key); + if (client_key == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Provided client private key is unreadable"); + return; + } + + /* Use parsed key for authentication */ + if (!SSL_CTX_use_PrivateKey(context, client_key)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Client private key could not be used for SSL/TLS " + "client authentication"); + return; + } + + } + + /* Enable hostname checking */ + X509_VERIFY_PARAM *param = SSL_CTX_get0_param(context); + X509_VERIFY_PARAM_set_hostflags(param, + X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + + /* Validate properly depending on whether hostname is an IP address */ + if (guac_kubernetes_is_address(settings->hostname)) { + if (!X509_VERIFY_PARAM_set1_ip_asc(param, settings->hostname)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Server IP address validation could not be enabled"); + return; + } + } + else { + if (!X509_VERIFY_PARAM_set1_host(param, settings->hostname, 0)) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Server hostname validation could not be enabled"); + return; + } + } + +} + diff --git a/src/protocols/kubernetes/ssl.h b/src/protocols/kubernetes/ssl.h new file mode 100644 index 00000000..cca02bdb --- /dev/null +++ b/src/protocols/kubernetes/ssl.h @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#ifndef GUAC_KUBERNETES_SSL_H +#define GUAC_KUBERNETES_SSL_H + +#include "settings.h" + +#include + +/** + * Initializes the given SSL/TLS context using the configuration parameters + * associated with the given guac_client, setting up hostname/address + * validation and client authentication. + * + * @param client + * The guac_client associated with the Kubernetes connection. + * + * @param context + * The SSL_CTX in use by libwebsockets. + */ +void guac_kubernetes_init_ssl(guac_client* client, SSL_CTX* context); + +#endif + From 61df2956b322a01a777c8169cbbd2b2a2877d696 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 11 Sep 2018 03:17:00 -0700 Subject: [PATCH 18/18] GUACAMOLE-623: Clean up logging (libwebsockets adds newline characters). --- src/protocols/kubernetes/client.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c index 331e03d7..1a1eb3a7 100644 --- a/src/protocols/kubernetes/client.c +++ b/src/protocols/kubernetes/client.c @@ -48,9 +48,32 @@ guac_client* guac_kubernetes_lws_current_client = NULL; * The line of logging output to log. */ static void guac_kubernetes_log(int level, const char* line) { - if (guac_kubernetes_lws_current_client != NULL) - guac_client_log(guac_kubernetes_lws_current_client, GUAC_LOG_DEBUG, - "libwebsockets: %s", line); + + char buffer[1024]; + + /* Drop log message if there's nowhere to log yet */ + if (guac_kubernetes_lws_current_client == NULL) + return; + + /* Trim length of line to fit buffer (plus null terminator) */ + int length = strlen(line); + if (length > sizeof(buffer) - 1) + length = sizeof(buffer) - 1; + + /* Copy as much of the received line as will fit in the buffer */ + memcpy(buffer, line, length); + + /* If the line ends with a newline character, trim the character */ + if (length > 0 && buffer[length - 1] == '\n') + length--; + + /* Null-terminate the trimmed string */ + buffer[length] = '\0'; + + /* Log using guacd's own log facilities */ + guac_client_log(guac_kubernetes_lws_current_client, GUAC_LOG_DEBUG, + "libwebsockets: %s", buffer); + } int guac_client_init(guac_client* client) {