cscg24-guacamole

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

print-job.c (20576B)


      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 "print-job.h"
     21#include "rdp.h"
     22
     23#include <guacamole/client.h>
     24#include <guacamole/mem.h>
     25#include <guacamole/protocol.h>
     26#include <guacamole/socket.h>
     27#include <guacamole/stream.h>
     28#include <guacamole/user.h>
     29
     30#include <errno.h>
     31#include <pthread.h>
     32#include <signal.h>
     33#include <stdlib.h>
     34#include <string.h>
     35#include <unistd.h>
     36
     37/**
     38 * The command to run when filtering postscript to produce PDF. This must be
     39 * a NULL-terminated array of arguments, where the first argument is the name
     40 * of the file to run.
     41 */
     42char* const guac_rdp_pdf_filter_command[] = {
     43    "gs",
     44    "-q",
     45    "-dNOPAUSE",
     46    "-dBATCH",
     47    "-dSAFER",
     48    "-dPARANOIDSAFER",
     49    "-sDEVICE=pdfwrite",
     50    "-sOutputFile=-",
     51    "-sstdout=/dev/null",
     52    "-f",
     53    "-",
     54    NULL
     55};
     56
     57/**
     58 * Updates the state of the given print job. Any threads currently blocked by a
     59 * call to guac_rdp_print_job_wait_for_ack() will be unblocked.
     60 *
     61 * @param job
     62 *     The print job whose state should be updated.
     63 *
     64 * @param state
     65 *     The new state to assign to the given print job.
     66 */
     67static void guac_rdp_print_job_set_state(guac_rdp_print_job* job,
     68        guac_rdp_print_job_state state) {
     69
     70    pthread_mutex_lock(&(job->state_lock));
     71
     72    /* Update stream state, signalling modification */
     73    job->state = state;
     74    pthread_cond_signal(&(job->state_modified));
     75
     76    pthread_mutex_unlock(&(job->state_lock));
     77
     78}
     79
     80/**
     81 * Suspends execution of the current thread until the state of the given print
     82 * job is not GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK. If the state of the print
     83 * job is GUAC_RDP_PRINT_JOB_ACK_RECEIVED, the state is automatically reset
     84 * back to GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK prior to returning.
     85 *
     86 * @param job
     87 *     The print job to wait for.
     88 *
     89 * @return
     90 *     Zero if the state of the print job is GUAC_RDP_PRINT_JOB_CLOSED,
     91 *     non-zero if the state was GUAC_RDP_PRINT_JOB_ACK_RECEIVED and has been
     92 *     automatically reset to GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK.
     93 */
     94static int guac_rdp_print_job_wait_for_ack(guac_rdp_print_job* job) {
     95
     96    /* Wait for ack if stream open and not yet received */
     97    pthread_mutex_lock(&(job->state_lock));
     98    if (job->state == GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK)
     99        pthread_cond_wait(&job->state_modified, &job->state_lock);
    100
    101    /* Reset state if ack received */
    102    int got_ack = (job->state == GUAC_RDP_PRINT_JOB_ACK_RECEIVED);
    103    if (got_ack)
    104        job->state = GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK;
    105
    106    /* Return whether ack was successfully received */
    107    pthread_mutex_unlock(&(job->state_lock));
    108    return got_ack;
    109
    110}
    111
    112/**
    113 * Sends a "file" instruction to the given user describing the PDF file that
    114 * will be sent using the output of the given print job. If the given user no
    115 * longer exists, the print stream will be automatically terminated.
    116 *
    117 * @param user
    118 *     The user receiving the "file" instruction.
    119 *
    120 * @param data
    121 *     A pointer to the guac_rdp_print_job representing the print job being
    122 *     streamed.
    123 *
    124 * @return
    125 *     Always NULL.
    126 */
    127static void* guac_rdp_print_job_begin_stream(guac_user* user, void* data) {
    128
    129    guac_rdp_print_job* job = (guac_rdp_print_job*) data;
    130    guac_client_log(job->client, GUAC_LOG_DEBUG, "Beginning print stream: %s",
    131            job->filename);
    132
    133    /* Kill job and do nothing if user no longer exists */
    134    if (user == NULL) {
    135        guac_rdp_print_job_kill(job);
    136        return NULL;
    137    }
    138
    139    /* Send document as a PDF file stream */
    140    guac_protocol_send_file(user->socket, job->stream,
    141            "application/pdf", job->filename);
    142
    143    guac_socket_flush(user->socket);
    144    return NULL;
    145
    146}
    147
    148/**
    149 * Sends a "blob" instruction to the given user containing the provided data
    150 * along the stream associated with the provided print job. If the given user
    151 * no longer exists, the print stream will be automatically terminated.
    152 *
    153 * @param user
    154 *     The user receiving the "blob" instruction.
    155 *
    156 * @param data
    157 *     A pointer to an guac_rdp_print_blob structure containing the data to
    158 *     be written, the number of bytes being written, and the print job being
    159 *     streamed.
    160 *
    161 * @return
    162 *     Always NULL.
    163 */
    164static void* guac_rdp_print_job_send_blob(guac_user* user, void* data) {
    165
    166    guac_rdp_print_blob* blob = (guac_rdp_print_blob*) data;
    167    guac_rdp_print_job* job = blob->job;
    168
    169    guac_client_log(job->client, GUAC_LOG_DEBUG, "Sending %i byte(s) "
    170            "of filtered output.", blob->length);
    171
    172    /* Kill job and do nothing if user no longer exists */
    173    if (user == NULL) {
    174        guac_rdp_print_job_kill(job);
    175        return NULL;
    176    }
    177
    178    /* Send single blob of print data */
    179    guac_protocol_send_blob(user->socket, job->stream,
    180            blob->buffer, blob->length);
    181
    182    guac_socket_flush(user->socket);
    183    return NULL;
    184
    185}
    186
    187/**
    188 * Sends an "end" instruction to the given user, closing the stream associated
    189 * with the given print job. If the given user no longer exists, the print
    190 * stream will be automatically terminated.
    191 *
    192 * @param user
    193 *     The user receiving the "end" instruction.
    194 *
    195 * @param data
    196 *     A pointer to the guac_rdp_print_job representing the print job being
    197 *     streamed.
    198 *
    199 * @return
    200 *     Always NULL.
    201 */
    202static void* guac_rdp_print_job_end_stream(guac_user* user, void* data) {
    203
    204    guac_rdp_print_job* job = (guac_rdp_print_job*) data;
    205    guac_client_log(job->client, GUAC_LOG_DEBUG, "End of print stream.");
    206
    207    /* Kill job and do nothing if user no longer exists */
    208    if (user == NULL) {
    209        guac_rdp_print_job_kill(job);
    210        return NULL;
    211    }
    212
    213    /* Explicitly close down stream */
    214    guac_protocol_send_end(user->socket, job->stream);
    215    guac_socket_flush(user->socket);
    216
    217    /* Clean up our end of the stream */
    218    guac_user_free_stream(job->user, job->stream);
    219
    220    return NULL;
    221
    222}
    223
    224/**
    225 * Handler for "ack" messages received in response to printed data. Additional
    226 * data will be sent as a result or, if no data remains, the stream will be
    227 * terminated. It is required that the data pointer of the provided stream be
    228 * set to the file descriptor from which the printed data should be read.
    229 *
    230 * @param user
    231 *     The user to whom the printed data is being sent.
    232 *
    233 * @param stream
    234 *     The stream along which the printed data is to be sent. The data pointer
    235 *     of this stream MUST be set to the file descriptor from which the data
    236 *     being sent is to be read.
    237 *
    238 * @param message
    239 *     An arbitrary, human-readable message describing the success/failure of
    240 *     the operation being acknowledged (either stream creation or receipt of
    241 *     a blob).
    242 *
    243 * @param status
    244 *     The status code describing the success/failure of the operation being
    245 *     acknowledged (either stream creation or receipt of a blob).
    246 *
    247 * @return
    248 *     Always zero.
    249 */
    250static int guac_rdp_print_filter_ack_handler(guac_user* user,
    251        guac_stream* stream, char* message, guac_protocol_status status) {
    252
    253    guac_rdp_print_job* job = (guac_rdp_print_job*) stream->data;
    254
    255    /* Update state for successful acks */
    256    if (status == GUAC_PROTOCOL_STATUS_SUCCESS)
    257        guac_rdp_print_job_set_state(job, GUAC_RDP_PRINT_JOB_ACK_RECEIVED);
    258
    259    /* Terminate stream if ack signals an error */
    260    else {
    261
    262        /* Note that the stream was aborted by the user */
    263        guac_client_log(job->client, GUAC_LOG_INFO, "User explicitly aborted "
    264                "print stream.");
    265
    266        /* Kill job (the results will no longer be received) */
    267        guac_rdp_print_job_kill(job);
    268
    269    }
    270
    271    return 0;
    272
    273}
    274
    275/**
    276 * Forks a new print filtering process which accepts PostScript input and
    277 * produces PDF output. File descriptors for writing input and reading output
    278 * will automatically be allocated and must be manually closed when processing
    279 * is complete.
    280 *
    281 * @param client
    282 *     The guac_client associated with the print job for which this filter
    283 *     process is being created.
    284 *
    285 * @param input_fd
    286 *     A pointer to an int which should receive the input file descriptor of
    287 *     the filter process. PostScript input for the filter process should be
    288 *     written to this file descriptor.
    289 *
    290 * @param output_fd
    291 *     A pointer to an int which should receive the output file descriptor of
    292 *     the filter process. PDF output from the filter process must be
    293 *     continuously read from this file descriptor or the pipeline may block.
    294 *
    295 * @return
    296 *     The PID of the filter process, or -1 if the filter process could not be
    297 *     created. If the filter process could not be created, the values assigned
    298 *     through input_fd and output_fd are undefined.
    299 */
    300static pid_t guac_rdp_create_filter_process(guac_client* client,
    301        int* input_fd, int* output_fd) {
    302
    303    int child_pid;
    304    int stdin_pipe[2];
    305    int stdout_pipe[2];
    306
    307    /* Create STDIN pipe */
    308    if (pipe(stdin_pipe)) {
    309        guac_client_log(client, GUAC_LOG_ERROR, "Unable to create STDIN "
    310                "pipe for PDF filter process: %s", strerror(errno));
    311        return -1;
    312    }
    313
    314    /* Create STDOUT pipe */
    315    if (pipe(stdout_pipe)) {
    316        guac_client_log(client, GUAC_LOG_ERROR, "Unable to create STDOUT "
    317                "pipe for PDF filter process: %s", strerror(errno));
    318        close(stdin_pipe[0]);
    319        close(stdin_pipe[1]);
    320        return -1;
    321    }
    322
    323    /* Store parent side of stdin/stdout */
    324    *input_fd = stdin_pipe[1];
    325    *output_fd = stdout_pipe[0];
    326
    327    /* Fork child process */
    328    child_pid = fork();
    329
    330    /* Log fork errors */
    331    if (child_pid == -1) {
    332        guac_client_log(client, GUAC_LOG_ERROR, "Unable to fork PDF filter "
    333                "process: %s", strerror(errno));
    334        close(stdin_pipe[0]);
    335        close(stdin_pipe[1]);
    336        close(stdout_pipe[0]);
    337        close(stdout_pipe[1]);
    338        return -1;
    339    }
    340
    341    /* Child process */
    342    if (child_pid == 0) {
    343
    344        /* Close unneeded ends of pipe */
    345        close(stdin_pipe[1]);
    346        close(stdout_pipe[0]);
    347
    348        /* Reassign file descriptors as STDIN/STDOUT */
    349        dup2(stdin_pipe[0], STDIN_FILENO);
    350        dup2(stdout_pipe[1], STDOUT_FILENO);
    351
    352        /* Run PDF filter */
    353        guac_client_log(client, GUAC_LOG_INFO, "Running %s",
    354                guac_rdp_pdf_filter_command[0]);
    355        if (execvp(guac_rdp_pdf_filter_command[0],
    356                    guac_rdp_pdf_filter_command) < 0)
    357            guac_client_log(client, GUAC_LOG_ERROR, "Unable to execute PDF "
    358                    "filter command: %s", strerror(errno));
    359        else
    360            guac_client_log(client, GUAC_LOG_ERROR, "Unable to execute PDF "
    361                    "filter command, but no error given");
    362
    363        /* Terminate child process */
    364        exit(1);
    365
    366    }
    367
    368    /* Log fork success */
    369    guac_client_log(client, GUAC_LOG_INFO, "Created PDF filter process "
    370            "PID=%i", child_pid);
    371
    372    /* Close unneeded ends of pipe */
    373    close(stdin_pipe[0]);
    374    close(stdout_pipe[1]);
    375    return child_pid;
    376
    377}
    378
    379/**
    380 * Thread which continuously reads from the output file descriptor associated
    381 * with the given print job, writing filtered PDF output to the associated
    382 * Guacamole stream, and terminating only after the print job has completed
    383 * processing or the associated Guacamole stream has closed.
    384 *
    385 * @param data
    386 *     A pointer to the guac_rdp_print_job representing the print job that
    387 *     should be read.
    388 *
    389 * @return
    390 *     Always NULL.
    391 */
    392static void* guac_rdp_print_job_output_thread(void* data) {
    393
    394    int length;
    395    char buffer[6048];
    396
    397    guac_rdp_print_job* job = (guac_rdp_print_job*) data;
    398    guac_client_log(job->client, GUAC_LOG_DEBUG, "Reading output from filter "
    399            "process...");
    400
    401    /* Read continuously while data remains */
    402    while ((length = read(job->output_fd, buffer, sizeof(buffer))) > 0) {
    403
    404        /* Wait for client to be ready for blob */
    405        if (guac_rdp_print_job_wait_for_ack(job)) {
    406
    407            guac_rdp_print_blob blob = {
    408                .job    = job,
    409                .buffer = buffer,
    410                .length = length
    411            };
    412
    413            /* Write a single blob of output */
    414            guac_client_for_user(job->client, job->user,
    415                    guac_rdp_print_job_send_blob, &blob);
    416
    417        }
    418
    419        /* Abort if stream is closed */
    420        else {
    421            guac_client_log(job->client, GUAC_LOG_DEBUG, "Print stream "
    422                    "explicitly aborted.");
    423            break;
    424        }
    425
    426    }
    427
    428    /* Warn of read errors */
    429    if (length < 0)
    430        guac_client_log(job->client, GUAC_LOG_ERROR,
    431                "Error reading from filter: %s", strerror(errno));
    432
    433    /* Terminate stream */
    434    guac_client_for_user(job->client, job->user,
    435            guac_rdp_print_job_end_stream, job);
    436
    437    /* Ensure all associated file descriptors are closed */
    438    close(job->input_fd);
    439    close(job->output_fd);
    440
    441    guac_client_log(job->client, GUAC_LOG_DEBUG, "Print job completed.");
    442    return NULL;
    443
    444}
    445
    446void* guac_rdp_print_job_alloc(guac_user* user, void* data) {
    447
    448    /* Allocate nothing if user does not exist */
    449    if (user == NULL)
    450        return NULL;
    451
    452    /* Allocate stream for print job output */
    453    guac_stream* stream = guac_user_alloc_stream(user);
    454    if (stream == NULL)
    455        return NULL;
    456
    457    /* Bail early if allocation fails */
    458    guac_rdp_print_job* job = guac_mem_alloc(sizeof(guac_rdp_print_job));
    459    if (job == NULL)
    460        return NULL;
    461
    462    /* Associate job with stream and dependent data */
    463    job->client = user->client;
    464    job->user = user;
    465    job->stream = stream;
    466    job->bytes_received = 0;
    467
    468    /* Set default filename for job */
    469    strcpy(job->filename, GUAC_RDP_PRINT_JOB_DEFAULT_FILENAME);
    470
    471    /* Prepare stream for receipt of acks */
    472    stream->ack_handler = guac_rdp_print_filter_ack_handler;
    473    stream->data = job;
    474
    475    /* Create print filter process */
    476    job->filter_pid = guac_rdp_create_filter_process(job->client,
    477            &job->input_fd, &job->output_fd);
    478
    479    /* Abort if print filter process cannot be created */
    480    if (job->filter_pid == -1) {
    481        guac_user_free_stream(user, stream);
    482        guac_mem_free(job);
    483        return NULL;
    484    }
    485
    486    /* Init stream state signal and lock */
    487    job->state = GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK;
    488    pthread_cond_init(&job->state_modified, NULL);
    489    pthread_mutex_init(&job->state_lock, NULL);
    490
    491    /* Start output thread */
    492    pthread_create(&job->output_thread, NULL,
    493            guac_rdp_print_job_output_thread, job);
    494
    495    /* Print job allocated successfully */
    496    return job;
    497
    498}
    499
    500/**
    501 * Attempts to parse the given PostScript "%%Title:" header, storing the
    502 * contents within the filename of the given print job. If the given buffer
    503 * does not immediately begin with the "%%Title:" header, this function has no
    504 * effect.
    505 *
    506 * @param job
    507 *     The job whose filename should be set if the "%%Title:" header is
    508 *     successfully parsed.
    509 *
    510 * @param buffer
    511 *     The buffer to parse as the "%%Title:" header.
    512 *
    513 * @param length
    514 *     The number of bytes within the buffer.
    515 *
    516 * @return
    517 *     Non-zero if the given buffer began with the "%%Title:" header and this
    518 *     header was successfully parsed, zero otherwise.
    519 */
    520static int guac_rdp_print_job_parse_title_header(guac_rdp_print_job* job,
    521        void* buffer, int length) {
    522
    523    int i;
    524    char* current = buffer;
    525    char* filename = job->filename;
    526
    527    /* Verify that the buffer begins with "%%Title: " */
    528    if (strncmp(current, "%%Title: ", 9) != 0)
    529        return 0;
    530
    531    /* Skip past "%%Title: " */
    532    current += 9;
    533    length -= 9;
    534
    535    /* Calculate space remaining in filename */
    536    int remaining = sizeof(job->filename) - 5 /* ".pdf\0" */;
    537
    538    /* Do not exceed bounds of provided buffer */
    539    if (length < remaining)
    540        remaining = length;
    541
    542    /* Copy as much of title as reasonable */
    543    for (i = 0; i < remaining; i++) {
    544
    545        /* Get character, stop at EOL */
    546        char c = *(current++);
    547        if (c == '\r' || c == '\n')
    548            break;
    549
    550        /* Copy to filename */
    551        *(filename++) = c;
    552
    553    }
    554
    555    /* Append extension to filename */
    556    strcpy(filename, ".pdf");
    557
    558    /* Title successfully parsed */
    559    return 1;
    560
    561}
    562
    563/**
    564 * Searches through the given buffer for PostScript headers denoting the title
    565 * of the document, assigning the filename of the given print job using the
    566 * discovered title. If no title can be found within
    567 * GUAC_RDP_PRINT_JOB_TITLE_SEARCH_LENGTH bytes, this function has no effect.
    568 *
    569 * @param job
    570 *     The job whose filename should be set if the document title can be found
    571 *     within the given buffer.
    572 *
    573 * @param buffer
    574 *     The buffer to search for the document title.
    575 *
    576 * @param length
    577 *     The number of bytes within the buffer.
    578 */
    579static void guac_rdp_print_job_read_filename(guac_rdp_print_job* job,
    580        void* buffer, int length) {
    581
    582    char* current = buffer;
    583    int i;
    584
    585    /* Restrict search area */
    586    if (length > GUAC_RDP_PRINT_JOB_TITLE_SEARCH_LENGTH)
    587        length = GUAC_RDP_PRINT_JOB_TITLE_SEARCH_LENGTH;
    588
    589    /* Search for document title within buffer */
    590    for (i = 0; i < length; i++) {
    591
    592        /* If document title has been found, we're done */
    593        if (guac_rdp_print_job_parse_title_header(job, current, length))
    594            break;
    595
    596        /* Advance to next character */
    597        length--;
    598        current++;
    599
    600    }
    601
    602}
    603
    604int guac_rdp_print_job_write(guac_rdp_print_job* job,
    605        void* buffer, int length) {
    606
    607    guac_client* client = job->client;
    608    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
    609
    610    /* Create print job, if not yet created */
    611    if (job->bytes_received == 0) {
    612
    613        /* Attempt to read document title from first buffer of data */
    614        guac_rdp_print_job_read_filename(job, buffer, length);
    615
    616        /* Begin print stream */
    617        guac_client_for_user(job->client, job->user,
    618                guac_rdp_print_job_begin_stream, job);
    619
    620    }
    621
    622    /* Update counter of bytes received */
    623    job->bytes_received += length;
    624
    625    /* Write data to filter process, unblocking any threads waiting on the
    626     * generic RDP message lock as this may be a lengthy operation that depends
    627     * on other threads sending outstanding messages (resulting in deadlock if
    628     * those messages are blocked) */
    629    int unlock_status = pthread_mutex_unlock(&(rdp_client->message_lock));
    630    int write_status = write(job->input_fd, buffer, length);
    631
    632    /* Restore RDP message lock state */
    633    if (!unlock_status)
    634        pthread_mutex_lock(&(rdp_client->message_lock));
    635
    636    return write_status;
    637
    638}
    639
    640void guac_rdp_print_job_free(guac_rdp_print_job* job) {
    641
    642    guac_client* client = job->client;
    643    guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
    644
    645    /* No more input will be provided */
    646    close(job->input_fd);
    647
    648    /* Wait for job to terminate, unblocking any threads waiting on the generic
    649     * RDP message lock as this may be a lengthy operation that depends on
    650     * other threads sending outstanding messages (resulting in deadlock if
    651     * those messages are blocked) */
    652    int unlock_status = pthread_mutex_unlock(&(rdp_client->message_lock));
    653    pthread_join(job->output_thread, NULL);
    654
    655    /* Restore RDP message lock state */
    656    if (!unlock_status)
    657        pthread_mutex_lock(&(rdp_client->message_lock));
    658
    659    /* Destroy lock */
    660    pthread_mutex_destroy(&(job->state_lock));
    661
    662    /* Free base structure */
    663    guac_mem_free(job);
    664
    665}
    666
    667void guac_rdp_print_job_kill(guac_rdp_print_job* job) {
    668
    669    /* Forcibly kill filter process, if running */
    670    kill(job->filter_pid, SIGKILL);
    671
    672    /* Stop all handling of I/O */
    673    close(job->input_fd);
    674    close(job->output_fd);
    675
    676    /* Mark stream as closed */
    677    guac_rdp_print_job_set_state(job, GUAC_RDP_PRINT_JOB_CLOSED);
    678
    679}
    680