diff --git a/Makefile.am b/Makefile.am index c2e303ce..c95a0dc0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,6 +38,7 @@ DIST_SUBDIRS = \ SUBDIRS = \ src/libguac \ src/common \ + src/guacd \ tests if ENABLE_COMMON_SSH diff --git a/src/guacd/Makefile.am b/src/guacd/Makefile.am index db1e7a54..8743fefe 100644 --- a/src/guacd/Makefile.am +++ b/src/guacd/Makefile.am @@ -29,21 +29,27 @@ man_MANS = \ man/guacd.conf.5 noinst_HEADERS = \ - client.h \ - client-map.h \ conf-args.h \ conf-file.h \ conf-parse.h \ - log.h + connection.h \ + log.h \ + move-fd.h \ + proc.h \ + proc-map.h \ + user.h -guacd_SOURCES = \ - daemon.c \ - client.c \ - client-map.c \ - conf-args.c \ - conf-file.c \ - conf-parse.c \ - log.c +guacd_SOURCES = \ + conf-args.c \ + conf-file.c \ + conf-parse.c \ + connection.c \ + daemon.c \ + log.c \ + move-fd.c \ + proc.c \ + proc-map.c \ + user.c guacd_CFLAGS = \ -Werror -Wall -pedantic \ diff --git a/src/guacd/client-map.h b/src/guacd/client-map.h deleted file mode 100644 index 8544bc4e..00000000 --- a/src/guacd/client-map.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2014 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - - -#ifndef _GUACD_CLIENT_MAP_H -#define _GUACD_CLIENT_MAP_H - -#include "config.h" -#include "client.h" -#include "guac_list.h" - -#include - -#define GUACD_CLIENT_MAP_BUCKETS GUACD_CLIENT_MAX_CONNECTIONS*2 - -/** - * Set of all active connections to guacd, indexed by connection ID. - */ -typedef struct guacd_client_map { - - /** - * Internal hash buckets. Each bucket is a linked list containing all - * guac_client instances which hash to this bucket location. - */ - guac_common_list* __buckets[GUACD_CLIENT_MAP_BUCKETS]; - -} guacd_client_map; - -/** - * Allocates a new client map, which persists for the life of guacd. - */ -guacd_client_map* guacd_client_map_alloc(); - -/** - * Adds the given client to the given client map. On success, zero is returned. - * If adding the client fails (due to lack of space, or duplicate ID), a - * non-zero value is returned instead. - */ -int guacd_client_map_add(guacd_client_map* map, guac_client* client); - -/** - * Retrieves the client having the given ID, or NULL if no such client - * is stored. - */ -guac_client* guacd_client_map_retrieve(guacd_client_map* map, const char* id); - -/** - * Removes the client having the given ID, returning the corresponding client. - * If no such client exists, NULL is returned. - */ -guac_client* guacd_client_map_remove(guacd_client_map* map, const char* id); - -#endif - diff --git a/src/guacd/client.c b/src/guacd/client.c deleted file mode 100644 index 075887cc..00000000 --- a/src/guacd/client.c +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#include "config.h" - -#include "client.h" -#include "log.h" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -/** - * Sleep for the given number of milliseconds. - * - * @param millis The number of milliseconds to sleep. - */ -void __guacdd_sleep(int millis) { - - struct timespec sleep_period; - - sleep_period.tv_sec = millis / 1000; - sleep_period.tv_nsec = (millis % 1000) * 1000000L; - - nanosleep(&sleep_period, NULL); - -} - -void* __guacd_client_output_thread(void* data) { - - guac_client* client = (guac_client*) data; - guac_socket* socket = client->socket; - - guac_client_log(client, GUAC_LOG_DEBUG, - "Starting output thread."); - - /* Guacamole client output loop */ - while (client->state == GUAC_CLIENT_RUNNING) { - - /* Handle server messages */ - if (client->handle_messages) { - - /* Only handle messages if synced within threshold */ - if (client->last_sent_timestamp - client->last_received_timestamp - < GUACD_SYNC_THRESHOLD) { - - int retval = client->handle_messages(client); - if (retval) { - guacd_client_log_guac_error(client, GUAC_LOG_DEBUG, - "Error handling server messages"); - guac_client_stop(client); - return NULL; - } - - /* Send sync instruction */ - client->last_sent_timestamp = guac_timestamp_current(); - if (guac_protocol_send_sync(socket, client->last_sent_timestamp)) { - guacd_client_log_guac_error(client, GUAC_LOG_DEBUG, - "Error sending \"sync\" instruction"); - guac_client_stop(client); - return NULL; - } - - /* Flush */ - if (guac_socket_flush(socket)) { - guacd_client_log_guac_error(client, GUAC_LOG_DEBUG, - "Error flushing output"); - guac_client_stop(client); - return NULL; - } - - } - - /* Do not spin while waiting for old sync */ - else - __guacdd_sleep(GUACD_MESSAGE_HANDLE_FREQUENCY); - - } - - /* If no message handler, just sleep until next sync ping */ - else - __guacdd_sleep(GUACD_SYNC_FREQUENCY); - - } /* End of output loop */ - - guac_client_log(client, GUAC_LOG_DEBUG, - "Output thread terminated."); - - guac_client_stop(client); - return NULL; - -} - -void* __guacd_client_input_thread(void* data) { - - guac_client* client = (guac_client*) data; - guac_socket* socket = client->socket; - - guac_client_log(client, GUAC_LOG_DEBUG, - "Starting input thread."); - - /* Guacamole client input loop */ - while (client->state == GUAC_CLIENT_RUNNING) { - - /* Read instruction */ - guac_instruction* instruction = - guac_instruction_read(socket, GUACD_USEC_TIMEOUT); - - /* Stop on error */ - if (instruction == NULL) { - - if (guac_error == GUAC_STATUS_TIMEOUT) - guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_TIMEOUT, "Client is not responding."); - - else { - if (guac_error != GUAC_STATUS_CLOSED) - guacd_client_log_guac_error(client, GUAC_LOG_WARNING, - "Guacamole connection failure"); - guac_client_stop(client); - } - - return NULL; - } - - /* Reset guac_error and guac_error_message (client handlers are not - * guaranteed to set these) */ - guac_error = GUAC_STATUS_SUCCESS; - guac_error_message = NULL; - - /* Call handler, stop on error */ - if (guac_client_handle_instruction(client, instruction) < 0) { - - /* Log error */ - guacd_client_log_guac_error(client, GUAC_LOG_WARNING, - "Connection aborted"); - - /* Log handler details */ - guac_client_log(client, GUAC_LOG_DEBUG, - "Failing instruction handler in client was \"%s\"", - instruction->opcode); - - guac_instruction_free(instruction); - guac_client_stop(client); - return NULL; - } - - /* Free allocated instruction */ - guac_instruction_free(instruction); - - } - - guac_client_log(client, GUAC_LOG_DEBUG, - "Input thread terminated."); - - return NULL; - -} - -int guacd_client_start(guac_client* client) { - - pthread_t input_thread, output_thread; - - if (pthread_create(&output_thread, NULL, __guacd_client_output_thread, (void*) client)) { - guac_client_log(client, GUAC_LOG_ERROR, "Unable to start output thread"); - return -1; - } - - if (pthread_create(&input_thread, NULL, __guacd_client_input_thread, (void*) client)) { - guac_client_log(client, GUAC_LOG_ERROR, "Unable to start input thread"); - guac_client_stop(client); - pthread_join(output_thread, NULL); - return -1; - } - - /* Wait for I/O threads */ - pthread_join(input_thread, NULL); - pthread_join(output_thread, NULL); - - /* Done */ - return 0; - -} - diff --git a/src/guacd/connection.c b/src/guacd/connection.c new file mode 100644 index 00000000..1973a8ce --- /dev/null +++ b/src/guacd/connection.c @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" + +#include "connection.h" +#include "log.h" +#include "move-fd.h" +#include "proc.h" +#include "proc-map.h" +#include "user.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_SSL +#include +#include "socket-ssl.h" +#endif + +#include +#include +#include +#include + +/** + * Behaves exactly as write(), but writes as much as possible, returning + * successfully only if the entire buffer was written. If the write fails for + * any reason, a negative value is returned. + * + * @param fd + * The file descriptor to write to. + * + * @param buffer + * The buffer containing the data to be written. + * + * @param length + * The number of bytes in the buffer to write. + * + * @return + * The number of bytes written, or -1 if an error occurs. As this function + * is guaranteed to write ALL bytes, this will always be the number of + * bytes specified by length unless an error occurs. + */ +static int __write_all(int fd, char* buffer, int length) { + + /* Repeatedly write() until all data is written */ + while (length > 0) { + + int written = write(fd, buffer, length); + if (written < 0) + return -1; + + length -= written; + buffer += written; + + } + + return length; + +} + +/** + * Continuously reads from a guac_socket, writing all data read to a file + * descriptor. Any data already buffered from that guac_socket by a given + * guac_parser is read first, prior to reading further data from the + * guac_socket. The provided guac_parser will be freed once its buffers have + * been emptied, but the guac_socket will not. + * + * This thread ultimately terminates when no further data can be read from the + * guac_socket. + * + * @param data + * A pointer to a guacd_connection_io_thread_params structure containing + * the guac_socket to read from, the file descriptor to write the read data + * to, and the guac_parser associated with the guac_socket which may have + * unhandled data in its parsing buffers. + * + * @return + * Always NULL. + */ +static void* guacd_connection_write_thread(void* data) { + + guacd_connection_io_thread_params* params = (guacd_connection_io_thread_params*) data; + char buffer[8192]; + + int length; + + /* Read all buffered data from parser first */ + while ((length = guac_parser_shift(params->parser, buffer, sizeof(buffer))) > 0) { + if (__write_all(params->fd, buffer, length) < 0) + break; + } + + /* Parser is no longer needed */ + guac_parser_free(params->parser); + + /* Transfer data from file descriptor to socket */ + while ((length = guac_socket_read(params->socket, buffer, sizeof(buffer))) > 0) { + if (__write_all(params->fd, buffer, length) < 0) + break; + } + + return NULL; + +} + +void* guacd_connection_io_thread(void* data) { + + guacd_connection_io_thread_params* params = (guacd_connection_io_thread_params*) data; + char buffer[8192]; + + int length; + + pthread_t write_thread; + pthread_create(&write_thread, NULL, guacd_connection_write_thread, params); + + /* Transfer data from file descriptor to socket */ + while ((length = read(params->fd, buffer, sizeof(buffer))) > 0) { + if (guac_socket_write(params->socket, buffer, length)) + break; + guac_socket_flush(params->socket); + } + + /* Wait for write thread to die */ + pthread_join(write_thread, NULL); + + /* Clean up */ + guac_socket_free(params->socket); + close(params->fd); + free(params); + + return NULL; + +} + +/** + * Adds the given socket as a new user to the given process, automatically + * reading/writing from the socket via read/write threads. The given socket and + * any associated resources will be freed unless the user is not added + * successfully. + * + * If adding the user fails for any reason, non-zero is returned. Zero is + * returned upon success. + * + * @param proc + * The existing process to add the user to. + * + * @param parser + * The parser associated with the given guac_socket (used to handle the + * user's connection handshake thus far). + * + * @param socket + * The socket associated with the user to be added to the existing + * process. + * + * @return + * Zero if the user was added successfully, non-zero if an error occurred. + */ +static int guacd_add_user(guacd_proc* proc, guac_parser* parser, guac_socket* socket) { + + int sockets[2]; + + /* Set up socket pair */ + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0) { + guacd_log(GUAC_LOG_ERROR, "Unable to allocate file descriptors for I/O transfer: %s", strerror(errno)); + return 1; + } + + int user_fd = sockets[0]; + int proc_fd = sockets[1]; + + /* Send user file descriptor to process */ + if (!guacd_send_fd(proc->fd_socket, proc_fd)) { + guacd_log(GUAC_LOG_ERROR, "Unable to add user."); + return 1; + } + + /* Close our end of the process file descriptor */ + close(proc_fd); + + guacd_connection_io_thread_params* params = malloc(sizeof(guacd_connection_io_thread_params)); + params->parser = parser; + params->socket = socket; + params->fd = user_fd; + + /* Start I/O thread */ + pthread_t io_thread; + pthread_create(&io_thread, NULL, guacd_connection_io_thread, params); + pthread_detach(io_thread); + + return 0; + +} + +/** + * Routes the connection on the given socket according to the Guacamole + * protocol, adding new users and creating new client processes as needed. If a + * new process is created, this function blocks until that process terminates, + * automatically deregistering the process at that point. + * + * The socket provided will be automatically freed when the connection + * terminates unless routing fails, in which case non-zero is returned. + * + * @param map + * The map of existing client processes. + * + * @param socket + * The socket associated with the new connection that must be routed to + * a new or existing process within the given map. + * + * @return + * Zero if the connection was successfully routed, non-zero if routing has + * failed. + */ +static int guacd_route_connection(guacd_proc_map* map, guac_socket* socket) { + + guac_parser* parser = guac_parser_alloc(); + + /* Reset guac_error */ + guac_error = GUAC_STATUS_SUCCESS; + guac_error_message = NULL; + + /* Get protocol from select instruction */ + if (guac_parser_expect(parser, socket, GUACD_USEC_TIMEOUT, "select")) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log_guac_error(GUAC_LOG_DEBUG, + "Error reading \"select\""); + + guac_parser_free(parser); + return 1; + } + + /* Validate args to select */ + if (parser->argc != 1) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log(GUAC_LOG_ERROR, "Bad number of arguments to \"select\" (%i)", + parser->argc); + + guac_parser_free(parser); + return 1; + } + + guacd_proc* proc; + int new_process; + + const char* identifier = parser->argv[0]; + + /* If connection ID, retrieve existing process */ + if (identifier[0] == GUAC_CLIENT_ID_PREFIX) { + + proc = guacd_proc_map_retrieve(map, identifier); + if (proc == NULL) + guacd_log(GUAC_LOG_INFO, "Connection \"%s\" does not exist.", identifier); + else + guacd_log(GUAC_LOG_INFO, "Joining existing connection \"%s\"", identifier); + + new_process = 0; + + } + + /* Otherwise, create new client */ + else { + + guacd_log(GUAC_LOG_INFO, "Creating new client for protocol \"%s\"", identifier); + proc = guacd_create_proc(identifier); + + new_process = 1; + + } + + if (proc == NULL) { + guacd_log_guac_error(GUAC_LOG_INFO, "Connection did not succeed"); + guac_parser_free(parser); + return 1; + } + + /* Add new user (in the case of a new process, this will be the owner */ + if (guacd_add_user(proc, parser, socket) == 0) { + + /* If new process was created, manage that process */ + if (new_process) { + + /* Log connection ID */ + guacd_log(GUAC_LOG_INFO, "Connection ID is \"%s\"", proc->client->connection_id); + + /* Store process, allowing other users to join */ + guacd_proc_map_add(map, proc); + + /* Wait for child to finish */ + waitpid(proc->pid, NULL, 0); + + /* Remove client */ + if (guacd_proc_map_remove(map, proc->client->connection_id) == NULL) + guacd_log(GUAC_LOG_ERROR, "Internal failure removing client \"%s\". Client record will never be freed.", + proc->client->connection_id); + else + guacd_log(GUAC_LOG_INFO, "Connection \"%s\" removed.", proc->client->connection_id); + + /* Free skeleton client */ + guac_client_free(proc->client); + + /* Clean up */ + close(proc->fd_socket); + free(proc); + + } + + return 0; + } + + /* Add of user failed */ + else { + guac_parser_free(parser); + return 1; + } + +} + +void* guacd_connection_thread(void* data) { + + guacd_connection_thread_params* params = (guacd_connection_thread_params*) data; + + guacd_proc_map* map = params->map; + int connected_socket_fd = params->connected_socket_fd; + + guac_socket* socket; + +#ifdef ENABLE_SSL + + SSL_CTX* ssl_context = params->ssl_context; + + /* If SSL chosen, use it */ + if (ssl_context != NULL) { + socket = guac_socket_open_secure(ssl_context, connected_socket_fd); + if (socket == NULL) { + guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to set up SSL/TLS"); + free(params); + return NULL; + } + } + else + socket = guac_socket_open(connected_socket_fd); + +#else + /* Open guac_socket */ + socket = guac_socket_open(connected_socket_fd); +#endif + + /* Route connection according to Guacamole, creating a new process if needed */ + if (guacd_route_connection(map, socket)) + guac_socket_free(socket); + + free(params); + return NULL; + +} + diff --git a/src/guacd/connection.h b/src/guacd/connection.h new file mode 100644 index 00000000..5a0c9d2f --- /dev/null +++ b/src/guacd/connection.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef GUACD_CONNECTION_H +#define GUACD_CONNECTION_H + +#include "config.h" + +#include "proc-map.h" + +#ifdef ENABLE_SSL +#include +#endif + +/** + * Parameters required by each connection thread. + */ +typedef struct guacd_connection_thread_params { + + /** + * The shared map of all connected clients. + */ + guacd_proc_map* map; + +#ifdef ENABLE_SSL + /** + * SSL context for encrypted connections to guacd. If SSL is not active, + * this will be NULL. + */ + SSL_CTX* ssl_context; +#endif + + /** + * The file descriptor associated with the newly-accepted connection. + */ + int connected_socket_fd; + +} guacd_connection_thread_params; + +/** + * Handles an inbound connection to guacd, allowing guacd to continue listening + * for other connections. The file descriptor of the inbound connection will + * either be given to a new process for a new remote desktop connection, or + * will be passed to an existing process for joining an existing remote desktop + * connection. It is expected that this thread will operate detached. The + * creating process need not join on the resulting thread. + * + * @param data + * A pointer to a guacd_connection_thread_params structure containing the + * shared overall map of currently-connected processes, the file + * descriptor associated with the newly-established connection that is to + * be either (1) associated with a new process or (2) passed on to an + * existing process, and the SSL context for the encryption surrounding + * that connection (if any). + * + * @return + * Always NULL. + */ +void* guacd_connection_thread(void* data); + +/** + * Parameters required by the per-connection I/O transfer thread. + */ +typedef struct guacd_connection_io_thread_params { + + /** + * The guac_parser which may contain buffered, but unparsed, data from the + * original guac_socket which must be transferred to the + * connection-specific process. + */ + guac_parser* parser; + + /** + * The guac_socket which is directly handling I/O from a user's connection + * to guacd. + */ + guac_socket* socket; + + /** + * The file descriptor which is being handled by a guac_socket within the + * connection-specific process. + */ + int fd; + +} guacd_connection_io_thread_params; + +/** + * Transfers data back and forth between the guacd-side guac_socket and the + * file descriptor used by the process-side guac_socket. Note that both the + * provided guac_parser and the guac_socket will be freed once this thread + * terminates, which will occur when no further data can be read from the + * guac_socket. + * + * @param data + * A pointer to a guacd_connection_io_thread_params structure containing + * the guac_socket and file descriptor to transfer data between + * (bidirectionally), as well as the guac_parser associated with the + * guac_socket (which may have unhandled data in its parsing buffers). + * + * @return + * Always NULL. + */ +void* guacd_connection_io_thread(void* data); + +#endif + diff --git a/src/guacd/daemon.c b/src/guacd/daemon.c index e7327044..d024e757 100644 --- a/src/guacd/daemon.c +++ b/src/guacd/daemon.c @@ -22,22 +22,15 @@ #include "config.h" -#include "client.h" -#include "client-map.h" +#include "connection.h" #include "conf-args.h" #include "conf-file.h" #include "log.h" - -#include -#include -#include -#include -#include -#include +#include "proc-map.h" +#include "user.h" #ifdef ENABLE_SSL #include -#include "socket-ssl.h" #endif #include @@ -59,347 +52,21 @@ #define GUACD_ROOT "/" /** - * Logs a reasonable explanatory message regarding handshake failure based on - * the current value of guac_error. - */ -static void guacd_log_handshake_failure() { - - if (guac_error == GUAC_STATUS_CLOSED) - guacd_log(GUAC_LOG_INFO, - "Guacamole connection closed during handshake"); - else if (guac_error == GUAC_STATUS_PROTOCOL_ERROR) - guacd_log(GUAC_LOG_ERROR, - "Guacamole protocol violation. Perhaps the version of " - "guacamole-client is incompatible with this version of " - "guacd?"); - else - guacd_log(GUAC_LOG_WARNING, - "Guacamole handshake failed: %s", - guac_status_string(guac_error)); - -} - -/** - * Copies the given array of mimetypes (strings) into a newly-allocated NULL- - * terminated array of strings. Both the array and the strings within the array - * are newly-allocated and must be later freed via guacd_free_mimetypes(). + * Redirects the given file descriptor to /dev/null. The given flags must match + * the read/write flags of the file descriptor given (if the given file + * descriptor was opened write-only, flags here must be O_WRONLY, etc.). * - * @param mimetypes - * The array of mimetypes to copy. + * @param fd + * The file descriptor to redirect to /dev/null. * - * @param count - * The number of mimetypes in the given array. + * @param flags + * The flags to use when opening /dev/null as the target for redirection. + * These flags must match the flags of the file descriptor given. * * @return - * A newly-allocated, NULL-terminated array containing newly-allocated - * copies of each of the mimetypes provided in the original mimetypes - * array. + * Zero on success, non-zero if redirecting the file descriptor fails. */ -static char** guacd_copy_mimetypes(char** mimetypes, int count) { - - int i; - - /* Allocate sufficient space for NULL-terminated array of mimetypes */ - char** mimetypes_copy = malloc(sizeof(char*) * (count+1)); - - /* Copy each provided mimetype */ - for (i = 0; i < count; i++) - mimetypes_copy[i] = strdup(mimetypes[i]); - - /* Terminate with NULL */ - mimetypes_copy[count] = NULL; - - return mimetypes_copy; - -} - -/** - * Frees the given array of mimetypes, including the space allocated to each - * mimetype string within the array. The provided array of mimetypes MUST have - * been allocated with guacd_copy_mimetypes(). - * - * @param mimetypes - * The NULL-terminated array of mimetypes to free. This array MUST have - * been previously allocated with guacd_copy_mimetypes(). - */ -static void guacd_free_mimetypes(char** mimetypes) { - - char** current_mimetype = mimetypes; - - /* Free all strings within NULL-terminated mimetype array */ - while (*current_mimetype != NULL) { - free(*current_mimetype); - current_mimetype++; - } - - /* Free the array itself, now that its contents have been freed */ - free(mimetypes); - -} - -/** - * Creates a new guac_client for the connection on the given socket, adding - * it to the client map based on its ID. - */ -static void guacd_handle_connection(guacd_client_map* map, guac_socket* socket) { - - guac_client* client; - guac_client_plugin* plugin; - guac_instruction* select; - guac_instruction* size; - guac_instruction* audio; - guac_instruction* video; - guac_instruction* image; - guac_instruction* connect; - int init_result; - - /* Reset guac_error */ - guac_error = GUAC_STATUS_SUCCESS; - guac_error_message = NULL; - - /* Get protocol from select instruction */ - select = guac_instruction_expect(socket, GUACD_USEC_TIMEOUT, "select"); - if (select == NULL) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log_guac_error(GUAC_LOG_DEBUG, - "Error reading \"select\""); - - /* Free resources */ - guac_socket_free(socket); - return; - - } - - /* Validate args to select */ - if (select->argc != 1) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log(GUAC_LOG_ERROR, "Bad number of arguments to \"select\" (%i)", - select->argc); - - /* Free resources */ - guac_socket_free(socket); - return; - } - - guacd_log(GUAC_LOG_INFO, "Protocol \"%s\" selected", select->argv[0]); - - /* Get plugin from protocol in select */ - plugin = guac_client_plugin_open(select->argv[0]); - guac_instruction_free(select); - - if (plugin == NULL) { - - /* Log error */ - if (guac_error == GUAC_STATUS_NOT_FOUND) - guacd_log(GUAC_LOG_WARNING, - "Support for selected protocol is not installed"); - else - guacd_log_guac_error(GUAC_LOG_ERROR, - "Unable to load client plugin"); - - /* Free resources */ - guac_socket_free(socket); - return; - } - - /* Send args response */ - if (guac_protocol_send_args(socket, plugin->args) - || guac_socket_flush(socket)) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log_guac_error(GUAC_LOG_DEBUG, "Error sending \"args\""); - - if (guac_client_plugin_close(plugin)) - guacd_log_guac_error(GUAC_LOG_WARNING, - "Unable to close client plugin"); - - guac_socket_free(socket); - return; - } - - /* Get client */ - client = guac_client_alloc(); - if (client == NULL) { - guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to create client"); - guac_socket_free(socket); - return; - } - - /* Get optimal screen size */ - size = guac_instruction_expect( - socket, GUACD_USEC_TIMEOUT, "size"); - if (size == NULL) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"size\""); - - /* Free resources */ - guac_client_free(client); - guac_socket_free(socket); - return; - } - - /* Parse optimal screen dimensions from size instruction */ - client->info.optimal_width = atoi(size->argv[0]); - client->info.optimal_height = atoi(size->argv[1]); - - /* If DPI given, set the client resolution */ - if (size->argc >= 3) - client->info.optimal_resolution = atoi(size->argv[2]); - - /* Otherwise, use a safe default for rough backwards compatibility */ - else - client->info.optimal_resolution = 96; - - guac_instruction_free(size); - - /* Get supported audio formats */ - audio = guac_instruction_expect( - socket, GUACD_USEC_TIMEOUT, "audio"); - if (audio == NULL) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"audio\""); - - /* Free resources */ - guac_client_free(client); - guac_socket_free(socket); - return; - } - - /* Store audio mimetypes */ - client->info.audio_mimetypes = - guacd_copy_mimetypes(audio->argv, audio->argc); - - guac_instruction_free(audio); - - /* Get supported video formats */ - video = guac_instruction_expect( - socket, GUACD_USEC_TIMEOUT, "video"); - if (video == NULL) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"video\""); - - /* Free resources */ - guac_client_free(client); - guac_socket_free(socket); - return; - } - - /* Store video mimetypes */ - client->info.video_mimetypes = - guacd_copy_mimetypes(video->argv, video->argc); - - guac_instruction_free(video); - - /* Get supported image formats */ - image = guac_instruction_expect( - socket, GUACD_USEC_TIMEOUT, "image"); - if (image == NULL) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"image\""); - - /* Free resources */ - guac_client_free(client); - guac_socket_free(socket); - return; - } - - /* Store image mimetypes */ - client->info.image_mimetypes = - guacd_copy_mimetypes(image->argv, image->argc); - - guac_instruction_free(image); - - /* Get args from connect instruction */ - connect = guac_instruction_expect( - socket, GUACD_USEC_TIMEOUT, "connect"); - if (connect == NULL) { - - /* Log error */ - guacd_log_handshake_failure(); - guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"connect\""); - - if (guac_client_plugin_close(plugin)) - guacd_log_guac_error(GUAC_LOG_WARNING, - "Unable to close client plugin"); - - guac_client_free(client); - guac_socket_free(socket); - return; - } - - client->socket = socket; - client->log_handler = guacd_client_log; - - /* Store client */ - if (guacd_client_map_add(map, client)) - guacd_log(GUAC_LOG_ERROR, "Unable to add client. Internal client storage has failed"); - - /* Send connection ID */ - guacd_log(GUAC_LOG_INFO, "Connection ID is \"%s\"", client->connection_id); - guac_protocol_send_ready(socket, client->connection_id); - - /* Init client */ - init_result = guac_client_plugin_init_client(plugin, - client, connect->argc, connect->argv); - - guac_instruction_free(connect); - - /* If client could not be started, free everything and fail */ - if (init_result) { - - guac_client_free(client); - - guacd_log_guac_error(GUAC_LOG_INFO, "Connection did not succeed"); - - if (guac_client_plugin_close(plugin)) - guacd_log_guac_error(GUAC_LOG_WARNING, - "Unable to close client plugin"); - - guac_socket_free(socket); - return; - } - - /* Start client threads */ - guacd_log(GUAC_LOG_INFO, "Starting client"); - if (guacd_client_start(client)) - guacd_log(GUAC_LOG_WARNING, "Client finished abnormally"); - else - guacd_log(GUAC_LOG_INFO, "Client disconnected"); - - /* Remove client */ - if (guacd_client_map_remove(map, client->connection_id) == NULL) - guacd_log(GUAC_LOG_ERROR, "Unable to remove client. Internal client storage has failed"); - - /* Free mimetype lists */ - guacd_free_mimetypes(client->info.audio_mimetypes); - guacd_free_mimetypes(client->info.video_mimetypes); - guacd_free_mimetypes(client->info.image_mimetypes); - - /* Clean up */ - guac_client_free(client); - if (guac_client_plugin_close(plugin)) - guacd_log_guac_error(GUAC_LOG_WARNING, - "Unable to close client plugin"); - - /* Close socket */ - guac_socket_free(socket); - -} - -int redirect_fd(int fd, int flags) { +static int redirect_fd(int fd, int flags) { /* Attempt to open bit bucket */ int new_fd = open(GUACD_DEV_NULL, flags); @@ -416,7 +83,21 @@ int redirect_fd(int fd, int flags) { } -int daemonize() { +/** + * Turns the current process into a daemon through a series of fork() calls. + * The standard I/O file desriptors for STDIN, STDOUT, and STDERR will be + * redirected to /dev/null, and the working directory is changed to root. + * Execution within the caller of this function will terminate before this + * function returns, while execution within the daemonized child process will + * continue. + * + * @return + * Zero if the daemonization process succeeded and we are now in the + * daemonized child process, or non-zero if daemonization failed and we are + * still the original caller. This function does not return for the original + * caller if daemonization succeeds. + */ +static int daemonize() { pid_t pid; @@ -499,7 +180,7 @@ int main(int argc, char* argv[]) { SSL_CTX* ssl_context = NULL; #endif - guacd_client_map* map = guacd_client_map_alloc(); + guacd_proc_map* map = guacd_proc_map_alloc(); /* General */ int retval; @@ -675,7 +356,7 @@ int main(int argc, char* argv[]) { /* Daemon loop */ for (;;) { - pid_t child_pid; + pthread_t child_thread; /* Accept connection */ client_addr_len = sizeof(client_addr); @@ -683,59 +364,27 @@ int main(int argc, char* argv[]) { (struct sockaddr*) &client_addr, &client_addr_len); if (connected_socket_fd < 0) { - guacd_log(GUAC_LOG_ERROR, "Could not accept client connection: %s", - strerror(errno)); - return 3; + guacd_log(GUAC_LOG_ERROR, "Could not accept client connection: %s", strerror(errno)); + continue; } - /* - * Once connection is accepted, send child into background. - * - * Note that we prefer fork() over threads for connection-handling - * processes as they give each connection its own memory area, and - * isolate the main daemon and other connections from errors in any - * particular client plugin. - */ + /* Create parameters for connection thread */ + guacd_connection_thread_params* params = malloc(sizeof(guacd_connection_thread_params)); + if (params == NULL) { + guacd_log(GUAC_LOG_ERROR, "Could not create connection thread: %s", strerror(errno)); + continue; + } - child_pid = fork(); - - /* If error, log */ - if (child_pid == -1) - guacd_log(GUAC_LOG_ERROR, "Error forking child process: %s", strerror(errno)); - - /* If child, start client, and exit when finished */ - else if (child_pid == 0) { - - guac_socket* socket; + params->map = map; + params->connected_socket_fd = connected_socket_fd; #ifdef ENABLE_SSL - - /* If SSL chosen, use it */ - if (ssl_context != NULL) { - socket = guac_socket_open_secure(ssl_context, connected_socket_fd); - if (socket == NULL) { - guacd_log_guac_error(GUAC_LOG_ERROR, - "Unable to set up SSL/TLS"); - return 0; - } - } - else - socket = guac_socket_open(connected_socket_fd); -#else - /* Open guac_socket */ - socket = guac_socket_open(connected_socket_fd); + params->ssl_context = ssl_context; #endif - guacd_handle_connection(map, socket); - close(connected_socket_fd); - return 0; - } - - /* If parent, close reference to child's descriptor */ - else if (close(connected_socket_fd) < 0) { - guacd_log(GUAC_LOG_ERROR, "Error closing daemon reference to " - "child descriptor: %s", strerror(errno)); - } + /* Spawn thread to handle connection */ + pthread_create(&child_thread, NULL, guacd_connection_thread, params); + pthread_detach(child_thread); } diff --git a/src/guacd/log.c b/src/guacd/log.c index 375877c5..2f8f0354 100644 --- a/src/guacd/log.c +++ b/src/guacd/log.c @@ -152,3 +152,20 @@ void guacd_client_log_guac_error(guac_client* client, } +void guacd_log_handshake_failure() { + + if (guac_error == GUAC_STATUS_CLOSED) + guacd_log(GUAC_LOG_INFO, + "Guacamole connection closed during handshake"); + else if (guac_error == GUAC_STATUS_PROTOCOL_ERROR) + guacd_log(GUAC_LOG_ERROR, + "Guacamole protocol violation. Perhaps the version of " + "guacamole-client is incompatible with this version of " + "guacd?"); + else + guacd_log(GUAC_LOG_WARNING, + "Guacamole handshake failed: %s", + guac_status_string(guac_error)); + +} + diff --git a/src/guacd/log.h b/src/guacd/log.h index bdb84521..68f9f78a 100644 --- a/src/guacd/log.h +++ b/src/guacd/log.h @@ -73,5 +73,11 @@ void guacd_log_guac_error(guac_client_log_level level, const char* message); void guacd_client_log_guac_error(guac_client* client, guac_client_log_level level, const char* message); +/** + * Logs a reasonable explanatory message regarding handshake failure based on + * the current value of guac_error. + */ +void guacd_log_handshake_failure(); + #endif diff --git a/src/guacd/move-fd.c b/src/guacd/move-fd.c new file mode 100644 index 00000000..07957336 --- /dev/null +++ b/src/guacd/move-fd.c @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" +#include "move-fd.h" + +/* Required for CMSG_* macros on BSD */ +#define __BSD_VISIBLE 1 + +#include +#include +#include +#include +#include +#include +#include + +int guacd_send_fd(int sock, int fd) { + + struct msghdr message = {0}; + char message_data[] = {'G'}; + + /* Assign data buffer */ + struct iovec io_vector[1]; + io_vector[0].iov_base = message_data; + io_vector[0].iov_len = sizeof(message_data); + message.msg_iov = io_vector; + message.msg_iovlen = 1; + + /* Assign ancillary data buffer */ + char buffer[CMSG_SPACE(sizeof(fd))]; + message.msg_control = buffer; + message.msg_controllen = sizeof(buffer); + + /* Set fields of control message header */ + struct cmsghdr* control = CMSG_FIRSTHDR(&message); + control->cmsg_level = SOL_SOCKET; + control->cmsg_type = SCM_RIGHTS; + control->cmsg_len = CMSG_LEN(sizeof(fd)); + + /* Add file descriptor to message data */ + memcpy(CMSG_DATA(control), &fd, sizeof(fd)); + + /* Send file descriptor */ + return (sendmsg(sock, &message, 0) == sizeof(message_data)); + +} + +int guacd_recv_fd(int sock) { + + int fd; + + struct msghdr message = {0}; + char message_data[1]; + + /* Assign data buffer */ + struct iovec io_vector[1]; + io_vector[0].iov_base = message_data; + io_vector[0].iov_len = sizeof(message_data); + message.msg_iov = io_vector; + message.msg_iovlen = 1; + + + /* Assign ancillary data buffer */ + char buffer[CMSG_SPACE(sizeof(fd))]; + message.msg_control = buffer; + message.msg_controllen = sizeof(buffer); + + /* Receive file descriptor */ + if (recvmsg(sock, &message, 0) == sizeof(message_data)) { + + /* Validate payload */ + if (message_data[0] != 'G') { + errno = EPROTO; + return -1; + } + + /* Iterate control headers, looking for the sent file descriptor */ + struct cmsghdr* control; + for (control = CMSG_FIRSTHDR(&message); control != NULL; control = CMSG_NXTHDR(&message, control)) { + + /* Pull file descriptor from data */ + if (control->cmsg_level == SOL_SOCKET && control->cmsg_type == SCM_RIGHTS) { + memcpy(&fd, CMSG_DATA(control), sizeof(fd)); + return fd; + } + + } + + } /* end if recvmsg() success */ + + /* Failed to receive file descriptor */ + return -1; + +} + diff --git a/src/guacd/move-fd.h b/src/guacd/move-fd.h new file mode 100644 index 00000000..8925db08 --- /dev/null +++ b/src/guacd/move-fd.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef GUACD_MOVE_FD_H +#define GUACD_MOVE_FD_H + +#include "config.h" + +/** + * Sends the given file descriptor along the given socket, allowing the + * receiving process to use that file descriptor normally. Returns non-zero on + * success, zero on error, just as a normal call to sendmsg() would. If an + * error does occur, errno will be set appropriately. + * + * @param sock + * The file descriptor of an open UNIX domain socket along which the file + * descriptor specified by fd should be sent. + * + * @param fd + * The file descriptor to send along the given UNIX domain socket. + * + * @return + * Non-zero if the send operation succeeded, zero on error. + */ +int guacd_send_fd(int sock, int fd); + +/** + * Waits for a file descriptor on the given socket, returning the received file + * descriptor. The file descriptor must have been sent via guacd_send_fd. If an + * error occurs, -1 is returned, and errno will be set appropriately. + * + * @param sock + * The file descriptor of an open UNIX domain socket along which the file + * descriptor will be sent (by guacd_send_fd()). + * + * @return + * The received file descriptor, or -1 if an error occurs preventing + * receipt of the file descriptor. + */ +int guacd_recv_fd(int sock); + +#endif + diff --git a/src/guacd/client-map.c b/src/guacd/proc-map.c similarity index 54% rename from src/guacd/client-map.c rename to src/guacd/proc-map.c index a13478a6..faa6dd1b 100644 --- a/src/guacd/client-map.c +++ b/src/guacd/proc-map.c @@ -21,9 +21,10 @@ */ #include "config.h" -#include "client.h" -#include "client-map.h" #include "guac_list.h" +#include "proc.h" +#include "proc-map.h" +#include "user.h" #include @@ -31,7 +32,13 @@ #include /** - * Returns a hash code based on the connection ID of the given client. + * Returns a hash code based on the given connection ID. + * + * @param str + * The string containing the connection ID. + * + * @return + * A reasonably well-distributed hash code for the given string. */ static unsigned int __guacd_client_hash(const char* str) { @@ -47,21 +54,47 @@ static unsigned int __guacd_client_hash(const char* str) { } /** - * Locates the bucket corresponding to the hash code indicated by the give id. - * Each bucket is an instance of guac_common_list. + * Locates the bucket corresponding to the hash code indicated by the given id, + * where the hash code is dictated by __guacd_client_hash(). Each bucket is an + * instance of guac_common_list. + * + * @param map + * The map to retrieve the hash bucket from. + * + * @param id + * The ID whose hash code determines the bucket being retrieved. + * + * @return + * The bucket corresponding to the hash code for the given ID, represented + * by a guac_common_list. */ -static guac_common_list* __guacd_client_find_bucket(guacd_client_map* map, const char* id) { +static guac_common_list* __guacd_proc_find_bucket(guacd_proc_map* map, + const char* id) { - const int index = __guacd_client_hash(id) % GUACD_CLIENT_MAP_BUCKETS; + const int index = __guacd_client_hash(id) % GUACD_PROC_MAP_BUCKETS; return map->__buckets[index]; } /** - * Given a list of guac_client instances, returns the guac_client having the - * given ID, or NULL if no such client is stored. + * Given a bucket of guacd_proc instances, returns the guacd_proc having the + * guac_client with the given ID, or NULL if no such client is stored. + * + * @param bucket + * The bucket of guacd_proc instances to search, represented as a + * guac_common_list. + * + * @param id + * The ID of the guac_client whose corresponding guacd_proc instance should + * be located within the bucket. + * + * @return + * The guac_common_list_element containing the guacd_proc instance + * corresponding to the guac_client having the given ID, or NULL of no such + * element exists. */ -static guac_common_list_element* __guacd_client_find(guac_common_list* bucket, const char* id) { +static guac_common_list_element* __guacd_proc_find(guac_common_list* bucket, + const char* id) { guac_common_list_element* current = bucket->head; @@ -69,8 +102,8 @@ static guac_common_list_element* __guacd_client_find(guac_common_list* bucket, c while (current != NULL) { /* Check connection ID */ - guac_client* client = (guac_client*) current->data; - if (strcmp(client->connection_id, id) == 0) + guacd_proc* proc = (guacd_proc*) current->data; + if (strcmp(proc->client->connection_id, id) == 0) break; current = current->next; @@ -80,9 +113,9 @@ static guac_common_list_element* __guacd_client_find(guac_common_list* bucket, c } -guacd_client_map* guacd_client_map_alloc() { +guacd_proc_map* guacd_proc_map_alloc() { - guacd_client_map* map = malloc(sizeof(guacd_client_map)); + guacd_proc_map* map = malloc(sizeof(guacd_proc_map)); guac_common_list** current; int i; @@ -90,7 +123,7 @@ guacd_client_map* guacd_client_map_alloc() { /* Init all buckets */ current = map->__buckets; - for (i=0; iconnection_id); + const char* identifier = proc->client->connection_id; + guac_common_list* bucket = __guacd_proc_find_bucket(map, identifier); guac_common_list_element* found; /* Retrieve corresponding element, if any */ guac_common_list_lock(bucket); - found = __guacd_client_find(bucket, client->connection_id); + found = __guacd_proc_find(bucket, identifier); /* If no such element, we can add the new client successfully */ if (found == NULL) { - guac_common_list_add(bucket, client); + guac_common_list_add(bucket, proc); guac_common_list_unlock(bucket); return 0; } @@ -121,16 +155,16 @@ int guacd_client_map_add(guacd_client_map* map, guac_client* client) { } -guac_client* guacd_client_map_retrieve(guacd_client_map* map, const char* id) { +guacd_proc* guacd_proc_map_retrieve(guacd_proc_map* map, const char* id) { - guac_client* client; + guacd_proc* proc; - guac_common_list* bucket = __guacd_client_find_bucket(map, id); + guac_common_list* bucket = __guacd_proc_find_bucket(map, id); guac_common_list_element* found; /* Retrieve corresponding element, if any */ guac_common_list_lock(bucket); - found = __guacd_client_find(bucket, id); + found = __guacd_proc_find(bucket, id); /* If no such element, fail */ if (found == NULL) { @@ -138,23 +172,23 @@ guac_client* guacd_client_map_retrieve(guacd_client_map* map, const char* id) { return NULL; } - client = (guac_client*) found->data; + proc = (guacd_proc*) found->data; guac_common_list_unlock(bucket); - return client; + return proc; } -guac_client* guacd_client_map_remove(guacd_client_map* map, const char* id) { +guacd_proc* guacd_proc_map_remove(guacd_proc_map* map, const char* id) { - guac_client* client; + guacd_proc* proc; - guac_common_list* bucket = __guacd_client_find_bucket(map, id); + guac_common_list* bucket = __guacd_proc_find_bucket(map, id); guac_common_list_element* found; /* Retrieve corresponding element, if any */ guac_common_list_lock(bucket); - found = __guacd_client_find(bucket, id); + found = __guacd_proc_find(bucket, id); /* If no such element, fail */ if (found == NULL) { @@ -162,11 +196,11 @@ guac_client* guacd_client_map_remove(guacd_client_map* map, const char* id) { return NULL; } - client = (guac_client*) found->data; + proc = (guacd_proc*) found->data; guac_common_list_remove(bucket, found); guac_common_list_unlock(bucket); - return client; + return proc; } diff --git a/src/guacd/proc-map.h b/src/guacd/proc-map.h new file mode 100644 index 00000000..df860be8 --- /dev/null +++ b/src/guacd/proc-map.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +#ifndef _GUACD_PROC_MAP_H +#define _GUACD_PROC_MAP_H + +#include "config.h" +#include "guac_list.h" +#include "proc.h" +#include "user.h" + +#include + +/** + * The number of hash buckets in each process map. + */ +#define GUACD_PROC_MAP_BUCKETS GUACD_CLIENT_MAX_CONNECTIONS*2 + +/** + * Set of all active connections to guacd, indexed by connection ID. + */ +typedef struct guacd_proc_map { + + /** + * Internal hash buckets. Each bucket is a linked list containing all + * guac_client instances which hash to this bucket location. + */ + guac_common_list* __buckets[GUACD_PROC_MAP_BUCKETS]; + +} guacd_proc_map; + +/** + * Allocates a new client process map. There is intended to be exactly one + * process map instance, which persists for the life of guacd. + * + * @return + * A newly-allocated client process map. + */ +guacd_proc_map* guacd_proc_map_alloc(); + +/** + * Adds the given process to the client process map. On success, zero is + * returned. If adding the client fails (due to lack of space, or duplicate + * ID), a non-zero value is returned instead. The client process is stored by + * the connection ID of the underlying guac_client. + * + * @param map + * The map in which the given client process should be stored. + * + * @param proc + * The client process to store in the given map. + * + * @return + * Zero if the process was successfully stored in the map, or non-zero if + * storing the process fails for any reason. + */ +int guacd_proc_map_add(guacd_proc_map* map, guacd_proc* proc); + +/** + * Retrieves the client process having the client with the given ID, or NULL if + * no such process is stored. + * + * @param map + * The map from which to retrieve the process associated with the client + * having the given ID. + * + * @param id + * The ID of the client whose process should be retrieved. + * + * @return + * The process associated with the client having the given ID, or NULL if + * no such process exists. + */ +guacd_proc* guacd_proc_map_retrieve(guacd_proc_map* map, const char* id); + +/** + * Removes the client process having the client with the given ID, returning + * the corresponding process. If no such process exists, NULL is returned. + * + * @param map + * The map from which to remove the process associated with the client + * having the given ID. + * + * @param id + * The ID of the client whose process should be removed. + * + * @return + * The process associated with the client having the given ID which has now + * been removed from the given map, or NULL if no such process exists. + */ +guacd_proc* guacd_proc_map_remove(guacd_proc_map* map, const char* id); + +#endif + diff --git a/src/guacd/proc.c b/src/guacd/proc.c new file mode 100644 index 00000000..4ccb041b --- /dev/null +++ b/src/guacd/proc.c @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" + +#include "log.h" +#include "move-fd.h" +#include "proc.h" +#include "proc-map.h" +#include "user.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/** + * Copies the given array of mimetypes (strings) into a newly-allocated NULL- + * terminated array of strings. Both the array and the strings within the array + * are newly-allocated and must be later freed via guacd_free_mimetypes(). + * + * @param mimetypes + * The array of mimetypes to copy. + * + * @param count + * The number of mimetypes in the given array. + * + * @return + * A newly-allocated, NULL-terminated array containing newly-allocated + * copies of each of the mimetypes provided in the original mimetypes + * array. + */ +static char** guacd_copy_mimetypes(char** mimetypes, int count) { + + int i; + + /* Allocate sufficient space for NULL-terminated array of mimetypes */ + char** mimetypes_copy = malloc(sizeof(char*) * (count+1)); + + /* Copy each provided mimetype */ + for (i = 0; i < count; i++) + mimetypes_copy[i] = strdup(mimetypes[i]); + + /* Terminate with NULL */ + mimetypes_copy[count] = NULL; + + return mimetypes_copy; + +} + +/** + * Frees the given array of mimetypes, including the space allocated to each + * mimetype string within the array. The provided array of mimetypes MUST have + * been allocated with guacd_copy_mimetypes(). + * + * @param mimetypes + * The NULL-terminated array of mimetypes to free. This array MUST have + * been previously allocated with guacd_copy_mimetypes(). + */ +static void guacd_free_mimetypes(char** mimetypes) { + + char** current_mimetype = mimetypes; + + /* Free all strings within NULL-terminated mimetype array */ + while (*current_mimetype != NULL) { + free(*current_mimetype); + current_mimetype++; + } + + /* Free the array itself, now that its contents have been freed */ + free(mimetypes); + +} + +/** + * Handles the initial handshake of a user and all subsequent I/O. This + * function blocks until the user disconnects. + * + * @param user + * The user whose handshake and entire Guacamole protocol exchange should + * be handled. + * + * @return + * Zero if the user's Guacamole connection was successfully handled and + * the user has disconnected, or non-zero if an error prevented the user's + * connection from being handled properly. + */ +static int guacd_handle_user(guac_user* user) { + + guac_socket* socket = user->socket; + guac_client* client = user->client; + + /* Send args */ + if (guac_protocol_send_args(socket, client->args) + || guac_socket_flush(socket)) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log_guac_error(GUAC_LOG_DEBUG, "Error sending \"args\" to new user"); + + return 1; + } + + guac_parser* parser = guac_parser_alloc(); + + /* Get optimal screen size */ + if (guac_parser_expect(parser, socket, GUACD_USEC_TIMEOUT, "size")) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"size\""); + + guac_parser_free(parser); + return 1; + } + + /* Validate content of size instruction */ + if (parser->argc < 2) { + guacd_log(GUAC_LOG_ERROR, "Received \"size\" instruction lacked required arguments."); + guac_parser_free(parser); + return 1; + } + + /* Parse optimal screen dimensions from size instruction */ + user->info.optimal_width = atoi(parser->argv[0]); + user->info.optimal_height = atoi(parser->argv[1]); + + /* If DPI given, set the client resolution */ + if (parser->argc >= 3) + user->info.optimal_resolution = atoi(parser->argv[2]); + + /* Otherwise, use a safe default for rough backwards compatibility */ + else + user->info.optimal_resolution = 96; + + /* Get supported audio formats */ + if (guac_parser_expect(parser, socket, GUACD_USEC_TIMEOUT, "audio")) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"audio\""); + + guac_parser_free(parser); + return 1; + } + + /* Store audio mimetypes */ + char** audio_mimetypes = guacd_copy_mimetypes(parser->argv, parser->argc); + user->info.audio_mimetypes = (const char**) audio_mimetypes; + + /* Get supported video formats */ + if (guac_parser_expect(parser, socket, GUACD_USEC_TIMEOUT, "video")) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"video\""); + + guac_parser_free(parser); + return 1; + } + + /* Store video mimetypes */ + char** video_mimetypes = guacd_copy_mimetypes(parser->argv, parser->argc); + user->info.video_mimetypes = (const char**) video_mimetypes; + + /* Get supported image formats */ + if (guac_parser_expect(parser, socket, GUACD_USEC_TIMEOUT, "image")) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"image\""); + + guac_parser_free(parser); + return 1; + } + + /* Store image mimetypes */ + char** image_mimetypes = guacd_copy_mimetypes(parser->argv, parser->argc); + user->info.image_mimetypes = (const char**) image_mimetypes; + + /* Get args from connect instruction */ + if (guac_parser_expect(parser, socket, GUACD_USEC_TIMEOUT, "connect")) { + + /* Log error */ + guacd_log_handshake_failure(); + guacd_log_guac_error(GUAC_LOG_DEBUG, "Error reading \"connect\""); + + guac_parser_free(parser); + return 1; + } + + /* Acknowledge connection availability */ + guac_protocol_send_ready(socket, client->connection_id); + guac_socket_flush(socket); + + /* Attempt join */ + if (guac_client_add_user(client, user, parser->argc, parser->argv)) + guacd_log(GUAC_LOG_ERROR, "User \"%s\" could NOT join connection \"%s\"", user->user_id, client->connection_id); + + /* Begin user connection if join successful */ + else { + + guacd_log(GUAC_LOG_INFO, "User \"%s\" joined connection \"%s\" (%i users now present)", + user->user_id, client->connection_id, client->connected_users); + + /* Handle user I/O, wait for connection to terminate */ + guacd_user_start(parser, user); + + /* Remove/free user */ + guac_client_remove_user(client, user); + guacd_log(GUAC_LOG_INFO, "User \"%s\" disconnected (%i users remain)", user->user_id, client->connected_users); + + } + + /* Free mimetype lists */ + guacd_free_mimetypes(audio_mimetypes); + guacd_free_mimetypes(video_mimetypes); + guacd_free_mimetypes(image_mimetypes); + + guac_parser_free(parser); + + /* Successful disconnect */ + return 0; + +} + +/** + * Parameters for the user thread. + */ +typedef struct guacd_user_thread_params { + + /** + * The process being joined. + */ + guacd_proc* proc; + + /** + * The file descriptor of the joining user's socket. + */ + int fd; + + /** + * Whether the joining user is the connection owner. + */ + int owner; + +} guacd_user_thread_params; + +/** + * Handles a user's entire connection and socket lifecycle. + * + * @param data + * A pointer to a guacd_user_thread_params structure describing the user's + * associated file descriptor, whether that user is the connection owner + * (the first person to join), as well as the process associated with the + * connection being joined. + * + * @return + * Always NULL. + */ +static void* guacd_user_thread(void* data) { + + guacd_user_thread_params* params = (guacd_user_thread_params*) data; + guacd_proc* proc = params->proc; + guac_client* client = proc->client; + + /* Get guac_socket for user's file descriptor */ + guac_socket* socket = guac_socket_open(params->fd); + if (socket == NULL) + return NULL; + + /* Create skeleton user */ + guac_user* user = guac_user_alloc(); + user->socket = socket; + user->client = client; + user->owner = params->owner; + + /* Handle user connection from handshake until disconnect/completion */ + guacd_handle_user(user); + + /* Stop client and prevent future users if all users are disconnected */ + if (client->connected_users == 0) { + guacd_log(GUAC_LOG_INFO, "Last user of connection \"%s\" disconnected", client->connection_id); + guacd_proc_stop(proc); + } + + /* Clean up */ + guac_socket_free(socket); + guac_user_free(user); + free(params); + + return NULL; + +} + +/** + * Begins a new user connection under a given process, using the given file + * descriptor. The connection will be managed by a separate and detached thread + * which is started by this function. + * + * @param proc + * The process that the user is being added to. + * + * @param fd + * The file descriptor associated with the user's network connection to + * guacd. + * + * @param owner + * Non-zero if the user is the owner of the connection being joined (they + * are the first user to join), or zero otherwise. + */ +static void guacd_proc_add_user(guacd_proc* proc, int fd, int owner) { + + guacd_user_thread_params* params = malloc(sizeof(guacd_user_thread_params)); + params->proc = proc; + params->fd = fd; + params->owner = owner; + + /* Start user thread */ + pthread_t user_thread; + pthread_create(&user_thread, NULL, guacd_user_thread, params); + pthread_detach(user_thread); + +} + +/** + * Starts protocol-specific handling on the given process by loading the client + * plugin for that protocol. This function does NOT return. It initializes the + * process with protocol-specific handlers and then runs until the guacd_proc's + * fd_socket is closed, adding any file descriptors received along fd_socket as + * new users. + * + * @param proc + * The process that any new users received along fd_socket should be added + * to (after the process has been initialized for the given protocol). + * + * @param protocol + * The protocol to initialize the given process for. + */ +static void guacd_exec_proc(guacd_proc* proc, const char* protocol) { + + /* Init client for selected protocol */ + if (guac_client_load_plugin(proc->client, protocol)) { + + /* Log error */ + if (guac_error == GUAC_STATUS_NOT_FOUND) + guacd_log(GUAC_LOG_WARNING, + "Support for protocol \"%s\" is not installed", protocol); + else + guacd_log_guac_error(GUAC_LOG_ERROR, + "Unable to load client plugin"); + + guac_client_free(proc->client); + close(proc->fd_socket); + free(proc); + exit(1); + } + + /* The first file descriptor is the owner */ + int owner = 1; + + /* Add each received file descriptor as a new user */ + int received_fd; + while ((received_fd = guacd_recv_fd(proc->fd_socket)) != -1) { + + guacd_proc_add_user(proc, received_fd, owner); + + /* Future file descriptors are not owners */ + owner = 0; + + } + + /* Stop and free client */ + guac_client_stop(proc->client); + guac_client_free(proc->client); + + /* Child is finished */ + close(proc->fd_socket); + free(proc); + exit(0); + +} + +guacd_proc* guacd_create_proc(const char* protocol) { + + int sockets[2]; + + /* Open UNIX socket pair */ + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets) < 0) { + guacd_log(GUAC_LOG_ERROR, "Error opening socket pair: %s", strerror(errno)); + return NULL; + } + + int parent_socket = sockets[0]; + int child_socket = sockets[1]; + + /* Allocate process */ + guacd_proc* proc = calloc(1, sizeof(guacd_proc)); + if (proc == NULL) { + close(parent_socket); + close(child_socket); + return NULL; + } + + /* Associate new client */ + proc->client = guac_client_alloc(); + if (proc->client == NULL) { + guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to create client"); + close(parent_socket); + close(child_socket); + free(proc); + return NULL; + } + + /* Init logging */ + proc->client->log_handler = guacd_client_log; + + /* Fork */ + proc->pid = fork(); + if (proc->pid < 0) { + guacd_log(GUAC_LOG_ERROR, "Cannot fork child process: %s", strerror(errno)); + close(parent_socket); + close(child_socket); + guac_client_free(proc->client); + free(proc); + return NULL; + } + + /* Child */ + else if (proc->pid == 0) { + + /* Communicate with parent */ + proc->fd_socket = parent_socket; + close(child_socket); + + /* Start protocol-specific handling */ + guacd_exec_proc(proc, protocol); + + } + + /* Parent */ + else { + + /* Communicate with child */ + proc->fd_socket = child_socket; + close(parent_socket); + + } + + return proc; + +} + +void guacd_proc_stop(guacd_proc* proc) { + + /* Signal client to stop */ + guac_client_stop(proc->client); + + /* Shutdown socket - in-progress recvmsg() will not fail otherwise */ + if (shutdown(proc->fd_socket, SHUT_RDWR) == -1) + guacd_log(GUAC_LOG_ERROR, "Unable to shutdown internal socket for " + "connection %s. Corresponding process may remain running but " + "inactive.", proc->client->connection_id); + + /* Clean up our end of the socket */ + close(proc->fd_socket); + +} + diff --git a/src/guacd/proc.h b/src/guacd/proc.h new file mode 100644 index 00000000..abbfe8e3 --- /dev/null +++ b/src/guacd/proc.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef GUACD_PROC_H +#define GUACD_PROC_H + +#include "config.h" + +#include +#include + +#include + +/** + * Process information of the internal remote desktop client. + */ +typedef struct guacd_proc { + + /** + * The process ID of the client. This will only be available to the + * parent process. The child process will see this as 0. + */ + pid_t pid; + + /** + * The file descriptor of the UNIX domain socket to use for sending and + * receiving file descriptors of new users. This parent will see this + * as the file descriptor for communicating with the child and vice + * versa. + */ + int fd_socket; + + /** + * The actual client instance. This will be visible to both child and + * parent process, but only the child will have a full guac_client + * instance, containing handlers from the plugin, etc. + * + * The parent process will receive a skeleton guac_client, containing only + * a proper connection_id and logging handlers. The actual + * protocol-specific handling will be absent. + */ + guac_client* client; + +} guacd_proc; + +/** + * Creates a new background process for handling the given protocol, returning + * a structure allowing communication with and monitoring of the process + * created. Within the child process, this function does not return - the + * entire child process simply terminates instead. + * + * @param protocol + * The protocol for which this process is client being created. + * + * @return + * A newly-allocated process structure pointing to the file descriptor of + * the background process specific to the specified protocol, or NULL of + * the process could not be created. + */ +guacd_proc* guacd_create_proc(const char* protocol); + +/** + * Signals the given process to stop accepting new users and clean up. This + * will eventually cause the child process to exit. + * + * @param proc + * The process to stop. + */ +void guacd_proc_stop(guacd_proc* proc); + +#endif + diff --git a/src/guacd/socket-ssl.c b/src/guacd/socket-ssl.c index 751286d8..921164d3 100644 --- a/src/guacd/socket-ssl.c +++ b/src/guacd/socket-ssl.c @@ -113,6 +113,9 @@ static int __guac_socket_ssl_free_handler(guac_socket* socket) { guac_socket_ssl_data* data = (guac_socket_ssl_data*) socket->data; SSL_shutdown(data->ssl); + /* Close file descriptor */ + close(data->fd); + free(data); return 0; } diff --git a/src/guacd/socket-ssl.h b/src/guacd/socket-ssl.h index 3ad0a4da..7af9c2f5 100644 --- a/src/guacd/socket-ssl.h +++ b/src/guacd/socket-ssl.h @@ -54,7 +54,19 @@ typedef struct guac_socket_ssl_data { } guac_socket_ssl_data; /** - * Creates a new guac_socket which will use SSL for all communication. + * Creates a new guac_socket which will use SSL for all communication. Freeing + * this guac_socket will automatically close the associated file descriptor. + * + * @param context + * The SSL_CTX structure describing the desired SSL configuration. + * + * @param fd + * The file descriptor to use for the SSL connection underlying the + * created guac_socket. + * + * @return + * A newly-allocated guac_socket which will transparently use SSL for + * all communication. */ guac_socket* guac_socket_open_secure(SSL_CTX* context, int fd); diff --git a/src/guacd/user.c b/src/guacd/user.c new file mode 100644 index 00000000..0609831c --- /dev/null +++ b/src/guacd/user.c @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" + +#include "log.h" +#include "user.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +void* guacd_user_input_thread(void* data) { + + guacd_user_input_thread_params* params = (guacd_user_input_thread_params*) data; + guac_user* user = params->user; + guac_parser* parser = params->parser; + guac_client* client = user->client; + guac_socket* socket = user->socket; + + /* Guacamole user input loop */ + while (client->state == GUAC_CLIENT_RUNNING && user->active) { + + /* Read instruction, stop on error */ + if (guac_parser_read(parser, socket, GUACD_USEC_TIMEOUT)) { + + if (guac_error == GUAC_STATUS_TIMEOUT) + guac_user_abort(user, GUAC_PROTOCOL_STATUS_CLIENT_TIMEOUT, "User is not responding."); + + else { + if (guac_error != GUAC_STATUS_CLOSED) + guacd_client_log_guac_error(client, GUAC_LOG_WARNING, + "Guacamole connection failure"); + guac_user_stop(user); + } + + return NULL; + } + + /* Reset guac_error and guac_error_message (user/client handlers are not + * guaranteed to set these) */ + guac_error = GUAC_STATUS_SUCCESS; + guac_error_message = NULL; + + /* Call handler, stop on error */ + if (guac_user_handle_instruction(user, parser->opcode, parser->argc, parser->argv) < 0) { + + /* Log error */ + guacd_client_log_guac_error(client, GUAC_LOG_WARNING, + "User connection aborted"); + + /* Log handler details */ + guac_user_log(user, GUAC_LOG_DEBUG, "Failing instruction handler in user was \"%s\"", parser->opcode); + + guac_user_stop(user); + return NULL; + } + + } + + return NULL; + +} + +int guacd_user_start(guac_parser* parser, guac_user* user) { + + guacd_user_input_thread_params params = { + .parser = parser, + .user = user + }; + + pthread_t input_thread; + + if (pthread_create(&input_thread, NULL, guacd_user_input_thread, (void*) ¶ms)) { + guac_user_log(user, GUAC_LOG_ERROR, "Unable to start input thread"); + guac_user_stop(user); + return -1; + } + + /* Wait for I/O threads */ + pthread_join(input_thread, NULL); + + /* Done */ + return 0; + +} + diff --git a/src/guacd/client.h b/src/guacd/user.h similarity index 54% rename from src/guacd/client.h rename to src/guacd/user.h index 214af2a0..2caa32af 100644 --- a/src/guacd/client.h +++ b/src/guacd/user.h @@ -21,38 +21,14 @@ */ -#ifndef _GUACD_CLIENT_H -#define _GUACD_CLIENT_H +#ifndef _GUACD_USER_H +#define _GUACD_USER_H #include "config.h" -#include - -/** - * The time to allow between sync responses in milliseconds. If a sync - * instruction is sent to the client and no response is received within this - * timeframe, server messages will not be handled until a sync instruction is - * received from the client. - */ -#define GUACD_SYNC_THRESHOLD 500 - -/** - * The time to allow between server sync messages in milliseconds. A sync - * message from the server will be sent every GUACD_SYNC_FREQUENCY milliseconds. - * As this will induce a response from a client that is not malfunctioning, - * this is used to detect when a client has died. This must be set to a - * reasonable value to avoid clients being disconnected unnecessarily due - * to timeout. - */ -#define GUACD_SYNC_FREQUENCY 5000 - -/** - * The amount of time to wait after handling server messages. If a client - * plugin has a message handler, and sends instructions when server messages - * are being handled, there will be a pause of this many milliseconds before - * the next call to the message handler. - */ -#define GUACD_MESSAGE_HANDLE_FREQUENCY 50 +#include +#include +#include /** * The number of milliseconds to wait for messages in any phase before @@ -73,7 +49,53 @@ */ #define GUACD_CLIENT_MAX_CONNECTIONS 65536 -int guacd_client_start(guac_client* client); +/** + * Parameters required by the user input thread. + */ +typedef struct guacd_user_input_thread_params { + + /** + * The parser which will be used throughout the user's session. + */ + guac_parser* parser; + + /** + * A reference to the connected user. + */ + guac_user* user; + +} guacd_user_input_thread_params; + +/** + * Starts the input/output threads of a new user. This function will block + * until the user disconnects. If an error prevents the input/output threads + * from starting, guac_user_stop() will be invoked on the given user. + * + * @param parser + * The guac_parser to use to handle all input from the given user. + * + * @param user + * The user whose associated I/O transfer threads should be started. + * + * @return + * Zero if the I/O threads started successfully and user has disconnected, + * or non-zero if the I/O threads could not be started. + */ +int guacd_user_start(guac_parser* parser, guac_user* user); + +/** + * The thread which handles all user input, calling event handlers for received + * instructions. + * + * @param data + * A pointer to a guacd_user_input_thread_params structure describing the + * user whose input is being handled and the guac_parser with which to + * handle it. + * + * @return + * Always NULL. + */ +void* guacd_user_input_thread(void* data); #endif