| /* |
| * 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 <guacamole/client.h> |
| #include <guacamole/protocol.h> |
| #include <guacamole/socket.h> |
| #include <guacamole/stream.h> |
| #include <guacamole/user.h> |
| |
| #include <errno.h> |
| #include <pthread.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| /** |
| * 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); |
| |
| } |
| |