diff --git a/src/libguac/client.c b/src/libguac/client.c index 76dc67a83f..21ac10523a 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -139,6 +139,7 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { allocd_stream->ack_handler = NULL; allocd_stream->blob_handler = NULL; allocd_stream->end_handler = NULL; + allocd_stream->free_handler = NULL; return allocd_stream; @@ -146,6 +147,17 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { void guac_client_free_stream(guac_client* client, guac_stream* stream) { + /* Run the registered free handler for the stream's data, if any */ + if (stream->free_handler != NULL) + stream->free_handler(stream->data); + + /* Clean up any remaining dangling pointers before permitting reuse */ + stream->data = NULL; + stream->ack_handler = NULL; + stream->blob_handler = NULL; + stream->end_handler = NULL; + stream->free_handler = NULL; + /* Mark stream as closed */ int freed_index = stream->index; stream->index = GUAC_CLIENT_CLOSED_STREAM_INDEX; @@ -289,6 +301,7 @@ guac_client* guac_client_alloc(void) { for (i=0; i__output_streams[i].index = GUAC_CLIENT_CLOSED_STREAM_INDEX; + client->__output_streams[i].free_handler = NULL; } /* Init locks */ @@ -344,6 +357,15 @@ void guac_client_free(guac_client* client) { guac_pool_free(client->__buffer_pool); guac_pool_free(client->__layer_pool); + /* Run any registered free handlers for streams still open at + * disconnect so their data pointers do not leak */ + for (int i = 0; i < GUAC_CLIENT_MAX_STREAMS; i++) { + guac_stream* stream = &client->__output_streams[i]; + if (stream->index != GUAC_CLIENT_CLOSED_STREAM_INDEX + && stream->free_handler != NULL) + stream->free_handler(stream->data); + } + /* Free streams */ guac_mem_free(client->__output_streams); diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h index 1a690d1c04..34b6a4a7ea 100644 --- a/src/libguac/guacamole/protocol.h +++ b/src/libguac/guacamole/protocol.h @@ -1119,6 +1119,38 @@ int guac_protocol_send_clipboard(guac_socket* socket, const guac_stream* stream, */ int guac_protocol_send_name(guac_socket* socket, const char* name); +/** + * Sends an auth-challenge instruction over the given guac_socket + * connection, announcing a stream carrying the challenge body for a + * pending authentication exchange. The peer responds via an + * auth-response instruction carrying the same challenge_id. + * + * The body is shipped on the given stream as blobs, terminated by an + * end, after this function returns. Callers are responsible for + * allocating and freeing the stream. + * + * @param socket + * The guac_socket connection to use. + * + * @param stream + * The stream along which the challenge body will be sent. + * + * @param mimetype + * The mimetype of the data that will be sent along the given stream. + * The mimetype identifies the auth flavor (e.g. WebAuthn create vs. + * get). + * + * @param challenge_id + * Opaque identifier carried by the auth-challenge. The peer's + * matching auth-response will reference this identifier. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_protocol_send_auth_challenge(guac_socket* socket, + const guac_stream* stream, const char* mimetype, + const char* challenge_id); + /** * Decodes the given base64-encoded string in-place. The base64 string must * be NULL-terminated. diff --git a/src/libguac/guacamole/stream.h b/src/libguac/guacamole/stream.h index c4c1b60b89..4d18d37598 100644 --- a/src/libguac/guacamole/stream.h +++ b/src/libguac/guacamole/stream.h @@ -105,6 +105,34 @@ struct guac_stream { */ guac_user_end_handler* end_handler; + /** + * Optional cleanup callback for whatever the stream's data pointer + * holds. Called from guac_user_free_stream when the stream is freed, + * and from guac_user_free for any streams still open when the user + * disconnects. NULL if no cleanup is needed. + * + * Example: + * @code + * static void my_data_free(void* data) { + * my_data* d = (my_data*) data; + * free(d->buffer); + * free(d); + * } + * + * int my_pipe_handler(guac_user* user, guac_stream* stream, + * char* mimetype, char* name) { + * my_data* d = malloc(sizeof(*d)); + * d->buffer = NULL; + * stream->data = d; + * stream->blob_handler = my_blob_handler; + * stream->end_handler = my_end_handler; + * stream->free_handler = my_data_free; + * return 0; + * } + * @endcode + */ + guac_stream_free_handler* free_handler; + }; #endif diff --git a/src/libguac/guacamole/user-fntypes.h b/src/libguac/guacamole/user-fntypes.h index fa3cac6f8e..d2f6780ff1 100644 --- a/src/libguac/guacamole/user-fntypes.h +++ b/src/libguac/guacamole/user-fntypes.h @@ -380,6 +380,18 @@ typedef int guac_user_ack_handler(guac_user* user, guac_stream* stream, */ typedef int guac_user_end_handler(guac_user* user, guac_stream* stream); +/** + * Optional cleanup callback installed on a guac_stream for whatever the + * stream's data pointer holds. Called from guac_user_free_stream and + * guac_client_free_stream when the stream is freed, and from + * guac_user_free and guac_client_free for any streams still open when + * the user or client is disposed. + * + * @param data + * The stream's data pointer at the time the stream is being freed. + */ +typedef void guac_stream_free_handler(void* data); + /** * Handler for Guacamole join events. A join event is fired by the * guac_client whenever a guac_user joins the connection. There is no @@ -500,7 +512,7 @@ typedef int guac_user_put_handler(guac_user* user, guac_object* object, guac_stream* stream, char* mimetype, char* name); /** - * Handler for Guacamole USB connect events, invoked when a "usbconnect" + * Handler for Guacamole USB connect events, invoked when a "usbconnect" * instruction has been received from a user. This indicates that the user * has connected a USB device via WebUSB and it is available for redirection. * @@ -540,10 +552,36 @@ typedef int guac_user_put_handler(guac_user* user, guac_object* object, * an error occurred. */ typedef int guac_user_usbconnect_handler(guac_user* user, const char* device_id, - int vendor_id, int product_id, const char* device_name, + int vendor_id, int product_id, const char* device_name, const char* serial_number, int device_class, int device_subclass, int device_protocol, const char* interface_data); +/** + * Handler for Guacamole "auth-response" events, invoked when an + * "auth-response" instruction has been received from a user. An + * auth-response announces a stream carrying the response body for a + * previously-issued auth-challenge identified by the same challenge_id. + * + * @param user + * The user that sent the auth-response. + * + * @param stream + * The stream along which the response body will be received. + * + * @param mimetype + * The mimetype of the data that will be received along the given + * stream. + * + * @param challenge_id + * The challenge identifier of the originating auth-challenge being + * responded to. + * + * @return + * Zero on success, non-zero on error. + */ +typedef int guac_user_auth_response_handler(guac_user* user, + guac_stream* stream, char* mimetype, char* challenge_id); + /** * Handler for Guacamole USB data events, invoked when a "usbdata" instruction * has been received from a user. This carries data from a client-side USB diff --git a/src/libguac/guacamole/user.h b/src/libguac/guacamole/user.h index 501d9e7118..8cdae452b7 100644 --- a/src/libguac/guacamole/user.h +++ b/src/libguac/guacamole/user.h @@ -598,6 +598,24 @@ struct guac_user { */ guac_user_usbdisconnect_handler* usbdisconnect_handler; + /** + * Handler for "auth-response" events sent by the Guacamole web-client, + * carrying the response body for a previously-issued auth-challenge + * identified by the same challenge_id. The handler installs the blob + * and end handlers on the stream so the body can be consumed. + * + * Example: + * @code + * int auth_response_handler(guac_user* user, guac_stream* stream, + * char* mimetype, char* challenge_id); + * + * int guac_user_init(guac_user* user, int argc, char** argv) { + * user->auth_response_handler = auth_response_handler; + * } + * @endcode + */ + guac_user_auth_response_handler* auth_response_handler; + }; /** diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c index b634a8abc6..a571b3c864 100644 --- a/src/libguac/protocol.c +++ b/src/libguac/protocol.c @@ -1301,7 +1301,7 @@ int guac_protocol_send_video(guac_socket* socket, const guac_stream* stream, int ret_val; guac_socket_instruction_begin(socket); - ret_val = + ret_val = guac_socket_write_string(socket, "5.video,") || __guac_socket_write_length_int(socket, stream->index) || guac_socket_write_string(socket, ",") @@ -1315,6 +1315,27 @@ int guac_protocol_send_video(guac_socket* socket, const guac_stream* stream, } +int guac_protocol_send_auth_challenge(guac_socket* socket, + const guac_stream* stream, const char* mimetype, + const char* challenge_id) { + + int ret_val; + + guac_socket_instruction_begin(socket); + ret_val = + guac_socket_write_string(socket, "14.auth-challenge,") + || __guac_socket_write_length_int(socket, stream->index) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_string(socket, mimetype) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_string(socket, challenge_id) + || guac_socket_write_string(socket, ";"); + guac_socket_instruction_end(socket); + + return ret_val; + +} + /** * Returns the value of a single base64 character. */ diff --git a/src/libguac/tests/Makefile.am b/src/libguac/tests/Makefile.am index 1d5c244ef8..8fb4884a4c 100644 --- a/src/libguac/tests/Makefile.am +++ b/src/libguac/tests/Makefile.am @@ -39,6 +39,7 @@ noinst_HEADERS = \ test_libguac_SOURCES = \ client/buffer_pool.c \ client/layer_pool.c \ + client/stream_free_handler.c \ fifo/fifo.c \ file/openat.c \ flag/flag.c \ @@ -59,6 +60,7 @@ test_libguac_SOURCES = \ pool/next_free.c \ protocol/base64_decode.c \ protocol/guac_protocol_version.c \ + protocol/auth_send.c \ rect/align.c \ rect/constrain.c \ rect/extend.c \ @@ -74,7 +76,8 @@ test_libguac_SOURCES = \ unicode/charsize.c \ unicode/read.c \ unicode/strlen.c \ - unicode/write.c + unicode/write.c \ + user/auth_dispatch.c test_libguac_CFLAGS = \ -Werror -Wall -pedantic \ diff --git a/src/libguac/tests/client/stream_free_handler.c b/src/libguac/tests/client/stream_free_handler.c new file mode 100644 index 0000000000..fe8afb7885 --- /dev/null +++ b/src/libguac/tests/client/stream_free_handler.c @@ -0,0 +1,201 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include + +/** + * Count of free_handler invocations across the active test. The + * free_handler increments this on every call. + */ +static int free_handler_calls = 0; + +/** + * The data pointer most recently passed to the free_handler. + */ +static void* last_free_handler_data = NULL; + +/** + * free_handler for the stream->data pointer. Records the invocation in + * free_handler_calls and last_free_handler_data so the test can assert it. + * + * @param data + * The stream's data pointer at the time the stream is freed. + */ +static void counting_free_handler(void* data) { + free_handler_calls++; + last_free_handler_data = data; +} + +/** + * Verifies that a stream's free_handler fires when the stream is freed + * via guac_user_free_stream while the user is still alive. + */ +void test_client__stream_free_handler_on_free_stream(void) { + + free_handler_calls = 0; + last_free_handler_data = NULL; + + guac_client* client = guac_client_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(client); + + guac_user* user = guac_user_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(user); + user->client = client; + + guac_stream* stream = guac_user_alloc_stream(user); + CU_ASSERT_PTR_NOT_NULL_FATAL(stream); + + int dummy = 42; + stream->data = &dummy; + stream->free_handler = counting_free_handler; + + guac_user_free_stream(user, stream); + + CU_ASSERT_EQUAL(free_handler_calls, 1); + CU_ASSERT_PTR_EQUAL(last_free_handler_data, &dummy); + + guac_user_free(user); + guac_client_free(client); + +} + +/** + * Verifies that a free_handler on a stream still open at disconnect + * fires during guac_user_free, so a stream whose end never arrived does + * not leak its data pointer. + */ +void test_client__stream_free_handler_on_user_free(void) { + + free_handler_calls = 0; + last_free_handler_data = NULL; + + guac_client* client = guac_client_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(client); + + guac_user* user = guac_user_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(user); + user->client = client; + + guac_stream* stream = guac_user_alloc_stream(user); + CU_ASSERT_PTR_NOT_NULL_FATAL(stream); + + int dummy = 7; + stream->data = &dummy; + stream->free_handler = counting_free_handler; + + /* No guac_user_free_stream. Simulate disconnect with the stream + * still open. */ + guac_user_free(user); + + CU_ASSERT_EQUAL(free_handler_calls, 1); + CU_ASSERT_PTR_EQUAL(last_free_handler_data, &dummy); + + guac_client_free(client); + +} + +/** + * Verifies that a stream allocated with no free_handler set (the + * default) does not invoke any callback when freed. Confirms that the + * free_handler field is opt-in and existing handlers are unaffected. + */ +void test_client__stream_free_handler_unset_is_noop(void) { + + free_handler_calls = 0; + + guac_client* client = guac_client_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(client); + + guac_user* user = guac_user_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(user); + user->client = client; + + guac_stream* stream = guac_user_alloc_stream(user); + CU_ASSERT_PTR_NOT_NULL_FATAL(stream); + + int dummy = 99; + stream->data = &dummy; + /* No free_handler installed */ + + guac_user_free_stream(user, stream); + guac_user_free(user); + guac_client_free(client); + + CU_ASSERT_EQUAL(free_handler_calls, 0); + +} + +/** + * Regression test: verifies that opening an input stream via the regular + * pipe handler path leaves the free_handler at NULL even when a stale + * pointer was present on the slot before init. If __init_input_stream + * forgot to reset free_handler, guac_user_free would walk the open slot + * and call the stale function pointer. + */ +void test_client__stream_free_handler_reset_on_input_init(void) { + + free_handler_calls = 0; + + guac_client* client = guac_client_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(client); + + guac_user* user = guac_user_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(user); + user->client = client; + + /* Send-side socket so the unsupported-pipe ack does not crash. */ + int devnull = open("/dev/null", O_WRONLY); + CU_ASSERT_TRUE_FATAL(devnull >= 0); + user->socket = guac_socket_open(devnull); + CU_ASSERT_PTR_NOT_NULL_FATAL(user->socket); + + /* Simulate a stale free_handler pointer left over from prior use of + * the slot's storage. __init_input_stream must reset this when the + * slot is reused. */ + user->__input_streams[0].free_handler = counting_free_handler; + + /* Send a pipe instruction. With no pipe_handler installed, the + * dispatcher will ack unsupported and return. The slot is now + * "open" (index != CLOSED) but should have its free_handler + * cleared. */ + char idx[] = "0"; + char mt[] = "text/plain"; + char name[] = "some-pipe"; + char* pipe_argv[] = { idx, mt, name }; + CU_ASSERT_EQUAL(guac_user_handle_instruction(user, "pipe", 3, pipe_argv), 0); + + /* Disconnect with the stream still open. guac_user_free should + * walk the slot and find free_handler == NULL, calling nothing. */ + guac_socket_free(user->socket); + user->socket = NULL; + guac_user_free(user); + guac_client_free(client); + + CU_ASSERT_EQUAL(free_handler_calls, 0); + +} diff --git a/src/libguac/tests/protocol/auth_send.c b/src/libguac/tests/protocol/auth_send.c new file mode 100644 index 0000000000..ce53b2eea6 --- /dev/null +++ b/src/libguac/tests/protocol/auth_send.c @@ -0,0 +1,159 @@ +/* + * 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 +#include +#include +#include + +#include +#include +#include +#include + +/** + * Reads everything available from the given file descriptor into a + * heap-allocated buffer. The buffer grows geometrically as needed. + * Closes the file descriptor before returning. + * + * @param fd + * The file descriptor to read from. + * + * @param out_len + * Out parameter that receives the number of bytes read. + * + * @return + * A heap-allocated buffer containing the bytes read. The caller + * takes ownership and must free the buffer. + */ +static char* read_all(int fd, int* out_len) { + + int capacity = 4096; + char* buffer = malloc(capacity); + int offset = 0; + int numread; + + while ((numread = read(fd, buffer + offset, capacity - offset)) > 0) { + offset += numread; + if (offset == capacity) { + capacity *= 2; + buffer = realloc(buffer, capacity); + } + } + + close(fd); + *out_len = offset; + return buffer; + +} + +/** + * Runs the given sender inside a forked child process, writing to + * write_fd, then reads everything from read_fd in the parent and asserts + * equality against the expected bytes. + * + * @param sender + * The function the child process invokes to write its bytes to + * write_fd. + * + * @param write_fd + * The file descriptor the sender should write to. Closed in the + * parent before reading. + * + * @param read_fd + * The file descriptor the parent reads from. Closed in the child + * before sending. + * + * @param expected + * The byte sequence expected to be received on read_fd. + * + * @param expected_len + * The length of the expected byte sequence. + */ +static void run_sender_check(void (*sender)(int), int write_fd, int read_fd, + const char* expected, int expected_len) { + + int childpid; + CU_ASSERT_NOT_EQUAL_FATAL((childpid = fork()), -1); + + if (childpid == 0) { + close(read_fd); + sender(write_fd); + _exit(0); + } + + close(write_fd); + + int len; + char* buffer = read_all(read_fd, &len); + + int status; + waitpid(childpid, &status, 0); + + CU_ASSERT_EQUAL(len, expected_len); + if (len == expected_len) + CU_ASSERT_EQUAL(memcmp(buffer, expected, expected_len), 0); + + free(buffer); + +} + +/** + * Sender for the auth-challenge test. Emits a single + * guac_protocol_send_auth_challenge with a synthetic UUID-shaped + * challenge id and a WebAuthn create mimetype, then flushes the socket + * so the parent process can read the bytes. + * + * @param fd + * The file descriptor to write the wire bytes to. + */ +static void write_auth_challenge(int fd) { + + guac_socket* socket = guac_socket_open(fd); + if (socket == NULL) + return; + + guac_stream stream = { .index = 7 }; + guac_protocol_send_auth_challenge(socket, &stream, + "application/x-webauthn-create+json", + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + guac_socket_flush(socket); + + guac_socket_free(socket); + +} + +/** + * Verifies that guac_protocol_send_auth_challenge emits the expected wire + * form announcing a stream carrying the challenge body. + */ +void test_protocol__send_auth_challenge(void) { + + int fd[2]; + CU_ASSERT_EQUAL_FATAL(pipe(fd), 0); + + const char expected[] = + "14.auth-challenge,1.7,34.application/x-webauthn-create+json," + "36.aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa;"; + + run_sender_check(write_auth_challenge, fd[1], fd[0], + expected, sizeof(expected) - 1); + +} + diff --git a/src/libguac/tests/user/auth_dispatch.c b/src/libguac/tests/user/auth_dispatch.c new file mode 100644 index 0000000000..a7107dfa23 --- /dev/null +++ b/src/libguac/tests/user/auth_dispatch.c @@ -0,0 +1,186 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#include +#include +#include + +/** + * State captured by the test auth_response_handler so the test can + * verify what it was invoked with. + */ +typedef struct auth_capture { + + /** Number of times the capturing handler has been invoked. */ + int call_count; + + /** Stream index of the most recent invocation's stream. */ + int stream_index; + + /** Mimetype argument of the most recent invocation. */ + char mimetype[64]; + + /** Challenge id argument of the most recent invocation. */ + char challenge_id[64]; + +} auth_capture; + +/** + * auth_response_handler that copies its arguments into the user's data + * pointer (assumed to be an auth_capture) so the test can assert them. + * + * @param user + * The user whose handler is being invoked. user->data is expected + * to point at an auth_capture. + * + * @param stream + * The stream announced by the inbound auth-response. + * + * @param mimetype + * The mimetype announced by the inbound auth-response. + * + * @param challenge_id + * The challenge id announced by the inbound auth-response. + * + * @return + * Zero, always; the capture handler never fails. + */ +static int capture_auth(guac_user* user, guac_stream* stream, + char* mimetype, char* challenge_id) { + auth_capture* cap = (auth_capture*) user->data; + cap->call_count++; + cap->stream_index = stream->index; + snprintf(cap->mimetype, sizeof(cap->mimetype), "%s", mimetype); + snprintf(cap->challenge_id, sizeof(cap->challenge_id), "%s", + challenge_id); + return 0; +} + +/** + * Allocates a guac_user attached to the given client, installs + * capture_auth as the auth_response_handler, and points user->data at + * the given capture struct so the handler can record its arguments. + * + * @param client + * The client the new user should be attached to. + * + * @param cap + * The capture struct that capture_auth will write into. + * + * @return + * The allocated user. Caller must free via teardown_auth_user. + */ +static guac_user* setup_auth_user(guac_client* client, auth_capture* cap) { + + guac_user* user = guac_user_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(user); + user->client = client; + user->data = cap; + user->auth_response_handler = capture_auth; + + /* Wire the user's outbound socket to /dev/null so any ack writes do + * not crash. */ + int devnull = open("/dev/null", O_WRONLY); + CU_ASSERT_TRUE_FATAL(devnull >= 0); + user->socket = guac_socket_open(devnull); + CU_ASSERT_PTR_NOT_NULL_FATAL(user->socket); + + return user; + +} + +/** + * Frees a user previously allocated by setup_auth_user, closing its + * outbound socket first. + * + * @param user + * The user to free. + */ +static void teardown_auth_user(guac_user* user) { + guac_socket_free(user->socket); + user->socket = NULL; + guac_user_free(user); +} + +/** + * Verifies that an auth-response instruction dispatches to the user's + * auth_response_handler with the correct arguments. + */ +void test_user__auth_response_dispatch(void) { + + guac_client* client = guac_client_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(client); + + auth_capture cap = { 0 }; + guac_user* user = setup_auth_user(client, &cap); + + char stream_idx[] = "11"; + char mimetype[] = "application/x-webauthn-get+json"; + char challenge_id[] = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"; + + char* argv[] = { stream_idx, mimetype, challenge_id }; + CU_ASSERT_EQUAL( + guac_user_handle_instruction(user, "auth-response", 3, argv), 0); + + CU_ASSERT_EQUAL(cap.call_count, 1); + CU_ASSERT_EQUAL(cap.stream_index, 11); + CU_ASSERT_STRING_EQUAL(cap.mimetype, + "application/x-webauthn-get+json"); + CU_ASSERT_STRING_EQUAL(cap.challenge_id, + "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); + + teardown_auth_user(user); + guac_client_free(client); + +} + +/** + * Verifies that an auth-response instruction with no registered handler + * does not crash and does not invoke the response handler. + */ +void test_user__auth_response_no_handler(void) { + + guac_client* client = guac_client_alloc(); + CU_ASSERT_PTR_NOT_NULL_FATAL(client); + + auth_capture cap = { 0 }; + guac_user* user = setup_auth_user(client, &cap); + user->auth_response_handler = NULL; + + char stream_idx[] = "3"; + char mimetype[] = "application/x-webauthn-create+json"; + char challenge_id[] = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"; + + char* argv[] = { stream_idx, mimetype, challenge_id }; + CU_ASSERT_EQUAL( + guac_user_handle_instruction(user, "auth-response", 3, argv), 0); + + CU_ASSERT_EQUAL(cap.call_count, 0); + + teardown_auth_user(user); + guac_client_free(client); + +} diff --git a/src/libguac/user-handlers.c b/src/libguac/user-handlers.c index ff3e73fe4e..443e55d5a1 100644 --- a/src/libguac/user-handlers.c +++ b/src/libguac/user-handlers.c @@ -58,6 +58,7 @@ __guac_instruction_handler_mapping __guac_instruction_handler_map[] = { {"usbconnect", __guac_handle_usbconnect}, {"usbdata", __guac_handle_usbdata}, {"usbdisconnect", __guac_handle_usbdisconnect}, + {"auth-response", __guac_handle_auth_response}, {NULL, NULL} }; @@ -319,6 +320,7 @@ static guac_stream* __init_input_stream(guac_user* user, int stream_index) { stream->ack_handler = NULL; stream->blob_handler = NULL; stream->end_handler = NULL; + stream->free_handler = NULL; return stream; @@ -857,3 +859,28 @@ int __guac_user_call_opcode_handler(__guac_instruction_handler_mapping* map, return 0; } + +int __guac_handle_auth_response(guac_user* user, int argc, char** argv) { + + /* Pull corresponding stream */ + int stream_index = atoi(argv[0]); + guac_stream* stream = __init_input_stream(user, stream_index); + if (stream == NULL) + return 0; + + /* If supported, call handler */ + if (user->auth_response_handler) + return user->auth_response_handler( + user, + stream, + argv[1], /* mimetype */ + argv[2] /* challenge_id */ + ); + + /* Otherwise, abort */ + guac_protocol_send_ack(user->socket, stream, + "Auth responses unsupported", GUAC_PROTOCOL_STATUS_UNSUPPORTED); + return 0; + +} + diff --git a/src/libguac/user-handlers.h b/src/libguac/user-handlers.h index 31d4b9ed46..7fb8d4ea03 100644 --- a/src/libguac/user-handlers.h +++ b/src/libguac/user-handlers.h @@ -188,6 +188,13 @@ __guac_instruction_handler __guac_handle_disconnect; */ __guac_instruction_handler __guac_handle_nop; +/** + * Internal handler for the auth-response instruction. Looks up the + * stream announced by the instruction and dispatches to the user's + * auth_response_handler so the body riding the stream can be consumed. + */ +__guac_instruction_handler __guac_handle_auth_response; + /** * Internal handler function that is called when the size instruction is * received during the handshake process. diff --git a/src/libguac/user.c b/src/libguac/user.c index 1fe09c8eb9..814b422ec7 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -69,7 +69,9 @@ guac_user* guac_user_alloc(void) { for (i=0; i__input_streams[i].index = GUAC_USER_CLOSED_STREAM_INDEX; + user->__input_streams[i].free_handler = NULL; user->__output_streams[i].index = GUAC_USER_CLOSED_STREAM_INDEX; + user->__output_streams[i].free_handler = NULL; } /* Allocate object pool */ @@ -86,6 +88,19 @@ guac_user* guac_user_alloc(void) { void guac_user_free(guac_user* user) { + /* Run any registered free handlers for streams still open at + * disconnect so their data pointers do not leak */ + for (int i = 0; i < GUAC_USER_MAX_STREAMS; i++) { + guac_stream* stream = &user->__input_streams[i]; + if (stream->index != GUAC_USER_CLOSED_STREAM_INDEX + && stream->free_handler != NULL) + stream->free_handler(stream->data); + stream = &user->__output_streams[i]; + if (stream->index != GUAC_USER_CLOSED_STREAM_INDEX + && stream->free_handler != NULL) + stream->free_handler(stream->data); + } + /* Free streams */ guac_mem_free(user->__input_streams); guac_mem_free(user->__output_streams); @@ -122,6 +137,7 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { allocd_stream->ack_handler = NULL; allocd_stream->blob_handler = NULL; allocd_stream->end_handler = NULL; + allocd_stream->free_handler = NULL; return allocd_stream; @@ -129,6 +145,17 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { void guac_user_free_stream(guac_user* user, guac_stream* stream) { + /* Run the registered free handler for the stream's data, if any */ + if (stream->free_handler != NULL) + stream->free_handler(stream->data); + + /* Clean up any remaining dangling pointers before permitting reuse */ + stream->data = NULL; + stream->ack_handler = NULL; + stream->blob_handler = NULL; + stream->end_handler = NULL; + stream->free_handler = NULL; + /* Mark stream as closed */ int freed_index = stream->index; stream->index = GUAC_USER_CLOSED_STREAM_INDEX;