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