From ed560938886e92dbe656d610966585b8178c2d73 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 10 Sep 2018 18:39:06 -0700 Subject: [PATCH] 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 +