blob: f64fd299909e8b41144be2492764ca009d72d39c [file] [log] [blame]
/*
* 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 "guacamole/client.h"
#include "guacamole/object.h"
#include "guacamole/protocol.h"
#include "guacamole/stream.h"
#include "guacamole/timestamp.h"
#include "guacamole/user.h"
#include "user-handlers.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* Guacamole instruction handler map */
__guac_instruction_handler_mapping __guac_instruction_handler_map[] = {
{"sync", __guac_handle_sync},
{"mouse", __guac_handle_mouse},
{"key", __guac_handle_key},
{"clipboard", __guac_handle_clipboard},
{"disconnect", __guac_handle_disconnect},
{"size", __guac_handle_size},
{"file", __guac_handle_file},
{"pipe", __guac_handle_pipe},
{"ack", __guac_handle_ack},
{"blob", __guac_handle_blob},
{"end", __guac_handle_end},
{"get", __guac_handle_get},
{"put", __guac_handle_put},
{"audio", __guac_handle_audio},
{"argv", __guac_handle_argv},
{"nop", __guac_handle_nop},
{NULL, NULL}
};
/* Guacamole handshake handler map */
__guac_instruction_handler_mapping __guac_handshake_handler_map[] = {
{"size", __guac_handshake_size_handler},
{"audio", __guac_handshake_audio_handler},
{"video", __guac_handshake_video_handler},
{"image", __guac_handshake_image_handler},
{"timezone", __guac_handshake_timezone_handler},
{NULL, NULL}
};
/**
* Parses a 64-bit integer from the given string. It is assumed that the string
* will contain only decimal digits, with an optional leading minus sign.
* The result of parsing a string which does not conform to this pattern is
* undefined.
*
* @param str
* The string to parse, which must contain only decimal digits and an
* optional leading minus sign.
*
* @return
* The 64-bit integer value represented by the given string.
*/
static int64_t __guac_parse_int(const char* str) {
int sign = 1;
int64_t num = 0;
for (; *str != '\0'; str++) {
if (*str == '-')
sign = -sign;
else
num = num * 10 + (*str - '0');
}
return num * sign;
}
/* Guacamole instruction handlers */
int __guac_handle_sync(guac_user* user, int argc, char** argv) {
int frame_duration;
guac_timestamp current = guac_timestamp_current();
guac_timestamp timestamp = __guac_parse_int(argv[0]);
/* Error if timestamp is in future */
if (timestamp > user->client->last_sent_timestamp)
return -1;
/* Only update lag calculations if timestamp is sane */
if (timestamp >= user->last_received_timestamp) {
/* Update stored timestamp */
user->last_received_timestamp = timestamp;
/* Calculate length of frame, including network and processing lag */
frame_duration = current - timestamp;
/* Update lag statistics if at least one frame has been rendered */
if (user->last_frame_duration != 0) {
/* Calculate lag using the previous frame as a baseline */
int processing_lag = frame_duration - user->last_frame_duration;
/* Adjust back to zero if cumulative error leads to a negative
* value */
if (processing_lag < 0)
processing_lag = 0;
user->processing_lag = processing_lag;
}
/* Record baseline duration of frame by excluding lag */
user->last_frame_duration = frame_duration - user->processing_lag;
}
/* Log received timestamp and calculated lag (at TRACE level only) */
guac_user_log(user, GUAC_LOG_TRACE,
"User confirmation of frame %" PRIu64 "ms received "
"at %" PRIu64 "ms (processing_lag=%ims)",
timestamp, current, user->processing_lag);
if (user->sync_handler)
return user->sync_handler(user, timestamp);
return 0;
}
int __guac_handle_mouse(guac_user* user, int argc, char** argv) {
if (user->mouse_handler)
return user->mouse_handler(
user,
atoi(argv[0]), /* x */
atoi(argv[1]), /* y */
atoi(argv[2]) /* mask */
);
return 0;
}
int __guac_handle_key(guac_user* user, int argc, char** argv) {
if (user->key_handler)
return user->key_handler(
user,
atoi(argv[0]), /* keysym */
atoi(argv[1]) /* pressed */
);
return 0;
}
/**
* Retrieves the existing user-level input stream having the given index. These
* will be streams which were created by the remotely-connected user. If the
* index is invalid or too large, this function will automatically respond with
* an "ack" instruction containing an appropriate error code.
*
* @param user
* The user associated with the stream being retrieved.
*
* @param stream_index
* The index of the stream to retrieve.
*
* @return
* The stream associated with the given user and having the given index,
* or NULL if the index is invalid.
*/
static guac_stream* __get_input_stream(guac_user* user, int stream_index) {
/* Validate stream index */
if (stream_index < 0 || stream_index >= GUAC_USER_MAX_STREAMS) {
guac_stream dummy_stream;
dummy_stream.index = stream_index;
guac_protocol_send_ack(user->socket, &dummy_stream,
"Invalid stream index", GUAC_PROTOCOL_STATUS_CLIENT_BAD_REQUEST);
return NULL;
}
return &(user->__input_streams[stream_index]);
}
/**
* Retrieves the existing, in-progress (open) user-level input stream having
* the given index. These will be streams which were created by the
* remotely-connected user. If the index is invalid, too large, or the stream
* is closed, this function will automatically respond with an "ack"
* instruction containing an appropriate error code.
*
* @param user
* The user associated with the stream being retrieved.
*
* @param stream_index
* The index of the stream to retrieve.
*
* @return
* The in-progress (open)stream associated with the given user and having
* the given index, or NULL if the index is invalid or the stream is
* closed.
*/
static guac_stream* __get_open_input_stream(guac_user* user, int stream_index) {
guac_stream* stream = __get_input_stream(user, stream_index);
/* Fail if no such stream */
if (stream == NULL)
return NULL;
/* Validate initialization of stream */
if (stream->index == GUAC_USER_CLOSED_STREAM_INDEX) {
guac_stream dummy_stream;
dummy_stream.index = stream_index;
guac_protocol_send_ack(user->socket, &dummy_stream,
"Invalid stream index", GUAC_PROTOCOL_STATUS_CLIENT_BAD_REQUEST);
return NULL;
}
return stream;
}
/**
* Initializes and returns a new user-level input stream having the given
* index, clearing any values that may have been assigned by a past use of the
* underlying stream object storage. If the stream was already open, it will
* first be closed and its end handlers invoked as if explicitly closed by the
* user.
*
* @param user
* The user associated with the stream being initialized.
*
* @param stream_index
* The index of the stream to initialized.
*
* @return
* A new initialized user-level input stream having the given index, or
* NULL if the index is invalid.
*/
static guac_stream* __init_input_stream(guac_user* user, int stream_index) {
guac_stream* stream = __get_input_stream(user, stream_index);
/* Fail if no such stream */
if (stream == NULL)
return NULL;
/* Force end of previous stream if open */
if (stream->index != GUAC_USER_CLOSED_STREAM_INDEX) {
/* Call stream handler if defined */
if (stream->end_handler)
stream->end_handler(user, stream);
/* Fall back to global handler if defined */
else if (user->end_handler)
user->end_handler(user, stream);
}
/* Initialize stream */
stream->index = stream_index;
stream->data = NULL;
stream->ack_handler = NULL;
stream->blob_handler = NULL;
stream->end_handler = NULL;
return stream;
}
int __guac_handle_audio(guac_user* user, int argc, char** argv) {
/* Pull corresponding stream */
int stream_index = atoi(argv[0]);
guac_stream* stream = __init_input_stream(user, stream_index);
if (stream == NULL)
return 0;
/* If supported, call handler */
if (user->audio_handler)
return user->audio_handler(
user,
stream,
argv[1] /* mimetype */
);
/* Otherwise, abort */
guac_protocol_send_ack(user->socket, stream,
"Audio input unsupported", GUAC_PROTOCOL_STATUS_UNSUPPORTED);
return 0;
}
int __guac_handle_clipboard(guac_user* user, int argc, char** argv) {
/* Pull corresponding stream */
int stream_index = atoi(argv[0]);
guac_stream* stream = __init_input_stream(user, stream_index);
if (stream == NULL)
return 0;
/* If supported, call handler */
if (user->clipboard_handler)
return user->clipboard_handler(
user,
stream,
argv[1] /* mimetype */
);
/* Otherwise, abort */
guac_protocol_send_ack(user->socket, stream,
"Clipboard unsupported", GUAC_PROTOCOL_STATUS_UNSUPPORTED);
return 0;
}
int __guac_handle_size(guac_user* user, int argc, char** argv) {
if (user->size_handler)
return user->size_handler(
user,
atoi(argv[0]), /* width */
atoi(argv[1]) /* height */
);
return 0;
}
int __guac_handle_file(guac_user* user, int argc, char** argv) {
/* Pull corresponding stream */
int stream_index = atoi(argv[0]);
guac_stream* stream = __init_input_stream(user, stream_index);
if (stream == NULL)
return 0;
/* If supported, call handler */
if (user->file_handler)
return user->file_handler(
user,
stream,
argv[1], /* mimetype */
argv[2] /* filename */
);
/* Otherwise, abort */
guac_protocol_send_ack(user->socket, stream,
"File transfer unsupported", GUAC_PROTOCOL_STATUS_UNSUPPORTED);
return 0;
}
int __guac_handle_pipe(guac_user* user, int argc, char** argv) {
/* Pull corresponding stream */
int stream_index = atoi(argv[0]);
guac_stream* stream = __init_input_stream(user, stream_index);
if (stream == NULL)
return 0;
/* If supported, call handler */
if (user->pipe_handler)
return user->pipe_handler(
user,
stream,
argv[1], /* mimetype */
argv[2] /* name */
);
/* Otherwise, abort */
guac_protocol_send_ack(user->socket, stream,
"Named pipes unsupported", GUAC_PROTOCOL_STATUS_UNSUPPORTED);
return 0;
}
int __guac_handle_argv(guac_user* user, int argc, char** argv) {
/* Pull corresponding stream */
int stream_index = atoi(argv[0]);
guac_stream* stream = __init_input_stream(user, stream_index);
if (stream == NULL)
return 0;
/* If supported, call handler */
if (user->argv_handler)
return user->argv_handler(
user,
stream,
argv[1], /* mimetype */
argv[2] /* name */
);
/* Otherwise, abort */
guac_protocol_send_ack(user->socket, stream,
"Reconfiguring in-progress connections unsupported",
GUAC_PROTOCOL_STATUS_UNSUPPORTED);
return 0;
}
int __guac_handle_ack(guac_user* user, int argc, char** argv) {
guac_stream* stream;
/* Parse stream index */
int stream_index = atoi(argv[0]);
/* Ignore indices of client-level streams */
if (stream_index % 2 != 0)
return 0;
/* Determine index within user-level array of streams */
stream_index /= 2;
/* Validate stream index */
if (stream_index < 0 || stream_index >= GUAC_USER_MAX_STREAMS)
return 0;
stream = &(user->__output_streams[stream_index]);
/* Validate initialization of stream */
if (stream->index == GUAC_USER_CLOSED_STREAM_INDEX)
return 0;
/* Call stream handler if defined */
if (stream->ack_handler)
return stream->ack_handler(user, stream, argv[1],
atoi(argv[2]));
/* Fall back to global handler if defined */
if (user->ack_handler)
return user->ack_handler(user, stream, argv[1],
atoi(argv[2]));
return 0;
}
int __guac_handle_blob(guac_user* user, int argc, char** argv) {
int stream_index = atoi(argv[0]);
guac_stream* stream = __get_open_input_stream(user, stream_index);
/* Fail if no such stream */
if (stream == NULL)
return 0;
/* Call stream handler if defined */
if (stream->blob_handler) {
int length = guac_protocol_decode_base64(argv[1]);
return stream->blob_handler(user, stream, argv[1],
length);
}
/* Fall back to global handler if defined */
if (user->blob_handler) {
int length = guac_protocol_decode_base64(argv[1]);
return user->blob_handler(user, stream, argv[1],
length);
}
guac_protocol_send_ack(user->socket, stream,
"File transfer unsupported", GUAC_PROTOCOL_STATUS_UNSUPPORTED);
return 0;
}
int __guac_handle_end(guac_user* user, int argc, char** argv) {
int result = 0;
int stream_index = atoi(argv[0]);
guac_stream* stream = __get_open_input_stream(user, stream_index);
/* Fail if no such stream */
if (stream == NULL)
return 0;
/* Call stream handler if defined */
if (stream->end_handler)
result = stream->end_handler(user, stream);
/* Fall back to global handler if defined */
else if (user->end_handler)
result = user->end_handler(user, stream);
/* Mark stream as closed */
stream->index = GUAC_USER_CLOSED_STREAM_INDEX;
return result;
}
int __guac_handle_get(guac_user* user, int argc, char** argv) {
guac_object* object;
/* Validate object index */
int object_index = atoi(argv[0]);
if (object_index < 0 || object_index >= GUAC_USER_MAX_OBJECTS)
return 0;
object = &(user->__objects[object_index]);
/* Validate initialization of object */
if (object->index == GUAC_USER_UNDEFINED_OBJECT_INDEX)
return 0;
/* Call object handler if defined */
if (object->get_handler)
return object->get_handler(
user,
object,
argv[1] /* name */
);
/* Fall back to global handler if defined */
if (user->get_handler)
return user->get_handler(
user,
object,
argv[1] /* name */
);
return 0;
}
int __guac_handle_put(guac_user* user, int argc, char** argv) {
guac_object* object;
/* Validate object index */
int object_index = atoi(argv[0]);
if (object_index < 0 || object_index >= GUAC_USER_MAX_OBJECTS)
return 0;
object = &(user->__objects[object_index]);
/* Validate initialization of object */
if (object->index == GUAC_USER_UNDEFINED_OBJECT_INDEX)
return 0;
/* Pull corresponding stream */
int stream_index = atoi(argv[1]);
guac_stream* stream = __init_input_stream(user, stream_index);
if (stream == NULL)
return 0;
/* Call object handler if defined */
if (object->put_handler)
return object->put_handler(
user,
object,
stream,
argv[2], /* mimetype */
argv[3] /* name */
);
/* Fall back to global handler if defined */
if (user->put_handler)
return user->put_handler(
user,
object,
stream,
argv[2], /* mimetype */
argv[3] /* name */
);
/* Otherwise, abort */
guac_protocol_send_ack(user->socket, stream,
"Object write unsupported", GUAC_PROTOCOL_STATUS_UNSUPPORTED);
return 0;
}
int __guac_handle_nop(guac_user* user, int argc, char** argv) {
guac_user_log(user, GUAC_LOG_TRACE,
"Received nop instruction");
return 0;
}
int __guac_handle_disconnect(guac_user* user, int argc, char** argv) {
guac_user_stop(user);
return 0;
}
/* Guacamole handshake handler functions. */
int __guac_handshake_size_handler(guac_user* user, int argc, char** argv) {
/* Validate size of instruction. */
if (argc < 2) {
guac_user_log(user, GUAC_LOG_ERROR, "Received \"size\" "
"instruction lacked required arguments.");
return 1;
}
/* Parse optimal screen dimensions from size instruction */
user->info.optimal_width = atoi(argv[0]);
user->info.optimal_height = atoi(argv[1]);
/* If DPI given, set the user resolution */
if (argc >= 3)
user->info.optimal_resolution = atoi(argv[2]);
/* Otherwise, use a safe default for rough backwards compatibility */
else
user->info.optimal_resolution = 96;
return 0;
}
int __guac_handshake_audio_handler(guac_user* user, int argc, char** argv) {
guac_free_mimetypes((char **) user->info.audio_mimetypes);
/* Store audio mimetypes */
user->info.audio_mimetypes = (const char**) guac_copy_mimetypes(argv, argc);
return 0;
}
int __guac_handshake_video_handler(guac_user* user, int argc, char** argv) {
guac_free_mimetypes((char **) user->info.video_mimetypes);
/* Store video mimetypes */
user->info.video_mimetypes = (const char**) guac_copy_mimetypes(argv, argc);
return 0;
}
int __guac_handshake_image_handler(guac_user* user, int argc, char** argv) {
guac_free_mimetypes((char **) user->info.image_mimetypes);
/* Store image mimetypes */
user->info.image_mimetypes = (const char**) guac_copy_mimetypes(argv, argc);
return 0;
}
int __guac_handshake_timezone_handler(guac_user* user, int argc, char** argv) {
/* Free any past value */
free((char *) user->info.timezone);
/* Store timezone, if present */
if (argc > 0 && strcmp(argv[0], ""))
user->info.timezone = (const char*) strdup(argv[0]);
else
user->info.timezone = NULL;
return 0;
}
char** guac_copy_mimetypes(char** mimetypes, int count) {
int i;
/* Allocate sufficient space for NULL-terminated array of mimetypes */
char** mimetypes_copy = malloc(sizeof(char*) * (count+1));
/* Copy each provided mimetype */
for (i = 0; i < count; i++)
mimetypes_copy[i] = strdup(mimetypes[i]);
/* Terminate with NULL */
mimetypes_copy[count] = NULL;
return mimetypes_copy;
}
void guac_free_mimetypes(char** mimetypes) {
if (mimetypes == NULL)
return;
char** current_mimetype = mimetypes;
/* Free all strings within NULL-terminated mimetype array */
while (*current_mimetype != NULL) {
free(*current_mimetype);
current_mimetype++;
}
/* Free the array itself, now that its contents have been freed */
free(mimetypes);
}
int __guac_user_call_opcode_handler(__guac_instruction_handler_mapping* map,
guac_user* user, const char* opcode, int argc, char** argv) {
/* For each defined instruction */
__guac_instruction_handler_mapping* current = map;
while (current->opcode != NULL) {
/* If recognized, call handler */
if (strcmp(opcode, current->opcode) == 0)
return current->handler(user, argc, argv);
current++;
}
/* If unrecognized, log and ignore */
guac_user_log(user, GUAC_LOG_DEBUG, "Handler not found for \"%s\"",
opcode);
return 0;
}