GUACAMOLE-200: Merge fix for print blocking input handling.

This commit is contained in:
James Muehlner 2017-02-15 20:50:11 -08:00
commit a7ee5edd8b
8 changed files with 948 additions and 356 deletions

View File

@ -41,6 +41,7 @@ libguac_client_rdp_la_SOURCES = \
rdp_gdi.c \ rdp_gdi.c \
rdp_glyph.c \ rdp_glyph.c \
rdp_keymap.c \ rdp_keymap.c \
rdp_print_job.c \
rdp_pointer.c \ rdp_pointer.c \
rdp_rail.c \ rdp_rail.c \
rdp_settings.c \ rdp_settings.c \
@ -74,6 +75,7 @@ guacdr_sources = \
guac_rdpdr/rdpdr_printer.c \ guac_rdpdr/rdpdr_printer.c \
guac_rdpdr/rdpdr_service.c \ guac_rdpdr/rdpdr_service.c \
rdp_fs.c \ rdp_fs.c \
rdp_print_job.c \
rdp_stream.c \ rdp_stream.c \
unicode.c unicode.c
@ -109,6 +111,7 @@ noinst_HEADERS = \
rdp_glyph.h \ rdp_glyph.h \
rdp_keymap.h \ rdp_keymap.h \
rdp_pointer.h \ rdp_pointer.h \
rdp_print_job.h \
rdp_rail.h \ rdp_rail.h \
rdp_settings.h \ rdp_settings.h \
rdp_status.h \ rdp_status.h \

View File

@ -23,6 +23,7 @@
#include "client.h" #include "client.h"
#include "rdp.h" #include "rdp.h"
#include "rdp_disp.h" #include "rdp_disp.h"
#include "rdp_fs.h"
#include "user.h" #include "user.h"
#ifdef ENABLE_COMMON_SSH #ifdef ENABLE_COMMON_SSH

View File

@ -22,6 +22,8 @@
#include "rdpdr_messages.h" #include "rdpdr_messages.h"
#include "rdpdr_printer.h" #include "rdpdr_printer.h"
#include "rdpdr_service.h" #include "rdpdr_service.h"
#include "rdp.h"
#include "rdp_print_job.h"
#include "rdp_status.h" #include "rdp_status.h"
#include <freerdp/utils/svc_plugin.h> #include <freerdp/utils/svc_plugin.h>
@ -38,330 +40,65 @@
#endif #endif
#include <errno.h> #include <errno.h>
#include <pthread.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
/* Command to run GhostScript safely as a filter writing PDF */
char* const guac_rdpdr_pdf_filter_command[] = {
"gs",
"-q",
"-dNOPAUSE",
"-dBATCH",
"-dSAFER",
"-dPARANOIDSAFER",
"-sDEVICE=pdfwrite",
"-sOutputFile=-",
"-c",
".setpdfwrite",
"-sstdout=/dev/null",
"-f",
"-",
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) {
char buffer[6048];
/* Pull file descriptor from stream data */
int fd = (intptr_t) stream->data;
/* Reading only if ack reports success */
if (status == GUAC_PROTOCOL_STATUS_SUCCESS) {
/* Write a single blob of output */
int length = read(fd, buffer, sizeof(buffer));
if (length > 0) {
guac_protocol_send_blob(user->socket, stream, buffer, length);
guac_socket_flush(user->socket);
return 0;
}
/* Warn of read errors, fall through to termination */
else if (length < 0)
guac_user_log(user, GUAC_LOG_ERROR,
"Error reading from filter: %s", strerror(errno));
}
/* Note if stream aborted by user, fall through to termination */
else
guac_user_log(user, GUAC_LOG_INFO, "Print stream aborted.");
/* Explicitly close down stream */
guac_protocol_send_end(user->socket, stream);
guac_socket_flush(user->socket);
/* Clean up our end of the stream */
guac_user_free_stream(user, stream);
close(fd);
return 0;
}
static int guac_rdpdr_create_print_process(guac_rdpdr_device* device) {
guac_rdpdr_printer_data* printer_data = (guac_rdpdr_printer_data*) device->data;
int child_pid;
int stdin_pipe[2];
int stdout_pipe[2];
/* Create STDIN pipe */
if (pipe(stdin_pipe)) {
guac_client_log(device->rdpdr->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(device->rdpdr->client, GUAC_LOG_ERROR,
"Unable to create STDIN pipe for PDF filter process: %s", strerror(errno));
close(stdin_pipe[0]);
close(stdin_pipe[1]);
return 1;
}
/* Store our side of stdin/stdout */
printer_data->printer_input = stdin_pipe[1];
printer_data->printer_output = stdout_pipe[0];
/* Fork child process */
child_pid = fork();
/* Log fork errors */
if (child_pid == -1) {
guac_client_log(device->rdpdr->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(device->rdpdr->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(device->rdpdr->client, GUAC_LOG_ERROR, "Unable to execute PDF filter command: %s", strerror(errno));
else
guac_client_log(device->rdpdr->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(device->rdpdr->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 0;
}
void guac_rdpdr_process_print_job_create(guac_rdpdr_device* device, void guac_rdpdr_process_print_job_create(guac_rdpdr_device* device,
wStream* input_stream, int completion_id) { wStream* input_stream, int completion_id) {
guac_rdpdr_printer_data* printer_data = guac_client* client = device->rdpdr->client;
(guac_rdpdr_printer_data*) device->data; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
/* Log creation of print job */
guac_client_log(client, GUAC_LOG_INFO, "Print job created");
/* Create print job */
rdp_client->active_job = guac_client_for_owner(client,
guac_rdp_print_job_alloc, NULL);
/* Respond with success */
wStream* output_stream = guac_rdpdr_new_io_completion(device, wStream* output_stream = guac_rdpdr_new_io_completion(device,
completion_id, STATUS_SUCCESS, 4); completion_id, STATUS_SUCCESS, 4);
/* No bytes received yet */
printer_data->bytes_received = 0;
Stream_Write_UINT32(output_stream, 0); /* fileId */ Stream_Write_UINT32(output_stream, 0); /* fileId */
svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream); svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
} }
/**
* Given data representing a print device with a pending pring job, allocates a
* new stream for the given user, associating it with the provided data and
* returning the resulting guac_stream. The stream will be pre-configured to
* send blobs of print data in response to "ack" messages received from the
* user. If the given user is NULL, no stream will be allocated, and the
* print job will be immediately aborted.
*
* @param user
* The user to whom the print job is being sent, or NULL if no stream
* should be allocated.
*
* @param data
* A pointer to the guac_rdpdr_device instance associated with the new
* print job.
*
* @return
* The guac_stream allocated for the new print job, or NULL if no stream
* could be allocated.
*/
static void* guac_rdpdr_alloc_printer_stream(guac_user* owner, void* data) {
guac_rdpdr_device* device = (guac_rdpdr_device*) data;
guac_rdpdr_printer_data* printer_data =
(guac_rdpdr_printer_data*) device->data;
/* Abort immediately if there is no owner */
if (owner == NULL) {
close(printer_data->printer_output);
close(printer_data->printer_input);
printer_data->printer_output = -1;
printer_data->printer_input = -1;
return NULL;
}
/* Allocate stream for owner */
guac_stream* stream = guac_user_alloc_stream(owner);
stream->ack_handler = guac_rdpdr_print_filter_ack_handler;
stream->data = (void*) (intptr_t) printer_data->printer_output;
return stream;
}
void guac_rdpdr_process_print_job_write(guac_rdpdr_device* device, void guac_rdpdr_process_print_job_write(guac_rdpdr_device* device,
wStream* input_stream, int completion_id) { wStream* input_stream, int completion_id) {
guac_rdpdr_printer_data* printer_data = (guac_rdpdr_printer_data*) device->data; guac_client* client = device->rdpdr->client;
int status=0, length; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
guac_rdp_print_job* job = (guac_rdp_print_job*) rdp_client->active_job;
unsigned char* buffer; unsigned char* buffer;
int length;
int status;
wStream* output_stream; /* Read buffer of print data */
Stream_Read_UINT32(input_stream, length); Stream_Read_UINT32(input_stream, length);
Stream_Seek(input_stream, 8); /* Offset */ Stream_Seek(input_stream, 8); /* Offset */
Stream_Seek(input_stream, 20); /* Padding */ Stream_Seek(input_stream, 20); /* Padding */
buffer = Stream_Pointer(input_stream); buffer = Stream_Pointer(input_stream);
/* Create print job, if not yet created */ /* Write data only if job exists, translating status for RDP */
if (printer_data->bytes_received == 0) { if (job != NULL && (length = guac_rdp_print_job_write(job,
buffer, length)) >= 0) {
char filename[1024] = "guacamole-print.pdf"; status = STATUS_SUCCESS;
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<sizeof(filename) - 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 */ /* Report device offline if write fails */
strcpy(&(filename[j]), ".pdf");
break;
}
/* Next character */
search++;
}
/* Start print process */
if (guac_rdpdr_create_print_process(device) != 0) {
status = STATUS_DEVICE_OFF_LINE;
length = 0;
}
/* If print started successfully, create outbound stream */
else { else {
guac_client_log(device->rdpdr->client, GUAC_LOG_INFO,
"Print job created");
/* Allocate stream */
guac_stream* stream = (guac_stream*) guac_client_for_owner(
device->rdpdr->client, guac_rdpdr_alloc_printer_stream,
device);
/* Begin file if stream allocation was successful */
if (stream != NULL)
guac_protocol_send_file(device->rdpdr->client->socket, stream,
"application/pdf", filename);
}
} /* end if print job beginning */
printer_data->bytes_received += length;
/* If not yet failed, write received data */
if (status == 0) {
/* Write data to printer, translate output for RDP */
length = write(printer_data->printer_input, buffer, length);
if (length == -1) {
guac_client_log(device->rdpdr->client, GUAC_LOG_ERROR, "Error writing to printer: %s", strerror(errno));
status = STATUS_DEVICE_OFF_LINE; status = STATUS_DEVICE_OFF_LINE;
length = 0; length = 0;
} }
} wStream* output_stream = guac_rdpdr_new_io_completion(device,
completion_id, status, 5);
output_stream = guac_rdpdr_new_io_completion(device, completion_id,
status, 5);
Stream_Write_UINT32(output_stream, length); Stream_Write_UINT32(output_stream, length);
Stream_Write_UINT8(output_stream, 0); /* Padding */ Stream_Write_UINT8(output_stream, 0); /* Padding */
@ -373,21 +110,25 @@ void guac_rdpdr_process_print_job_write(guac_rdpdr_device* device,
void guac_rdpdr_process_print_job_close(guac_rdpdr_device* device, void guac_rdpdr_process_print_job_close(guac_rdpdr_device* device,
wStream* input_stream, int completion_id) { wStream* input_stream, int completion_id) {
guac_rdpdr_printer_data* printer_data = guac_client* client = device->rdpdr->client;
(guac_rdpdr_printer_data*) device->data; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
guac_rdp_print_job* job = (guac_rdp_print_job*) rdp_client->active_job;
/* End print job */
if (job != NULL) {
guac_rdp_print_job_free(job);
rdp_client->active_job = NULL;
}
wStream* output_stream = guac_rdpdr_new_io_completion(device, wStream* output_stream = guac_rdpdr_new_io_completion(device,
completion_id, STATUS_SUCCESS, 4); completion_id, STATUS_SUCCESS, 4);
Stream_Write_UINT32(output_stream, 0); /* Padding */ Stream_Write_UINT32(output_stream, 0); /* Padding */
/* Close input - the Guacamole stream will continue while output remains */
close(printer_data->printer_input);
printer_data->printer_input = -1;
guac_client_log(device->rdpdr->client, GUAC_LOG_INFO, "Print job closed");
svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream); svc_plugin_send((rdpSvcPlugin*) device->rdpdr, output_stream);
/* Log end of print job */
guac_client_log(client, GUAC_LOG_INFO, "Print job closed");
} }
static void guac_rdpdr_device_printer_announce_handler(guac_rdpdr_device* device, static void guac_rdpdr_device_printer_announce_handler(guac_rdpdr_device* device,
@ -446,21 +187,7 @@ static void guac_rdpdr_device_printer_iorequest_handler(guac_rdpdr_device* devic
} }
static void guac_rdpdr_device_printer_free_handler(guac_rdpdr_device* device) { static void guac_rdpdr_device_printer_free_handler(guac_rdpdr_device* device) {
/* Do nothing */
guac_rdpdr_printer_data* printer_data =
(guac_rdpdr_printer_data*) device->data;
/* Close print job input (STDIN for filter process) if open */
if (printer_data->printer_input != -1)
close(printer_data->printer_input);
/* Close print job output (STDOUT for filter process) if open */
if (printer_data->printer_output != -1)
close(printer_data->printer_output);
/* Free underlying data */
free(device->data);
} }
void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr) { void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr) {
@ -469,7 +196,6 @@ void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr) {
/* Get new device */ /* Get new device */
guac_rdpdr_device* device = &(rdpdr->devices[id]); guac_rdpdr_device* device = &(rdpdr->devices[id]);
guac_rdpdr_printer_data* printer_data;
/* Init device */ /* Init device */
device->rdpdr = rdpdr; device->rdpdr = rdpdr;
@ -481,11 +207,5 @@ void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr) {
device->iorequest_handler = guac_rdpdr_device_printer_iorequest_handler; device->iorequest_handler = guac_rdpdr_device_printer_iorequest_handler;
device->free_handler = guac_rdpdr_device_printer_free_handler; device->free_handler = guac_rdpdr_device_printer_free_handler;
/* Init data */
printer_data = malloc(sizeof(guac_rdpdr_printer_data));
printer_data->printer_input = -1;
printer_data->printer_output = -1;
device->data = printer_data;
} }

View File

@ -31,42 +31,11 @@
#include "compat/winpr-stream.h" #include "compat/winpr-stream.h"
#endif #endif
/**
* Data specific to an instance of the printer device.
*/
typedef struct guac_rdpdr_printer_data {
/**
* File descriptor that should be written to when sending documents to the
* printer. If no print job is in progress, this will be -1.
*/
int printer_input;
/**
* File descriptor that should be read from when receiving output from the
* printer. If no print job is in progress, this will be -1.
*/
int printer_output;
/**
* The number of bytes received in the current print job.
*/
int bytes_received;
} guac_rdpdr_printer_data;
/** /**
* Registers a new printer device within the RDPDR plugin. This must be done * Registers a new printer device within the RDPDR plugin. This must be done
* before RDPDR connection finishes. * before RDPDR connection finishes.
*/ */
void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr); void guac_rdpdr_register_printer(guac_rdpdrPlugin* rdpdr);
/**
* 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.
*/
extern char* const guac_rdpdr_pdf_filter_command[];
#endif #endif

View File

@ -31,6 +31,7 @@
#include "rdp_cliprdr.h" #include "rdp_cliprdr.h"
#include "rdp_disp.h" #include "rdp_disp.h"
#include "rdp_fs.h" #include "rdp_fs.h"
#include "rdp_print_job.h"
#include "rdp_gdi.h" #include "rdp_gdi.h"
#include "rdp_glyph.h" #include "rdp_glyph.h"
#include "rdp_pointer.h" #include "rdp_pointer.h"
@ -853,6 +854,12 @@ static int guac_rdp_handle_connection(guac_client* client) {
} }
/* Clean up print job, if active */
if (rdp_client->active_job != NULL) {
guac_rdp_print_job_kill(rdp_client->active_job);
guac_rdp_print_job_free(rdp_client->active_job);
}
pthread_mutex_lock(&(rdp_client->rdp_lock)); pthread_mutex_lock(&(rdp_client->rdp_lock));
/* Disconnect client and channels */ /* Disconnect client and channels */

View File

@ -30,6 +30,7 @@
#include "keyboard.h" #include "keyboard.h"
#include "rdp_disp.h" #include "rdp_disp.h"
#include "rdp_fs.h" #include "rdp_fs.h"
#include "rdp_print_job.h"
#include "rdp_settings.h" #include "rdp_settings.h"
#include <freerdp/freerdp.h> #include <freerdp/freerdp.h>
@ -120,6 +121,11 @@ typedef struct guac_rdp_client {
*/ */
guac_rdp_fs* filesystem; guac_rdp_fs* filesystem;
/**
* The currently-active print job, or NULL if no print job is active.
*/
guac_rdp_print_job* active_job;
#ifdef ENABLE_COMMON_SSH #ifdef ENABLE_COMMON_SSH
/** /**
* The user and credentials used to authenticate for SFTP. * The user and credentials used to authenticate for SFTP.

View File

@ -0,0 +1,651 @@
/*
* 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);
}

View File

@ -0,0 +1,235 @@
/*
* 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.
*/
#ifndef GUAC_RDP_PRINT_JOB_H
#define GUAC_RDP_PRINT_JOB_H
#include "config.h"
#include <guacamole/client.h>
#include <guacamole/stream.h>
#include <guacamole/user.h>
#include <pthread.h>
#include <unistd.h>
/**
* The maximum number of bytes in the filename of an RDP print job sent as a
* file over the Guacamole protocol, including NULL terminator.
*/
#define GUAC_RDP_PRINT_JOB_FILENAME_MAX_LENGTH 1024
/**
* The default filename to use for the PDF output of an RDP print job if no
* document title can be found within the printed data.
*/
#define GUAC_RDP_PRINT_JOB_DEFAULT_FILENAME "guacamole-print.pdf"
/**
* The maximum number of bytes to search through at the beginning of a
* PostScript document when locating the document title.
*/
#define GUAC_RDP_PRINT_JOB_TITLE_SEARCH_LENGTH 2048
/**
* The current state of an RDP print job.
*/
typedef enum guac_rdp_print_job_state {
/**
* The print stream has been opened with the Guacamole client, but the
* client has not yet confirmed that it is ready to receive data.
*/
GUAC_RDP_PRINT_JOB_WAITING_FOR_ACK,
/**
* The print stream has been opened with the Guacamole client, and the
* client has responded with an "ack", confirming that it is ready to
* receive data (or that data has been received and it is ready to receive
* more).
*/
GUAC_RDP_PRINT_JOB_ACK_RECEIVED,
/**
* The print stream has been closed or the printer is terminating, and no
* further data should be sent to the client.
*/
GUAC_RDP_PRINT_JOB_CLOSED
} guac_rdp_print_job_state;
/**
* Data specific to an instance of the printer device.
*/
typedef struct guac_rdp_print_job {
guac_client* client;
/**
* The user receiving the output from the print job.
*/
guac_user* user;
/**
* The stream along which the print job output should be sent.
*/
guac_stream* stream;
/**
* The PID of the print filter process converting PostScript data into PDF.
*/
pid_t filter_pid;
/**
* The filename that should be used when the converted PDF output is
* streamed to the Guacamole user. This value will be automatically
* determined based on the contents of the printed document.
*/
char filename[GUAC_RDP_PRINT_JOB_FILENAME_MAX_LENGTH];
/**
* File descriptor that should be written to when sending documents to the
* printer.
*/
int input_fd;
/**
* File descriptor that should be read from when receiving output from the
* printer.
*/
int output_fd;
/**
* The current state of the print stream, dependent on whether the client
* has acknowledged creation of the stream, whether the client has
* acknowledged receipt of data along the steam, and whether the print
* stream itself has closed.
*/
guac_rdp_print_job_state state;
/**
* Lock which is acquired prior to modifying the state property or waiting
* on the state_modified conditional.
*/
pthread_mutex_t state_lock;
/**
* Conditional which signals modification to the state property of this
* structure.
*/
pthread_cond_t state_modified;
/**
* Thread which transfers data from the printer to the Guacamole client.
*/
pthread_t output_thread;
/**
* The number of bytes received in the current print job.
*/
int bytes_received;
} guac_rdp_print_job;
/**
* A blob of print data being sent to the Guacamole user.
*/
typedef struct guac_rdp_print_blob {
/**
* The print job which generated the data being sent.
*/
guac_rdp_print_job* job;
/**
* The data being sent.
*/
void* buffer;
/**
* The number of bytes of data being sent.
*/
int length;
} guac_rdp_print_blob;
/**
* Allocates a new print job for the given user. It is expected that this
* function will be invoked via a call to guac_client_for_user() or
* guac_client_for_owner().
*
* @param user
* The user that should receive the output from the print job.
*
* @param data
* An arbitrary data parameter required by guac_client_for_user() and
* guac_client_for_owner() but ignored by this function. This should
* always be NULL.
*
* @return
* A pointer to a newly-allocated guac_rdp_print_job, or NULL if the
* print job could not be created.
*/
void* guac_rdp_print_job_alloc(guac_user* user, void* data);
/**
* Writes PostScript print data to the given active print job. The print job
* will automatically convert this data to PDF, streaming the result to the
* Guacamole user associated with the print job. This function may block if
* the print job is not yet ready for more data.
*
* @param job
* The print job to write to.
*
* @param buffer
* The PostScript print data to write to the given print job.
*
* @param length
* The number of bytes of PostScript print data to write.
*
* @return
* The number of bytes written, or -1 if an error occurs which prevents
* further writes.
*/
int guac_rdp_print_job_write(guac_rdp_print_job* job,
void* buffer, int length);
/**
* Frees the memory associated with the given print job, closing all underlying
* file descriptors, and ending the file transfer to the associated Guacamole
* user. This function may block if the print filter process has not yet
* finished processing the received data.
*
* @param job
* The print job to free.
*/
void guac_rdp_print_job_free(guac_rdp_print_job* job);
/**
* Forcibly kills the given print job, stopping all associated processing and
* streaming. The memory associated with the print job will still need to be
* reclaimed via guac_rdp_print_job_free().
*
* @param job
* The print job to kill.
*/
void guac_rdp_print_job_kill(guac_rdp_print_job* job);
#endif