GUACAMOLE-632: Merge dynamic JPEG/WebP quality scaling.
diff --git a/Makefile.am b/Makefile.am
index e923376..91c8abe 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -52,6 +52,10 @@
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 6b20c97..d26db39 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1173,6 +1173,62 @@
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"],
+ [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
+# 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 <libwebsockets.h>])
+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_ssl}" = "xyes" \
+ -a "x${have_terminal}" = "xyes"])
+
+#
# guacd
#
@@ -1230,6 +1286,7 @@
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 +1297,11 @@
# 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 +1345,17 @@
libVNCServer ........ ${have_libvncserver}
libvorbis ........... ${have_vorbis}
libpulse ............ ${have_pulse}
+ libwebsockets ....... ${have_libwebsockets}
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 0000000..56db4d6
--- /dev/null
+++ b/src/protocols/kubernetes/Makefile.am
@@ -0,0 +1,64 @@
+#
+# 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 \
+ io.c \
+ pipe.c \
+ settings.c \
+ ssl.c \
+ kubernetes.c \
+ url.c \
+ user.c
+
+noinst_HEADERS = \
+ client.h \
+ clipboard.h \
+ input.h \
+ io.h \
+ pipe.h \
+ settings.h \
+ ssl.h \
+ kubernetes.h \
+ url.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@ \
+ @SSL_LIBS@ \
+ @WEBSOCKETS_LIBS@
+
diff --git a/src/protocols/kubernetes/client.c b/src/protocols/kubernetes/client.c
new file mode 100644
index 0000000..1a1eb3a
--- /dev/null
+++ b/src/protocols/kubernetes/client.c
@@ -0,0 +1,133 @@
+/*
+ * 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 "client.h"
+#include "common/clipboard.h"
+#include "kubernetes.h"
+#include "settings.h"
+#include "user.h"
+
+#include <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <langinfo.h>
+#include <locale.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <string.h>
+
+guac_client* guac_kubernetes_lws_current_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) {
+
+ 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) {
+
+ /* Ensure reference to main guac_client remains available in all
+ * libwebsockets contexts */
+ guac_kubernetes_lws_current_client = client;
+
+ /* Redirect libwebsockets logging */
+ lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO,
+ guac_kubernetes_log);
+
+ /* 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;
+
+ /* Wait client thread to terminate */
+ 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 0000000..ec4ba32
--- /dev/null
+++ b/src/protocols/kubernetes/client.h
@@ -0,0 +1,44 @@
+/*
+ * 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 <guacamole/client.h>
+
+/**
+ * The maximum number of bytes to allow within the clipboard.
+ */
+#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.
+ */
+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 0000000..f168206
--- /dev/null
+++ b/src/protocols/kubernetes/clipboard.c
@@ -0,0 +1,65 @@
+/*
+ * 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 "clipboard.h"
+#include "common/clipboard.h"
+#include "kubernetes.h"
+
+#include <guacamole/client.h>
+#include <guacamole/stream.h>
+#include <guacamole/user.h>
+
+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 0000000..87a393c
--- /dev/null
+++ b/src/protocols/kubernetes/clipboard.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_CLIPBOARD_H
+#define GUAC_KUBERNETES_CLIPBOARD_H
+
+#include <guacamole/user.h>
+
+/**
+ * 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 0000000..814578e
--- /dev/null
+++ b/src/protocols/kubernetes/input.c
@@ -0,0 +1,94 @@
+/*
+ * 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 "common/recording.h"
+#include "input.h"
+#include "kubernetes.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <guacamole/user.h>
+
+#include <stdlib.h>
+
+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);
+
+ /* 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/input.h b/src/protocols/kubernetes/input.h
new file mode 100644
index 0000000..6f24cf2
--- /dev/null
+++ b/src/protocols/kubernetes/input.h
@@ -0,0 +1,44 @@
+/*
+ * 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 <guacamole/user.h>
+
+/**
+ * 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/io.c b/src/protocols/kubernetes/io.c
new file mode 100644
index 0000000..bfa37b1
--- /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 <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <string.h>
+
+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 0000000..40f2c69
--- /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 <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+ * 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
new file mode 100644
index 0000000..f314c59
--- /dev/null
+++ b/src/protocols/kubernetes/kubernetes.c
@@ -0,0 +1,387 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "config.h"
+#include "client.h"
+#include "common/recording.h"
+#include "io.h"
+#include "kubernetes.h"
+#include "ssl.h"
+#include "terminal/terminal.h"
+#include "url.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <libwebsockets.h>
+
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+/**
+ * Callback invoked by libwebsockets for events related to a WebSocket being
+ * used for communicating with an attached Kubernetes pod.
+ *
+ * @param wsi
+ * The libwebsockets handle for the WebSocket connection.
+ *
+ * @param reason
+ * The reason (event) that this callback was invoked.
+ *
+ * @param user
+ * Arbitrary data assocated with the WebSocket session. In some cases,
+ * this is actually event-specific data (such as the
+ * LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERT event).
+ *
+ * @param in
+ * A pointer to arbitrary, reason-specific data.
+ *
+ * @param length
+ * An arbitrary, reason-specific length value.
+ *
+ * @return
+ * An undocumented integer value related the success of handling the
+ * event, or -1 if the WebSocket connection should be closed.
+ */
+static int guac_kubernetes_lws_callback(struct lws* wsi,
+ enum lws_callback_reasons reason, void* user,
+ void* in, size_t length) {
+
+ guac_client* client = guac_kubernetes_lws_current_client;
+
+ /* 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,
+ "Error connecting to Kubernetes server: %s",
+ in != NULL ? (char*) in : "(no error description "
+ "available)");
+ break;
+
+ /* Connected / logged in */
+ case LWS_CALLBACK_CLIENT_ESTABLISHED:
+ guac_client_log(client, GUAC_LOG_INFO,
+ "Kubernetes connection successful.");
+
+ /* Schedule check for pending messages in case messages were added
+ * to the outbound message buffer prior to the connection being
+ * fully established */
+ lws_callback_on_writable(wsi);
+ break;
+
+ /* Data received via WebSocket */
+ case LWS_CALLBACK_CLIENT_RECEIVE:
+ guac_kubernetes_receive_data(client, (const char*) in, length);
+ break;
+
+ /* WebSocket is ready for writing */
+ case LWS_CALLBACK_CLIENT_WRITEABLE:
+
+ /* Send any pending messages, requesting another callback if
+ * yet more messages remain */
+ if (guac_kubernetes_write_pending_message(client))
+ lws_callback_on_writable(wsi);
+ break;
+
+#ifdef HAVE_LWS_CALLBACK_CLIENT_CLOSED
+ /* Connection closed (client-specific) */
+ case LWS_CALLBACK_CLIENT_CLOSED:
+#endif
+
+ /* Connection closed */
+ case LWS_CALLBACK_CLOSED:
+ guac_client_stop(client);
+ guac_client_log(client, GUAC_LOG_DEBUG, "WebSocket connection to "
+ "Kubernetes server closed.");
+ break;
+
+ /* No other event types are applicable */
+ default:
+ break;
+
+ }
+
+ 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
+ * data to the Kubernetes connection.
+ *
+ * @param data
+ * The current guac_client instance.
+ *
+ * @return
+ * Always NULL.
+ */
+static void* guac_kubernetes_input_thread(void* data) {
+
+ guac_client* client = (guac_client*) data;
+ guac_kubernetes_client* kubernetes_client =
+ (guac_kubernetes_client*) client->data;
+
+ char buffer[GUAC_KUBERNETES_MAX_MESSAGE_SIZE];
+ int bytes_read;
+
+ /* Write all data read */
+ while ((bytes_read = guac_terminal_read_stdin(kubernetes_client->term, buffer, sizeof(buffer))) > 0) {
+
+ /* Send received data to Kubernetes along STDIN channel */
+ guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_STDIN,
+ buffer, bytes_read);
+
+ }
+
+ return NULL;
+
+}
+
+void* guac_kubernetes_client_thread(void* data) {
+
+ guac_client* client = (guac_client*) data;
+ guac_kubernetes_client* kubernetes_client =
+ (guac_kubernetes_client*) client->data;
+
+ guac_kubernetes_settings* settings = kubernetes_client->settings;
+
+ pthread_t input_thread;
+ char endpoint_path[GUAC_KUBERNETES_MAX_ENDPOINT_LENGTH];
+
+ /* Verify that the pod name was specified (it's always required) */
+ if (settings->kubernetes_pod == NULL) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "The name of the Kubernetes pod is a required parameter.");
+ goto fail;
+ }
+
+ /* Generate endpoint for attachment URL */
+ if (guac_kubernetes_endpoint_attach(endpoint_path, sizeof(endpoint_path),
+ settings->kubernetes_namespace,
+ settings->kubernetes_pod,
+ settings->kubernetes_container)) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Unable to generate path for Kubernetes API endpoint: "
+ "Resulting path too long");
+ goto fail;
+ }
+
+ guac_client_log(client, GUAC_LOG_DEBUG, "The endpoint for attaching to "
+ "the requested Kubernetes pod is \"%s\".", endpoint_path);
+
+ /* Set up screen recording, if requested */
+ if (settings->recording_path != NULL) {
+ kubernetes_client->recording = guac_common_recording_create(client,
+ settings->recording_path,
+ settings->recording_name,
+ settings->create_recording_path,
+ !settings->recording_exclude_output,
+ !settings->recording_exclude_mouse,
+ settings->recording_include_keys);
+ }
+
+ /* Create terminal */
+ kubernetes_client->term = guac_terminal_create(client,
+ kubernetes_client->clipboard,
+ settings->max_scrollback, settings->font_name, settings->font_size,
+ settings->resolution, settings->width, settings->height,
+ settings->color_scheme, settings->backspace);
+
+ /* Fail if terminal init failed */
+ if (kubernetes_client->term == NULL) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Terminal initialization failed");
+ goto fail;
+ }
+
+ /* Set up typescript, if requested */
+ if (settings->typescript_path != NULL) {
+ guac_terminal_create_typescript(kubernetes_client->term,
+ settings->typescript_path,
+ settings->typescript_name,
+ settings->create_typescript_path);
+ }
+
+ /* Init libwebsockets context creation parameters */
+ struct lws_context_creation_info context_info = {
+ .port = CONTEXT_PORT_NO_LISTEN, /* We are not a WebSocket server */
+ .uid = -1,
+ .gid = -1,
+ .protocols = guac_kubernetes_lws_protocols,
+ .user = client
+ };
+
+ /* Init WebSocket connection parameters which do not vary by Guacmaole
+ * connection parameters or creation of future libwebsockets objects */
+ struct lws_client_connect_info connection_info = {
+ .host = settings->hostname,
+ .address = settings->hostname,
+ .origin = settings->hostname,
+ .port = settings->port,
+ .protocol = GUAC_KUBERNETES_LWS_PROTOCOL,
+ .pwsi = &kubernetes_client->wsi,
+ .userdata = client
+ };
+
+ /* If requested, use an SSL/TLS connection for communication with
+ * Kubernetes. Note that we disable hostname checks here because we
+ * do our own validation - libwebsockets does not validate properly if
+ * IP addresses are used. */
+ if (settings->use_ssl) {
+ context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
+ connection_info.ssl_connection = LCCSCF_USE_SSL
+ | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
+ }
+
+ /* Create libwebsockets context */
+ kubernetes_client->context = lws_create_context(&context_info);
+ if (!kubernetes_client->context) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Initialization of libwebsockets failed");
+ goto fail;
+ }
+
+ /* Generate path dynamically */
+ connection_info.context = kubernetes_client->context;
+ connection_info.path = endpoint_path;
+
+ /* Open WebSocket connection to Kubernetes */
+ kubernetes_client->wsi = lws_client_connect_via_info(&connection_info);
+ if (kubernetes_client->wsi == NULL) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
+ "Connection via libwebsockets failed");
+ goto fail;
+ }
+
+ /* Init outbound message buffer */
+ pthread_mutex_init(&(kubernetes_client->outbound_message_lock), NULL);
+
+ /* Start input thread */
+ if (pthread_create(&(input_thread), NULL, guac_kubernetes_input_thread, (void*) client)) {
+ guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Unable to start input thread");
+ goto fail;
+ }
+
+ /* Force a redraw of the attached display (there will be no content
+ * otherwise, given the stream nature of attaching to a running
+ * container) */
+ guac_kubernetes_force_redraw(client);
+
+ /* As long as client is connected, continue polling libwebsockets */
+ while (client->state == GUAC_CLIENT_RUNNING) {
+
+ /* Cease polling libwebsockets if an error condition is signalled */
+ if (lws_service(kubernetes_client->context,
+ GUAC_KUBERNETES_SERVICE_INTERVAL) < 0)
+ break;
+
+ }
+
+ /* Kill client and Wait for input thread to die */
+ guac_terminal_stop(kubernetes_client->term);
+ guac_client_stop(client);
+ pthread_join(input_thread, NULL);
+
+fail:
+
+ /* Kill and free terminal, if allocated */
+ if (kubernetes_client->term != NULL)
+ guac_terminal_free(kubernetes_client->term);
+
+ /* Clean up recording, if in progress */
+ if (kubernetes_client->recording != NULL)
+ guac_common_recording_free(kubernetes_client->recording);
+
+ /* Free WebSocket context if successfully allocated */
+ if (kubernetes_client->context != NULL)
+ lws_context_destroy(kubernetes_client->context);
+
+ guac_client_log(client, GUAC_LOG_INFO, "Kubernetes connection ended.");
+ return NULL;
+
+}
+
+void guac_kubernetes_resize(guac_client* client, int rows, int columns) {
+
+ char buffer[64];
+
+ guac_kubernetes_client* kubernetes_client =
+ (guac_kubernetes_client*) client->data;
+
+ /* Send request only if different from last request */
+ if (kubernetes_client->rows != rows ||
+ kubernetes_client->columns != columns) {
+
+ kubernetes_client->rows = rows;
+ kubernetes_client->columns = columns;
+
+ /* Construct terminal resize message for Kubernetes */
+ int length = snprintf(buffer, sizeof(buffer),
+ "{\"Width\":%i,\"Height\":%i}", columns, rows);
+
+ /* Schedule message for sending */
+ guac_kubernetes_send_message(client, GUAC_KUBERNETES_CHANNEL_RESIZE,
+ buffer, length);
+
+ }
+
+}
+
+void guac_kubernetes_force_redraw(guac_client* client) {
+
+ guac_kubernetes_client* kubernetes_client =
+ (guac_kubernetes_client*) client->data;
+
+ /* Get current terminal dimensions */
+ guac_terminal* term = kubernetes_client->term;
+ int rows = term->term_height;
+ int columns = term->term_width;
+
+ /* Force a redraw by increasing the terminal size by one character in
+ * each dimension and then resizing it back to normal (the same technique
+ * used by kubectl */
+ guac_kubernetes_resize(client, rows + 1, columns + 1);
+ guac_kubernetes_resize(client, rows, columns);
+
+}
+
diff --git a/src/protocols/kubernetes/kubernetes.h b/src/protocols/kubernetes/kubernetes.h
new file mode 100644
index 0000000..c37ca4c
--- /dev/null
+++ b/src/protocols/kubernetes/kubernetes.h
@@ -0,0 +1,168 @@
+/*
+ * 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 "common/clipboard.h"
+#include "common/recording.h"
+#include "io.h"
+#include "settings.h"
+#include "terminal/terminal.h"
+
+#include <guacamole/client.h>
+#include <libwebsockets.h>
+
+#include <pthread.h>
+
+/**
+ * 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 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
+
+/**
+ * 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
+
+/**
+ * Kubernetes-specific client data.
+ */
+typedef struct guac_kubernetes_client {
+
+ /**
+ * Kubernetes connection settings.
+ */
+ guac_kubernetes_settings* settings;
+
+ /**
+ * The libwebsockets context associated with the connected WebSocket.
+ */
+ struct lws_context* context;
+
+ /**
+ * The connected WebSocket.
+ */
+ 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.
+ */
+ 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 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.
+ */
+ 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);
+
+/**
+ * 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
+
diff --git a/src/protocols/kubernetes/pipe.c b/src/protocols/kubernetes/pipe.c
new file mode 100644
index 0000000..8f18530
--- /dev/null
+++ b/src/protocols/kubernetes/pipe.c
@@ -0,0 +1,52 @@
+/*
+ * 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 "pipe.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/stream-types.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+
+#include <string.h>
+
+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 0000000..47565bf
--- /dev/null
+++ b/src/protocols/kubernetes/pipe.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <guacamole/user.h>
+
+/**
+ * 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 0000000..4f00a44
--- /dev/null
+++ b/src/protocols/kubernetes/settings.c
@@ -0,0 +1,403 @@
+/*
+ * 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 "settings.h"
+
+#include <guacamole/user.h>
+
+#include <stdlib.h>
+
+/* Client plugin arguments */
+const char* GUAC_KUBERNETES_CLIENT_ARGS[] = {
+ "hostname",
+ "port",
+ "namespace",
+ "pod",
+ "container",
+ "use-ssl",
+ "client-cert",
+ "client-key",
+ "ca-cert",
+ "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,
+
+ /**
+ * 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.
+ */
+ IDX_USE_SSL,
+
+ /**
+ * 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,
+
+ /**
+ * 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,
+
+ /**
+ * 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,
+
+ /**
+ * 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: <color>", "foreground: <color>", or
+ * "color<n>: <color>", where <n> is a number from 0 to 255, and <color> is
+ * "color<n>" 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 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,
+ IDX_USE_SSL, false);
+
+ /* Read SSL/TLS connection details only if enabled */
+ if (settings->use_ssl) {
+
+ settings->client_cert =
+ guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS,
+ argv, IDX_CLIENT_CERT, NULL);
+
+ settings->client_key =
+ guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS,
+ argv, IDX_CLIENT_KEY, NULL);
+
+ settings->ca_cert =
+ guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS,
+ argv, IDX_CA_CERT, 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 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 Kubernetes pod/container details */
+ free(settings->kubernetes_namespace);
+ free(settings->kubernetes_pod);
+ free(settings->kubernetes_container);
+
+ /* Free SSL/TLS details */
+ free(settings->client_cert);
+ free(settings->client_key);
+ free(settings->ca_cert);
+
+ /* 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 0000000..6267a18
--- /dev/null
+++ b/src/protocols/kubernetes/settings.h
@@ -0,0 +1,279 @@
+/*
+ * 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 <guacamole/user.h>
+
+#include <stdbool.h>
+
+/**
+ * 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 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.
+ */
+#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.
+ */
+ 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.
+ */
+ bool use_ssl;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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/ssl.c b/src/protocols/kubernetes/ssl.c
new file mode 100644
index 0000000..6ebafc6
--- /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 <guacamole/client.h>
+#include <openssl/asn1.h>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+#include <openssl/ssl.h>
+#include <openssl/x509v3.h>
+#include <openssl/x509_vfy.h>
+
+/**
+ * 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 0000000..cca02bd
--- /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 <openssl/ssl.h>
+
+/**
+ * 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
+
diff --git a/src/protocols/kubernetes/url.c b/src/protocols/kubernetes/url.c
new file mode 100644
index 0000000..78c116e
--- /dev/null
+++ b/src/protocols/kubernetes/url.c
@@ -0,0 +1,137 @@
+/*
+ * 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 "url.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * 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')
+ || (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 0000000..285baa2
--- /dev/null
+++ b/src/protocols/kubernetes/url.h
@@ -0,0 +1,85 @@
+/*
+ * 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
+
+/**
+ * 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
+
diff --git a/src/protocols/kubernetes/user.c b/src/protocols/kubernetes/user.c
new file mode 100644
index 0000000..f90260e
--- /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 "clipboard.h"
+#include "common/cursor.h"
+#include "input.h"
+#include "kubernetes.h"
+#include "pipe.h"
+#include "settings.h"
+#include "terminal/terminal.h"
+#include "user.h"
+
+#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/user.h>
+
+#include <pthread.h>
+#include <stdlib.h>
+
+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 0000000..55d49fd
--- /dev/null
+++ b/src/protocols/kubernetes/user.h
@@ -0,0 +1,36 @@
+/*
+ * 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 <guacamole/user.h>
+
+/**
+ * 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
+