582 lines
17 KiB
C
582 lines
17 KiB
C
|
/*
|
||
|
* 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 "rdpdr_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_rdpdr_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_rdpdr_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_rdpdr_print_job_set_state(guac_rdpdr_print_job* job,
|
||
|
guac_rdpdr_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_RDPDR_PRINT_JOB_WAITING_FOR_ACK. If the state of the print
|
||
|
* job is GUAC_RDPDR_PRINT_JOB_ACK_RECEIVED, the state is automatically reset
|
||
|
* back to GUAC_RDPDR_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_RDPDR_PRINT_JOB_CLOSED,
|
||
|
* non-zero if the state was GUAC_RDPDR_PRINT_JOB_ACK_RECEIVED and has been
|
||
|
* automatically reset to GUAC_RDPDR_PRINT_JOB_WAITING_FOR_ACK.
|
||
|
*/
|
||
|
static int guac_rdpdr_print_job_wait_for_ack(guac_rdpdr_print_job* job) {
|
||
|
|
||
|
/* Wait for ack if stream open and not yet received */
|
||
|
pthread_mutex_lock(&(job->state_lock));
|
||
|
if (job->state == GUAC_RDPDR_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_RDPDR_PRINT_JOB_ACK_RECEIVED);
|
||
|
if (got_ack)
|
||
|
job->state = GUAC_RDPDR_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_rdpdr_print_job representing the print job being
|
||
|
* streamed.
|
||
|
*
|
||
|
* @return
|
||
|
* Always NULL.
|
||
|
*/
|
||
|
static void* guac_rdpdr_print_job_begin_stream(guac_user* user, void* data) {
|
||
|
|
||
|
guac_rdpdr_print_job* job = (guac_rdpdr_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_rdpdr_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_rdpdr_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_rdpdr_print_job_send_blob(guac_user* user, void* data) {
|
||
|
|
||
|
guac_rdpdr_print_blob* blob = (guac_rdpdr_print_blob*) data;
|
||
|
guac_rdpdr_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_rdpdr_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_rdpdr_print_job representing the print job being
|
||
|
* streamed.
|
||
|
*
|
||
|
* @return
|
||
|
* Always NULL.
|
||
|
*/
|
||
|
static void* guac_rdpdr_print_job_end_stream(guac_user* user, void* data) {
|
||
|
|
||
|
guac_rdpdr_print_job* job = (guac_rdpdr_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_rdpdr_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_rdpdr_print_filter_ack_handler(guac_user* user,
|
||
|
guac_stream* stream, char* message, guac_protocol_status status) {
|
||
|
|
||
|
guac_rdpdr_print_job* job = (guac_rdpdr_print_job*) stream->data;
|
||
|
|
||
|
/* Update state for successful acks */
|
||
|
if (status == GUAC_PROTOCOL_STATUS_SUCCESS)
|
||
|
guac_rdpdr_print_job_set_state(job, GUAC_RDPDR_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_rdpdr_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_rdpdr_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_rdpdr_pdf_filter_command[0]);
|
||
|
if (execvp(guac_rdpdr_pdf_filter_command[0],
|
||
|
guac_rdpdr_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_rdpdr_print_job representing the print job that
|
||
|
* should be read.
|
||
|
*
|
||
|
* @return
|
||
|
* Always NULL.
|
||
|
*/
|
||
|
static void* guac_rdpdr_print_job_output_thread(void* data) {
|
||
|
|
||
|
int length;
|
||
|
char buffer[6048];
|
||
|
|
||
|
guac_rdpdr_print_job* job = (guac_rdpdr_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_rdpdr_print_job_wait_for_ack(job)) {
|
||
|
|
||
|
guac_rdpdr_print_blob blob = {
|
||
|
.job = job,
|
||
|
.buffer = buffer,
|
||
|
.length = length
|
||
|
};
|
||
|
|
||
|
/* Write a single blob of output */
|
||
|
guac_client_for_user(job->client, job->user,
|
||
|
guac_rdpdr_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_rdpdr_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_rdpdr_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_rdpdr_print_job* job = malloc(sizeof(guac_rdpdr_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_RDPDR_PRINT_JOB_DEFAULT_FILENAME);
|
||
|
|
||
|
/* Prepare stream for receipt of acks */
|
||
|
stream->ack_handler = guac_rdpdr_print_filter_ack_handler;
|
||
|
stream->data = job;
|
||
|
|
||
|
/* Create print filter process */
|
||
|
job->filter_pid = guac_rdpdr_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_RDPDR_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_rdpdr_print_job_output_thread, job);
|
||
|
|
||
|
/* Print job allocated successfully */
|
||
|
return job;
|
||
|
|
||
|
}
|
||
|
|
||
|
int guac_rdpdr_print_job_write(guac_rdpdr_print_job* job,
|
||
|
void* buffer, int length) {
|
||
|
|
||
|
/* Create print job, if not yet created */
|
||
|
if (job->bytes_received == 0) {
|
||
|
|
||
|
char* filename = job->filename;
|
||
|
unsigned char* search = buffer;
|
||
|
int i;
|
||
|
|
||
|
/* Search for filename within buffer */
|
||
|
for (i=0; i<length-9 && i < 2048; i++) {
|
||
|
|
||
|
/* If title. use as filename */
|
||
|
if (memcmp(search, "%%Title: ", 9) == 0) {
|
||
|
|
||
|
/* Skip past "%%Title: " */
|
||
|
search += 9;
|
||
|
|
||
|
/* Copy as much of title as reasonable */
|
||
|
int j;
|
||
|
for (j=0; j < GUAC_RDPDR_PRINT_JOB_FILENAME_MAX_LENGTH - 5 /* extension + 1 */ && i<length; i++, j++) {
|
||
|
|
||
|
/* Get character, stop at EOL */
|
||
|
char c = *(search++);
|
||
|
if (c == '\r' || c == '\n')
|
||
|
break;
|
||
|
|
||
|
/* Copy to filename */
|
||
|
filename[j] = c;
|
||
|
|
||
|
}
|
||
|
|
||
|
/* Append filename with extension */
|
||
|
strcpy(&(filename[j]), ".pdf");
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
/* Next character */
|
||
|
search++;
|
||
|
|
||
|
}
|
||
|
|
||
|
/* Begin print stream */
|
||
|
guac_client_for_user(job->client, job->user,
|
||
|
guac_rdpdr_print_job_begin_stream, job);
|
||
|
|
||
|
} /* end if print job beginning */
|
||
|
|
||
|
/* Update counter of bytes received */
|
||
|
job->bytes_received += length;
|
||
|
|
||
|
/* Write data to filter process */
|
||
|
return write(job->input_fd, buffer, length);
|
||
|
|
||
|
}
|
||
|
|
||
|
void guac_rdpdr_print_job_free(guac_rdpdr_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_rdpdr_print_job_kill(guac_rdpdr_print_job* job) {
|
||
|
|
||
|
/* Stop all handling of I/O */
|
||
|
close(job->input_fd);
|
||
|
close(job->output_fd);
|
||
|
|
||
|
/* Mark stream as closed */
|
||
|
guac_rdpdr_print_job_set_state(job, GUAC_RDPDR_PRINT_JOB_CLOSED);
|
||
|
|
||
|
}
|
||
|
|