401 lines
11 KiB
C
401 lines
11 KiB
C
/*
|
|
* 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 <guacamole/client.h>
|
|
#include <guacamole/error.h>
|
|
#include <guacamole/parser.h>
|
|
#include <guacamole/plugin.h>
|
|
#include <guacamole/protocol.h>
|
|
#include <guacamole/socket.h>
|
|
#include <guacamole/user.h>
|
|
|
|
#ifdef ENABLE_SSL
|
|
#include <openssl/ssl.h>
|
|
#include <guacamole/socket-ssl.h>
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/wait.h>
|
|
|
|
/**
|
|
* 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;
|
|
|
|
}
|
|
|