/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #include "config.h" #include "connection.h" #include "log.h" #include "move-fd.h" #include "proc.h" #include "proc-map.h" #include #include #include #include #include #include #include #ifdef ENABLE_SSL #include #include #endif #include #include #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, * parser, 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); new_process = 0; /* Warn if requested connection does not exist */ 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); } /* Otherwise, create new client */ else { guacd_log(GUAC_LOG_INFO, "Creating new client for protocol \"%s\"", identifier); /* Create new process */ proc = guacd_create_proc(identifier); new_process = 1; } /* Abort if no process exists for the requested connection */ 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 */ int add_user_failed = guacd_add_user(proc, parser, socket); /* If new process was created, manage that process */ if (new_process) { /* The new process will only be active if the user was added */ if (!add_user_failed) { /* 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); } /* Parser must be manually freed if the process did not start */ else guac_parser_free(parser); /* Force process to stop and clean up */ guacd_proc_stop(proc); /* Free skeleton client */ guac_client_free(proc->client); /* Clean up */ close(proc->fd_socket); free(proc); } /* Routing succeeded only if the user was added to a process */ return add_user_failed; } 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"); close(connected_socket_fd); 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; }