cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

proc.c (16332B)


      1/*
      2 * Licensed to the Apache Software Foundation (ASF) under one
      3 * or more contributor license agreements.  See the NOTICE file
      4 * distributed with this work for additional information
      5 * regarding copyright ownership.  The ASF licenses this file
      6 * to you under the Apache License, Version 2.0 (the
      7 * "License"); you may not use this file except in compliance
      8 * with the License.  You may obtain a copy of the License at
      9 *
     10 *   http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing,
     13 * software distributed under the License is distributed on an
     14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     15 * KIND, either express or implied.  See the License for the
     16 * specific language governing permissions and limitations
     17 * under the License.
     18 */
     19
     20#include "config.h"
     21
     22#include "log.h"
     23#include "move-fd.h"
     24#include "proc.h"
     25#include "proc-map.h"
     26
     27#include <guacamole/client.h>
     28#include <guacamole/error.h>
     29#include <guacamole/mem.h>
     30#include <guacamole/parser.h>
     31#include <guacamole/plugin.h>
     32#include <guacamole/protocol.h>
     33#include <guacamole/socket.h>
     34#include <guacamole/user.h>
     35
     36#include <errno.h>
     37#include <pthread.h>
     38#include <signal.h>
     39#include <stdlib.h>
     40#include <string.h>
     41#include <unistd.h>
     42#include <sys/time.h>
     43#include <sys/types.h>
     44#include <sys/socket.h>
     45#include <sys/wait.h>
     46
     47/**
     48 * Parameters for the user thread.
     49 */
     50typedef struct guacd_user_thread_params {
     51
     52    /**
     53     * The process being joined.
     54     */
     55    guacd_proc* proc;
     56
     57    /**
     58     * The file descriptor of the joining user's socket.
     59     */
     60    int fd;
     61
     62    /**
     63     * Whether the joining user is the connection owner.
     64     */
     65    int owner;
     66
     67} guacd_user_thread_params;
     68
     69/**
     70 * Handles a user's entire connection and socket lifecycle.
     71 *
     72 * @param data
     73 *     A pointer to a guacd_user_thread_params structure describing the user's
     74 *     associated file descriptor, whether that user is the connection owner
     75 *     (the first person to join), as well as the process associated with the
     76 *     connection being joined.
     77 *
     78 * @return
     79 *     Always NULL.
     80 */
     81static void* guacd_user_thread(void* data) {
     82
     83    guacd_user_thread_params* params = (guacd_user_thread_params*) data;
     84    guacd_proc* proc = params->proc;
     85    guac_client* client = proc->client;
     86
     87    /* Get guac_socket for user's file descriptor */
     88    guac_socket* socket = guac_socket_open(params->fd);
     89    if (socket == NULL)
     90        return NULL;
     91
     92    /* Create skeleton user */
     93    guac_user* user = guac_user_alloc();
     94    user->socket = socket;
     95    user->client = client;
     96    user->owner  = params->owner;
     97
     98    /* Handle user connection from handshake until disconnect/completion */
     99    guac_user_handle_connection(user, GUACD_USEC_TIMEOUT);
    100
    101    /* Stop client and prevent future users if all users are disconnected */
    102    if (client->connected_users == 0) {
    103        guacd_log(GUAC_LOG_INFO, "Last user of connection \"%s\" disconnected", client->connection_id);
    104        guacd_proc_stop(proc);
    105    }
    106
    107    /* Clean up */
    108    guac_socket_free(socket);
    109    guac_user_free(user);
    110    guac_mem_free(params);
    111
    112    return NULL;
    113
    114}
    115
    116/**
    117 * Begins a new user connection under a given process, using the given file
    118 * descriptor. The connection will be managed by a separate and detached thread
    119 * which is started by this function.
    120 *
    121 * @param proc
    122 *     The process that the user is being added to.
    123 *
    124 * @param fd
    125 *     The file descriptor associated with the user's network connection to
    126 *     guacd.
    127 *
    128 * @param owner
    129 *     Non-zero if the user is the owner of the connection being joined (they
    130 *     are the first user to join), or zero otherwise.
    131 */
    132static void guacd_proc_add_user(guacd_proc* proc, int fd, int owner) {
    133
    134    guacd_user_thread_params* params = guac_mem_alloc(sizeof(guacd_user_thread_params));
    135    params->proc = proc;
    136    params->fd = fd;
    137    params->owner = owner;
    138
    139    /* Start user thread */
    140    pthread_t user_thread;
    141    pthread_create(&user_thread, NULL, guacd_user_thread, params);
    142    pthread_detach(user_thread);
    143
    144}
    145
    146/**
    147 * Forcibly kills all processes within the current process group, including the
    148 * current process and all child processes. This function is only safe to call
    149 * if the process group ID has been correctly set. Calling this function within
    150 * a process which does not have a PGID separate from the main guacd process
    151 * can result in guacd itself being terminated.
    152 */
    153static void guacd_kill_current_proc_group() {
    154
    155    /* Forcibly kill all children within process group */
    156    if (kill(0, SIGKILL))
    157        guacd_log(GUAC_LOG_WARNING, "Unable to forcibly terminate "
    158                "client process: %s ", strerror(errno));
    159
    160}
    161
    162/**
    163 * The current status of a background attempt to free a guac_client instance.
    164 */
    165typedef struct guacd_client_free {
    166
    167    /**
    168     * The guac_client instance being freed.
    169     */
    170    guac_client* client;
    171
    172    /**
    173     * The condition which is signalled whenever changes are made to the
    174     * completed flag. The completed flag only changes from zero (not yet
    175     * freed) to non-zero (successfully freed).
    176     */
    177    pthread_cond_t completed_cond;
    178
    179    /**
    180     * Mutex which must be acquired before any changes are made to the
    181     * completed flag.
    182     */
    183    pthread_mutex_t completed_mutex;
    184
    185    /**
    186     * Whether the guac_client has been successfully freed. Initially, this
    187     * will be zero, indicating that the free operation has not yet been
    188     * attempted. If the client is eventually successfully freed, this will be
    189     * set to a non-zero value. Changes to this flag are signalled through
    190     * the completed_cond condition.
    191     */
    192    int completed;
    193
    194} guacd_client_free;
    195
    196/**
    197 * Thread which frees a given guac_client instance in the background. If the
    198 * free operation succeeds, a flag is set on the provided structure, and the
    199 * change in that flag is signalled with a pthread condition.
    200 *
    201 * At the time this function is provided to a pthread_create() call, the
    202 * completed flag of the associated guacd_client_free structure MUST be
    203 * initialized to zero, the pthread mutex and condition MUST both be
    204 * initialized, and the client pointer must point to the guac_client being
    205 * freed.
    206 *
    207 * @param data
    208 *     A pointer to a guacd_client_free structure describing the free
    209 *     operation.
    210 *
    211 * @return
    212 *     Always NULL.
    213 */
    214static void* guacd_client_free_thread(void* data) {
    215
    216    guacd_client_free* free_operation = (guacd_client_free*) data;
    217
    218    /* Attempt to free client (this may never return if the client is
    219     * malfunctioning) */
    220    guac_client_free(free_operation->client);
    221
    222    /* Signal that the client was successfully freed */
    223    pthread_mutex_lock(&free_operation->completed_mutex);
    224    free_operation->completed = 1;
    225    pthread_cond_broadcast(&free_operation->completed_cond);
    226    pthread_mutex_unlock(&free_operation->completed_mutex);
    227
    228    return NULL;
    229
    230}
    231
    232/**
    233 * Attempts to free the given guac_client, restricting the time taken by the
    234 * free handler of the guac_client to a finite number of seconds. If the free
    235 * handler does not complete within the time alotted, this function returns
    236 * and the intended free operation is left in an undefined state.
    237 *
    238 * @param client
    239 *     The guac_client instance to free.
    240 *
    241 * @param timeout
    242 *     The maximum amount of time to wait for the guac_client to be freed,
    243 *     in seconds.
    244 *
    245 * @return
    246 *     Zero if the guac_client was successfully freed within the time alotted,
    247 *     non-zero otherwise.
    248 */
    249static int guacd_timed_client_free(guac_client* client, int timeout) {
    250
    251    pthread_t client_free_thread;
    252
    253    guacd_client_free free_operation = {
    254        .client = client,
    255        .completed_cond = PTHREAD_COND_INITIALIZER,
    256        .completed_mutex = PTHREAD_MUTEX_INITIALIZER,
    257        .completed = 0
    258    };
    259
    260    /* Get current time */
    261    struct timeval current_time;
    262    if (gettimeofday(&current_time, NULL))
    263        return 1;
    264
    265    /* Calculate exact time that the free operation MUST complete by */
    266    struct timespec deadline = {
    267        .tv_sec  = current_time.tv_sec + timeout,
    268        .tv_nsec = current_time.tv_usec * 1000
    269    };
    270
    271    /* The mutex associated with the pthread conditional and flag MUST be
    272     * acquired before attempting to wait for the condition */
    273    if (pthread_mutex_lock(&free_operation.completed_mutex))
    274        return 1;
    275
    276    /* Free the client in a separate thread, so we can time the free operation */
    277    if (!pthread_create(&client_free_thread, NULL,
    278                guacd_client_free_thread, &free_operation)) {
    279
    280        /* Wait a finite amount of time for the free operation to finish */
    281        (void) pthread_cond_timedwait(&free_operation.completed_cond,
    282                    &free_operation.completed_mutex, &deadline);
    283    }
    284
    285    (void) pthread_mutex_unlock(&free_operation.completed_mutex);
    286
    287    /* Return status of free operation */
    288    return !free_operation.completed;
    289}
    290
    291/**
    292 * A reference to the current guacd process.
    293 */
    294guacd_proc* guacd_proc_self = NULL;
    295
    296/**
    297 * A signal handler that will be invoked when a signal is caught telling this
    298 * guacd process to immediately exit.
    299 *
    300 * @param signal
    301 *     The signal that was received. Unused in this function since only
    302 *     signals that should result in stopping the proc should invoke this.
    303 */
    304static void signal_stop_handler(int signal) {
    305
    306    /* Stop the current guacd proc */
    307    guacd_proc_stop(guacd_proc_self);
    308
    309}
    310
    311/**
    312 * Starts protocol-specific handling on the given process by loading the client
    313 * plugin for that protocol. This function does NOT return. It initializes the
    314 * process with protocol-specific handlers and then runs until the guacd_proc's
    315 * fd_socket is closed, adding any file descriptors received along fd_socket as
    316 * new users.
    317 *
    318 * @param proc
    319 *     The process that any new users received along fd_socket should be added
    320 *     to (after the process has been initialized for the given protocol).
    321 *
    322 * @param protocol
    323 *     The protocol to initialize the given process for.
    324 */
    325static void guacd_exec_proc(guacd_proc* proc, const char* protocol) {
    326
    327    int result = 1;
    328   
    329    /* Set process group ID to match PID */ 
    330    if (setpgid(0, 0)) {
    331        guacd_log(GUAC_LOG_ERROR, "Cannot set PGID for connection process: %s",
    332                strerror(errno));
    333        goto cleanup_process;
    334    }
    335
    336    /* Init client for selected protocol */
    337    guac_client* client = proc->client;
    338    if (guac_client_load_plugin(client, protocol)) {
    339
    340        /* Log error */
    341        if (guac_error == GUAC_STATUS_NOT_FOUND)
    342            guacd_log(GUAC_LOG_WARNING,
    343                    "Support for protocol \"%s\" is not installed", protocol);
    344        else
    345            guacd_log_guac_error(GUAC_LOG_ERROR,
    346                    "Unable to load client plugin");
    347
    348        goto cleanup_client;
    349    }
    350
    351    /* The first file descriptor is the owner */
    352    int owner = 1;
    353
    354    /* Enable keep alive on the broadcast socket */
    355    guac_socket_require_keep_alive(client->socket);
    356
    357    guacd_proc_self = proc;
    358
    359    /* Clean up and exit if SIGINT or SIGTERM signals are caught */
    360    struct sigaction signal_stop_action = { .sa_handler = signal_stop_handler };
    361    sigaction(SIGINT, &signal_stop_action, NULL);
    362    sigaction(SIGTERM, &signal_stop_action, NULL);
    363
    364    /* Add each received file descriptor as a new user */
    365    int received_fd;
    366    while ((received_fd = guacd_recv_fd(proc->fd_socket)) != -1) {
    367
    368        guacd_proc_add_user(proc, received_fd, owner);
    369
    370        /* Future file descriptors are not owners */
    371        owner = 0;
    372
    373    }
    374    
    375cleanup_client:
    376
    377    /* Request client to stop/disconnect */
    378    guac_client_stop(client);
    379
    380    /* Attempt to free client cleanly */
    381    guacd_log(GUAC_LOG_DEBUG, "Requesting termination of client...");
    382    result = guacd_timed_client_free(client, GUACD_CLIENT_FREE_TIMEOUT);
    383
    384    /* If client was unable to be freed, warn and forcibly kill */
    385    if (result) {
    386        guacd_log(GUAC_LOG_WARNING, "Client did not terminate in a timely "
    387                "manner. Forcibly terminating client and any child "
    388                "processes.");
    389        guacd_kill_current_proc_group();
    390    }
    391    else
    392        guacd_log(GUAC_LOG_DEBUG, "Client terminated successfully.");
    393
    394    /* Verify whether children were all properly reaped */
    395    pid_t child_pid;
    396    while ((child_pid = waitpid(0, NULL, WNOHANG)) > 0) {
    397        guacd_log(GUAC_LOG_DEBUG, "Automatically reaped unreaped "
    398                "(zombie) child process with PID %i.", child_pid);
    399    }
    400
    401    /* If running children remain, warn and forcibly kill */
    402    if (child_pid == 0) {
    403        guacd_log(GUAC_LOG_WARNING, "Client reported successful termination, "
    404                "but child processes remain. Forcibly terminating client and "
    405                "child processes.");
    406        guacd_kill_current_proc_group();
    407    }
    408
    409cleanup_process:
    410
    411    /* Free up all internal resources outside the client */
    412    close(proc->fd_socket);
    413    guac_mem_free(proc);
    414
    415    exit(result);
    416
    417}
    418
    419guacd_proc* guacd_create_proc(const char* protocol) {
    420
    421    int sockets[2];
    422
    423    /* Open UNIX socket pair */
    424    if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets) < 0) {
    425        guacd_log(GUAC_LOG_ERROR, "Error opening socket pair: %s", strerror(errno));
    426        return NULL;
    427    }
    428
    429    int parent_socket = sockets[0];
    430    int child_socket = sockets[1];
    431
    432    /* Allocate process */
    433    guacd_proc* proc = guac_mem_zalloc(sizeof(guacd_proc));
    434    if (proc == NULL) {
    435        close(parent_socket);
    436        close(child_socket);
    437        return NULL;
    438    }
    439
    440    /* Associate new client */
    441    proc->client = guac_client_alloc();
    442    if (proc->client == NULL) {
    443        guacd_log_guac_error(GUAC_LOG_ERROR, "Unable to create client");
    444        close(parent_socket);
    445        close(child_socket);
    446        guac_mem_free(proc);
    447        return NULL;
    448    }
    449
    450    /* Init logging */
    451    proc->client->log_handler = guacd_client_log;
    452
    453    /* Fork */
    454    proc->pid = fork();
    455    if (proc->pid < 0) {
    456        guacd_log(GUAC_LOG_ERROR, "Cannot fork child process: %s", strerror(errno));
    457        close(parent_socket);
    458        close(child_socket);
    459        guac_client_free(proc->client);
    460        guac_mem_free(proc);
    461        return NULL;
    462    }
    463
    464    /* Child */
    465    else if (proc->pid == 0) {
    466
    467        /* Communicate with parent */
    468        proc->fd_socket = parent_socket;
    469        close(child_socket);
    470
    471        /* Start protocol-specific handling */
    472        guacd_exec_proc(proc, protocol);
    473
    474    }
    475
    476    /* Parent */
    477    else {
    478
    479        /* Communicate with child */
    480        proc->fd_socket = child_socket;
    481        close(parent_socket);
    482
    483    }
    484
    485    return proc;
    486
    487}
    488
    489/**
    490 * Kill the provided child guacd process. This function must be called by the
    491 * parent process, and will block until all processes associated with the
    492 * child process have terminated.
    493 *
    494 * @param proc
    495 *     The child guacd process to kill.
    496 */
    497static void guacd_proc_kill(guacd_proc* proc) {
    498
    499    /* Request orderly termination of process */
    500    if (kill(proc->pid, SIGTERM))
    501        guacd_log(GUAC_LOG_DEBUG, "Unable to request termination of "
    502                "client process: %s ", strerror(errno));
    503
    504    /* Wait for all processes within process group to terminate */
    505    pid_t child_pid;
    506    while ((child_pid = waitpid(-proc->pid, NULL, 0)) > 0 || errno == EINTR) {
    507        guacd_log(GUAC_LOG_DEBUG, "Child process %i of connection \"%s\" has terminated",
    508            child_pid, proc->client->connection_id);
    509    }
    510
    511    guacd_log(GUAC_LOG_DEBUG, "All child processes for connection \"%s\" have been terminated.",
    512        proc->client->connection_id);
    513
    514}
    515
    516void guacd_proc_stop(guacd_proc* proc) {
    517
    518    /* A non-zero PID means that this is the parent process */
    519    if (proc->pid != 0) {
    520        guacd_proc_kill(proc);
    521        return;
    522    }
    523
    524    /* Otherwise, this is the child process */
    525
    526    /* Signal client to stop */
    527    guac_client_stop(proc->client);
    528
    529    /* Shutdown socket - in-progress recvmsg() will not fail otherwise */
    530    if (shutdown(proc->fd_socket, SHUT_RDWR) == -1)
    531        guacd_log(GUAC_LOG_ERROR, "Unable to shutdown internal socket for "
    532                "connection %s. Corresponding process may remain running but "
    533                "inactive.", proc->client->connection_id);
    534
    535    /* Clean up our end of the socket */
    536    close(proc->fd_socket);
    537
    538}