/* * 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 "rdp_print_job.h" #include #include #include #include #include #include #include #include #include #include /** * The command to run when filtering postscript to produce PDF. This must be * a NULL-terminated array of arguments, where the first argument is the name * of the file to run. */ char* const guac_rdp_pdf_filter_command[] = { "gs", "-q", "-dNOPAUSE", "-dBATCH", "-dSAFER", "-dPARANOIDSAFER", "-sDEVICE=pdfwrite", "-sOutputFile=-", "-c", ".setpdfwrite", "-sstdout=/dev/null", "-f", "-", NULL }; /** * Updates the state of the given print job. Any threads currently blocked by a * call to guac_rdp_print_job_wait_for_ack() will be unblocked. * * @param job * The print job whose state should be updated. * * @param state * The new state to assign to the given print job. */ static void guac_rdp_print_job_set_state(guac_rdp_print_job* job, guac_rdp_print_job_state state) { pthread_mutex_lock(&(job->state_lock)); /* Update stream state, signalling modification */ job->state = state; pthread_cond_signal(&(job->state_modified)); pthread_mutex_unlock(&(job->state_lock)); } /** * Suspends execution of the current thread until the state of the given print * job is not GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK. If the state of the print * job is GUAC_RDP_PRINT_JOB_ACK_RECEIVED, the state is automatically reset * back to GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK prior to returning. * * @param job * The print job to wait for. * * @return * Zero if the state of the print job is GUAC_RDP_PRINT_JOB_CLOSED, * non-zero if the state was GUAC_RDP_PRINT_JOB_ACK_RECEIVED and has been * automatically reset to GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK. */ static int guac_rdp_print_job_wait_for_ack(guac_rdp_print_job* job) { /* Wait for ack if stream open and not yet received */ pthread_mutex_lock(&(job->state_lock)); if (job->state == GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK) pthread_cond_wait(&job->state_modified, &job->state_lock); /* Reset state if ack received */ int got_ack = (job->state == GUAC_RDP_PRINT_JOB_ACK_RECEIVED); if (got_ack) job->state = GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK; /* Return whether ack was successfully received */ pthread_mutex_unlock(&(job->state_lock)); return got_ack; } /** * Sends a "file" instruction to the given user describing the PDF file that * will be sent using the output of the given print job. If the given user no * longer exists, the print stream will be automatically terminated. * * @param user * The user receiving the "file" instruction. * * @param data * A pointer to the guac_rdp_print_job representing the print job being * streamed. * * @return * Always NULL. */ static void* guac_rdp_print_job_begin_stream(guac_user* user, void* data) { guac_rdp_print_job* job = (guac_rdp_print_job*) data; guac_client_log(job->client, GUAC_LOG_DEBUG, "Beginning print stream: %s", job->filename); /* Kill job and do nothing if user no longer exists */ if (user == NULL) { guac_rdp_print_job_kill(job); return NULL; } /* Send document as a PDF file stream */ guac_protocol_send_file(user->socket, job->stream, "application/pdf", job->filename); guac_socket_flush(user->socket); return NULL; } /** * Sends a "blob" instruction to the given user containing the provided data * along the stream associated with the provided print job. If the given user * no longer exists, the print stream will be automatically terminated. * * @param user * The user receiving the "blob" instruction. * * @param data * A pointer to an guac_rdp_print_blob structure containing the data to * be written, the number of bytes being written, and the print job being * streamed. * * @return * Always NULL. */ static void* guac_rdp_print_job_send_blob(guac_user* user, void* data) { guac_rdp_print_blob* blob = (guac_rdp_print_blob*) data; guac_rdp_print_job* job = blob->job; guac_client_log(job->client, GUAC_LOG_DEBUG, "Sending %i byte(s) " "of filtered output.", blob->length); /* Kill job and do nothing if user no longer exists */ if (user == NULL) { guac_rdp_print_job_kill(job); return NULL; } /* Send single blob of print data */ guac_protocol_send_blob(user->socket, job->stream, blob->buffer, blob->length); guac_socket_flush(user->socket); return NULL; } /** * Sends an "end" instruction to the given user, closing the stream associated * with the given print job. If the given user no longer exists, the print * stream will be automatically terminated. * * @param user * The user receiving the "end" instruction. * * @param data * A pointer to the guac_rdp_print_job representing the print job being * streamed. * * @return * Always NULL. */ static void* guac_rdp_print_job_end_stream(guac_user* user, void* data) { guac_rdp_print_job* job = (guac_rdp_print_job*) data; guac_client_log(job->client, GUAC_LOG_DEBUG, "End of print stream."); /* Kill job and do nothing if user no longer exists */ if (user == NULL) { guac_rdp_print_job_kill(job); return NULL; } /* Explicitly close down stream */ guac_protocol_send_end(user->socket, job->stream); guac_socket_flush(user->socket); /* Clean up our end of the stream */ guac_user_free_stream(job->user, job->stream); return NULL; } /** * Handler for "ack" messages received in response to printed data. Additional * data will be sent as a result or, if no data remains, the stream will be * terminated. It is required that the data pointer of the provided stream be * set to the file descriptor from which the printed data should be read. * * @param user * The user to whom the printed data is being sent. * * @param stream * The stream along which the printed data is to be sent. The data pointer * of this stream MUST be set to the file descriptor from which the data * being sent is to be read. * * @param message * An arbitrary, human-readable message describing the success/failure of * the operation being acknowledged (either stream creation or receipt of * a blob). * * @param status * The status code describing the success/failure of the operation being * acknowledged (either stream creation or receipt of a blob). * * @return * Always zero. */ static int guac_rdp_print_filter_ack_handler(guac_user* user, guac_stream* stream, char* message, guac_protocol_status status) { guac_rdp_print_job* job = (guac_rdp_print_job*) stream->data; /* Update state for successful acks */ if (status == GUAC_PROTOCOL_STATUS_SUCCESS) guac_rdp_print_job_set_state(job, GUAC_RDP_PRINT_JOB_ACK_RECEIVED); /* Terminate stream if ack signals an error */ else { /* Note that the stream was aborted by the user */ guac_client_log(job->client, GUAC_LOG_INFO, "User explicitly aborted " "print stream."); /* Kill job (the results will no longer be received) */ guac_rdp_print_job_kill(job); } return 0; } /** * Forks a new print filtering process which accepts PostScript input and * produces PDF output. File descriptors for writing input and reading output * will automatically be allocated and must be manually closed when processing * is complete. * * @param client * The guac_client associated with the print job for which this filter * process is being created. * * @param input_fd * A pointer to an int which should receive the input file descriptor of * the filter process. PostScript input for the filter process should be * written to this file descriptor. * * @param output_fd * A pointer to an int which should receive the output file descriptor of * the filter process. PDF output from the filter process must be * continuously read from this file descriptor or the pipeline may block. * * @return * The PID of the filter process, or -1 if the filter process could not be * created. If the filter process could not be created, the values assigned * through input_fd and output_fd are undefined. */ static pid_t guac_rdp_create_filter_process(guac_client* client, int* input_fd, int* output_fd) { int child_pid; int stdin_pipe[2]; int stdout_pipe[2]; /* Create STDIN pipe */ if (pipe(stdin_pipe)) { guac_client_log(client, GUAC_LOG_ERROR, "Unable to create STDIN " "pipe for PDF filter process: %s", strerror(errno)); return -1; } /* Create STDOUT pipe */ if (pipe(stdout_pipe)) { guac_client_log(client, GUAC_LOG_ERROR, "Unable to create STDOUT " "pipe for PDF filter process: %s", strerror(errno)); close(stdin_pipe[0]); close(stdin_pipe[1]); return -1; } /* Store parent side of stdin/stdout */ *input_fd = stdin_pipe[1]; *output_fd = stdout_pipe[0]; /* Fork child process */ child_pid = fork(); /* Log fork errors */ if (child_pid == -1) { guac_client_log(client, GUAC_LOG_ERROR, "Unable to fork PDF filter " "process: %s", strerror(errno)); close(stdin_pipe[0]); close(stdin_pipe[1]); close(stdout_pipe[0]); close(stdout_pipe[1]); return -1; } /* Child process */ if (child_pid == 0) { /* Close unneeded ends of pipe */ close(stdin_pipe[1]); close(stdout_pipe[0]); /* Reassign file descriptors as STDIN/STDOUT */ dup2(stdin_pipe[0], STDIN_FILENO); dup2(stdout_pipe[1], STDOUT_FILENO); /* Run PDF filter */ guac_client_log(client, GUAC_LOG_INFO, "Running %s", guac_rdp_pdf_filter_command[0]); if (execvp(guac_rdp_pdf_filter_command[0], guac_rdp_pdf_filter_command) < 0) guac_client_log(client, GUAC_LOG_ERROR, "Unable to execute PDF " "filter command: %s", strerror(errno)); else guac_client_log(client, GUAC_LOG_ERROR, "Unable to execute PDF " "filter command, but no error given"); /* Terminate child process */ exit(1); } /* Log fork success */ guac_client_log(client, GUAC_LOG_INFO, "Created PDF filter process " "PID=%i", child_pid); /* Close unneeded ends of pipe */ close(stdin_pipe[0]); close(stdout_pipe[1]); return child_pid; } /** * Thread which continuously reads from the output file descriptor associated * with the given print job, writing filtered PDF output to the associated * Guacamole stream, and terminating only after the print job has completed * processing or the associated Guacamole stream has closed. * * @param data * A pointer to the guac_rdp_print_job representing the print job that * should be read. * * @return * Always NULL. */ static void* guac_rdp_print_job_output_thread(void* data) { int length; char buffer[6048]; guac_rdp_print_job* job = (guac_rdp_print_job*) data; guac_client_log(job->client, GUAC_LOG_DEBUG, "Reading output from filter " "process..."); /* Read continuously while data remains */ while ((length = read(job->output_fd, buffer, sizeof(buffer))) > 0) { /* Wait for client to be ready for blob */ if (guac_rdp_print_job_wait_for_ack(job)) { guac_rdp_print_blob blob = { .job = job, .buffer = buffer, .length = length }; /* Write a single blob of output */ guac_client_for_user(job->client, job->user, guac_rdp_print_job_send_blob, &blob); } /* Abort if stream is closed */ else { guac_client_log(job->client, GUAC_LOG_DEBUG, "Print stream " "explicitly aborted."); break; } } /* Warn of read errors */ if (length < 0) guac_client_log(job->client, GUAC_LOG_ERROR, "Error reading from filter: %s", strerror(errno)); /* Terminate stream */ guac_client_for_user(job->client, job->user, guac_rdp_print_job_end_stream, job); /* Ensure all associated file descriptors are closed */ close(job->input_fd); close(job->output_fd); guac_client_log(job->client, GUAC_LOG_DEBUG, "Print job completed."); return NULL; } void* guac_rdp_print_job_alloc(guac_user* user, void* data) { /* Allocate nothing if user does not exist */ if (user == NULL) return NULL; /* Allocate stream for print job output */ guac_stream* stream = guac_user_alloc_stream(user); if (stream == NULL) return NULL; /* Bail early if allocation fails */ guac_rdp_print_job* job = malloc(sizeof(guac_rdp_print_job)); if (job == NULL) return NULL; /* Associate job with stream and dependent data */ job->client = user->client; job->user = user; job->stream = stream; job->bytes_received = 0; /* Set default filename for job */ strcpy(job->filename, GUAC_RDP_PRINT_JOB_DEFAULT_FILENAME); /* Prepare stream for receipt of acks */ stream->ack_handler = guac_rdp_print_filter_ack_handler; stream->data = job; /* Create print filter process */ job->filter_pid = guac_rdp_create_filter_process(job->client, &job->input_fd, &job->output_fd); /* Abort if print filter process cannot be created */ if (job->filter_pid == -1) { guac_user_free_stream(user, stream); free(job); return NULL; } /* Init stream state signal and lock */ job->state = GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK; pthread_cond_init(&job->state_modified, NULL); pthread_mutex_init(&job->state_lock, NULL); /* Start output thread */ pthread_create(&job->output_thread, NULL, guac_rdp_print_job_output_thread, job); /* Print job allocated successfully */ return job; } /** * Attempts to parse the given PostScript "%%Title:" header, storing the * contents within the filename of the given print job. If the given buffer * does not immediately begin with the "%%Title:" header, this function has no * effect. * * @param job * The job whose filename should be set if the "%%Title:" header is * successfully parsed. * * @param buffer * The buffer to parse as the "%%Title:" header. * * @param length * The number of bytes within the buffer. * * @return * Non-zero if the given buffer began with the "%%Title:" header and this * header was successfully parsed, zero otherwise. */ static int guac_rdp_print_job_parse_title_header(guac_rdp_print_job* job, void* buffer, int length) { int i; char* current = buffer; char* filename = job->filename; /* Verify that the buffer begins with "%%Title: " */ if (strncmp(current, "%%Title: ", 9) != 0) return 0; /* Skip past "%%Title: " */ current += 9; length -= 9; /* Calculate space remaining in filename */ int remaining = sizeof(job->filename) - 5 /* ".pdf\0" */; /* Do not exceed bounds of provided buffer */ if (length < remaining) remaining = length; /* Copy as much of title as reasonable */ for (i = 0; i < remaining; i++) { /* Get character, stop at EOL */ char c = *(current++); if (c == '\r' || c == '\n') break; /* Copy to filename */ *(filename++) = c; } /* Append extension to filename */ strcpy(filename, ".pdf"); /* Title successfully parsed */ return 1; } /** * Searches through the given buffer for PostScript headers denoting the title * of the document, assigning the filename of the given print job using the * discovered title. If no title can be found within * GUAC_RDP_PRINT_JOB_TITLE_SEARCH_LENGTH bytes, this function has no effect. * * @param job * The job whose filename should be set if the document title can be found * within the given buffer. * * @param buffer * The buffer to search for the document title. * * @param length * The number of bytes within the buffer. */ static void guac_rdp_print_job_read_filename(guac_rdp_print_job* job, void* buffer, int length) { char* current = buffer; int i; /* Restrict search area */ if (length > GUAC_RDP_PRINT_JOB_TITLE_SEARCH_LENGTH) length = GUAC_RDP_PRINT_JOB_TITLE_SEARCH_LENGTH; /* Search for document title within buffer */ for (i = 0; i < length; i++) { /* If document title has been found, we're done */ if (guac_rdp_print_job_parse_title_header(job, current, length)) break; /* Advance to next character */ length--; current++; } } int guac_rdp_print_job_write(guac_rdp_print_job* job, void* buffer, int length) { /* Create print job, if not yet created */ if (job->bytes_received == 0) { /* Attempt to read document title from first buffer of data */ guac_rdp_print_job_read_filename(job, buffer, length); /* Begin print stream */ guac_client_for_user(job->client, job->user, guac_rdp_print_job_begin_stream, job); } /* Update counter of bytes received */ job->bytes_received += length; /* Write data to filter process */ return write(job->input_fd, buffer, length); } void guac_rdp_print_job_free(guac_rdp_print_job* job) { /* No more input will be provided */ close(job->input_fd); /* Wait for job to terminate */ pthread_join(job->output_thread, NULL); /* Free base structure */ free(job); } void guac_rdp_print_job_kill(guac_rdp_print_job* job) { /* Stop all handling of I/O */ close(job->input_fd); close(job->output_fd); /* Mark stream as closed */ guac_rdp_print_job_set_state(job, GUAC_RDP_PRINT_JOB_CLOSED); }