guacamole-spice-protocol/src/protocols/rdp/print-job.c

650 lines
19 KiB
C
Raw Normal View History

/*
* 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 "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);
}