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