diff --git a/src/libguac/client.c b/src/libguac/client.c index 9738c0c2..29fb5b78 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -302,6 +302,10 @@ int guac_client_add_user(guac_client* client, guac_user* user, int argc, char** /* Update owner pointer if user is owner */ if (user->owner) client->__owner = user; + + /* Notify owner of user joining connection. */ + else + guac_client_owner_notify_join(client, user); } @@ -331,6 +335,10 @@ void guac_client_remove_user(guac_client* client, guac_user* user) { if (user->owner) client->__owner = NULL; + /* Update owner of user having left the connection. */ + else + guac_client_owner_notify_leave(client, user); + pthread_rwlock_unlock(&(client->__users_lock)); /* Call handler, if defined */ @@ -671,6 +679,36 @@ static void* __webp_support_callback(guac_user* user, void* data) { } #endif +/** + * A callback function which is invoked by guac_client_owner_supports_msg() + * to determine if the owner of a client supports the "msg" instruction, + * returning zero if the user does not support the instruction or non-zero if + * the user supports it. + * + * @param user + * The guac_user that will be checked for "msg" instruction support. + * + * @param data + * Data provided to the callback. This value is never used within this + * callback. + * + * @return + * A non-zero integer if the provided user who owns the connection supports + * the "msg" instruction, or zero if the user does not. The integer is cast + * as a void*. + */ +static void* guac_owner_supports_msg_callback(guac_user* user, void* data) { + + return (void*) ((intptr_t) guac_user_supports_msg(user)); + +} + +int guac_client_owner_supports_msg(guac_client* client) { + + return (int) ((intptr_t) guac_client_for_owner(client, guac_owner_supports_msg_callback, NULL)); + +} + /** * A callback function which is invoked by guac_client_owner_supports_required() * to determine if the owner of a client supports the "required" instruction, @@ -701,6 +739,124 @@ int guac_client_owner_supports_required(guac_client* client) { } +/** + * A callback function that is invokved by guac_client_owner_notify_join() to + * notify the owner of a connection that another user has joined the + * connection, returning zero if the message is sent successfully, or non-zero + * if an error occurs. + * + * @param user + * The user to send the notification to, which will be the owner of the + * connection. + * + * @param data + * The data provided to the callback, which is the user that is joining the + * connection. + * + * @return + * Zero if the message is sent successfully to the owner, otherwise + * non-zero, cast as a void*. + */ +static void* guac_client_owner_notify_join_callback(guac_user* user, void* data) { + + const guac_user* joiner = (const guac_user *) data; + + if (user == NULL) + return (void*) ((intptr_t) -1); + + char* log_owner = "owner"; + if (user->info.name != NULL) + log_owner = (char *) user->info.name; + + char* log_joiner = "anonymous"; + char* send_joiner = ""; + if (joiner->info.name != NULL) { + log_joiner = (char *) joiner->info.name; + send_joiner = (char *) joiner->info.name; + } + + guac_user_log(user, GUAC_LOG_DEBUG, "Notifying owner \"%s\" of \"%s\" joining.", + log_owner, log_joiner); + + /* Send user joined notification to owner. */ + const char* args[] = { (const char*)joiner->user_id, (const char*)send_joiner, NULL }; + return (void*) ((intptr_t) guac_protocol_send_msg(user->socket, GUAC_MESSAGE_USER_JOINED, args)); + +} + +int guac_client_owner_notify_join(guac_client* client, guac_user* joiner) { + + /* Don't send msg instruction if client does not support it. */ + if (!guac_client_owner_supports_msg(client)) { + guac_client_log(client, GUAC_LOG_DEBUG, + "Client does not support the \"msg\" instruction and " + "will not be notified of the user joining the connection."); + return -1; + } + + return (int) ((intptr_t) guac_client_for_owner(client, guac_client_owner_notify_join_callback, joiner)); + +} + +/** + * A callback function that is invokved by guac_client_owner_notify_leave() to + * notify the owner of a connection that another user has left the connection, + * returning zero if the message is sent successfully, or non-zero + * if an error occurs. + * + * @param user + * The user to send the notification to, which will be the owner of the + * connection. + * + * @param data + * The data provided to the callback, which is the user that is leaving the + * connection. + * + * @return + * Zero if the message is sent successfully to the owner, otherwise + * non-zero, cast as a void*. + */ +static void* guac_client_owner_notify_leave_callback(guac_user* user, void* data) { + + const guac_user* quitter = (const guac_user *) data; + + if (user == NULL) + return (void*) ((intptr_t) -1); + + char* log_owner = "owner"; + if (user->info.name != NULL) + log_owner = (char *) user->info.name; + + char* log_quitter = "anonymous"; + char* send_quitter = ""; + if (quitter->info.name != NULL) { + log_quitter = (char *) quitter->info.name; + send_quitter = (char *) quitter->info.name; + } + + guac_user_log(user, GUAC_LOG_DEBUG, "Notifying owner \"%s\" of \"%s\" leaving.", + log_owner, log_quitter); + + /* Send user left notification to owner. */ + const char* args[] = { (const char*)quitter->user_id, (const char*)send_quitter, NULL }; + return (void*) ((intptr_t) guac_protocol_send_msg(user->socket, GUAC_MESSAGE_USER_LEFT, args)); + +} + +int guac_client_owner_notify_leave(guac_client* client, guac_user* quitter) { + + /* Don't send msg instruction if client does not support it. */ + if (!guac_client_owner_supports_msg(client)) { + guac_client_log(client, GUAC_LOG_DEBUG, + "Client does not support the \"msg\" instruction and " + "will not be notified of the user leaving the connection."); + return -1; + } + + return (int) ((intptr_t) guac_client_for_owner(client, guac_client_owner_notify_leave_callback, quitter)); + +} + int guac_client_supports_webp(guac_client* client) { #ifdef ENABLE_WEBP diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h index 4ffe5cf4..5712476f 100644 --- a/src/libguac/guacamole/client.h +++ b/src/libguac/guacamole/client.h @@ -708,6 +708,21 @@ void guac_client_stream_webp(guac_client* client, guac_socket* socket, guac_composite_mode mode, const guac_layer* layer, int x, int y, cairo_surface_t* surface, int quality, int lossless); +/** + * Returns whether the owner of the given client supports the "msg" + * instruction, returning non-zero if the client owner does support the + * instruction, or zero if the owner does not. + * + * @param client + * The Guacamole client whose owner should be checked for supporting + * the "msg" instruction. + * + * @return + * Non-zero if the owner of the given client supports the "msg" + * instruction, zero otherwise. + */ +int guac_client_owner_supports_msg(guac_client* client); + /** * Returns whether the owner of the given client supports the "required" * instruction, returning non-zero if the client owner does support the @@ -723,6 +738,42 @@ void guac_client_stream_webp(guac_client* client, guac_socket* socket, */ int guac_client_owner_supports_required(guac_client* client); +/** + * Notifies the owner of the given client that a user has joined the connection, + * and returns zero if the message was sent successfully, or non-zero if the + * notification failed. + * + * @param client + * The Guacamole Client whose owner should be notified of a user joining + * the connection. + * + * @param joiner + * The Guacamole User who joined the connection. + * + * @return + * Zero if the notification to the owner was sent successfully, or non-zero + * if an error occurred. + */ +int guac_client_owner_notify_join(guac_client* client, guac_user* joiner); + +/** + * Notifies the owner of the given client that a user has left the connection, + * and returns zero if the message was sent successfully, or non-zero if the + * notification failed. + * + * @param client + * The Guacamole Client whose owner should be notified of a user leaving + * the connection. + * + * @param quitter + * The Guacamole User who left the connection. + * + * @return + * Zero if the notification to the owner was sent successfully, or non-zero + * if an error occurred. + */ +int guac_client_owner_notify_leave(guac_client* client, guac_user* quitter); + /** * Returns whether all users of the given client support WebP. If any user does * not support WebP, or the server cannot encode WebP images, zero is returned. diff --git a/src/libguac/guacamole/protocol-constants.h b/src/libguac/guacamole/protocol-constants.h index 13d7f680..a8fc9777 100644 --- a/src/libguac/guacamole/protocol-constants.h +++ b/src/libguac/guacamole/protocol-constants.h @@ -38,7 +38,7 @@ * This version is passed by the __guac_protocol_send_args() function from the * server to the client during the client/server handshake. */ -#define GUACAMOLE_PROTOCOL_VERSION "VERSION_1_3_0" +#define GUACAMOLE_PROTOCOL_VERSION "VERSION_1_5_0" /** * The maximum number of bytes that should be sent in any one blob instruction diff --git a/src/libguac/guacamole/protocol-types.h b/src/libguac/guacamole/protocol-types.h index 51652d2b..c943b431 100644 --- a/src/libguac/guacamole/protocol-types.h +++ b/src/libguac/guacamole/protocol-types.h @@ -306,9 +306,40 @@ typedef enum guac_protocol_version { * allowing connections in guacd to request information from the client and * await a response. */ - GUAC_PROTOCOL_VERSION_1_3_0 = 0x010300 + GUAC_PROTOCOL_VERSION_1_3_0 = 0x010300, + + /** + * Protocol version 1.5.0, which supports the "msg" instruction, allowing + * messages to be sent to the client, and adds support for the "name" + * handshake instruction. + */ + GUAC_PROTOCOL_VERSION_1_5_0 = 0x010500 } guac_protocol_version; +/** + * A type that represents codes for human-readable messages sent by the "msg" + * instruction to the Client, that will be displayed in the client's browser. + * The codes will be interpreted by the client into translatable messages, and + * make take arguments, as noted below. + */ +typedef enum guac_message_type { + + /** + * A message that notifies the owner of a connection that another user has + * joined their connection. There should be a single argument provided, the + * name of the user who has joined. + */ + GUAC_MESSAGE_USER_JOINED = 0x0001, + + /** + * A message that notifies the owner of a connection that another user has + * left their connection. There should be a single argument provided, the + * name of the user who has left. + */ + GUAC_MESSAGE_USER_LEFT = 0x0002 + +} guac_message_type; + #endif diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h index 362351c5..91debdde 100644 --- a/src/libguac/guacamole/protocol.h +++ b/src/libguac/guacamole/protocol.h @@ -171,6 +171,27 @@ int guac_protocol_send_log(guac_socket* socket, const char* format, ...); int vguac_protocol_send_log(guac_socket* socket, const char* format, va_list args); +/** + * Sends the given string over the socket to be displayed on the client. Returns + * zero if the message was sent successfully or non-zero if an error occurs. + * + * @param socket + * The guac_socket connection to send the message to. + * + * @param msg + * The message code to send to the client. + * + * @param args + * A null-terminated array of strings that will be provided to the client + * as part of the message, that the client may then place in the message, + * or null if the message requires no arguments. + * + * @return + * Zero if the message is sent successfully; otherwise non-zero. + */ +int guac_protocol_send_msg(guac_socket* socket, guac_message_type msg, + const char** args); + /** * Sends a mouse instruction over the given guac_socket connection. * diff --git a/src/libguac/guacamole/user.h b/src/libguac/guacamole/user.h index 963dbe68..13fb146d 100644 --- a/src/libguac/guacamole/user.h +++ b/src/libguac/guacamole/user.h @@ -88,7 +88,7 @@ struct guac_user_info { * stated resolution of the display size request is recommended. */ int optimal_resolution; - + /** * The timezone of the remote system. If the client does not provide * a specific timezone then this will be NULL. The format of the timezone @@ -102,6 +102,14 @@ struct guac_user_info { */ guac_protocol_version protocol_version; + /** + * The human-readable name of the Guacamole user, supplied by the client + * during the handshake. This is an arbitrary value, with no requirements or + * constraints, including that it need not uniquely identify the user. + * If the client does not provide a name then this will be NULL. + */ + const char* name; + }; struct guac_user { @@ -850,6 +858,17 @@ void guac_user_stream_webp(guac_user* user, guac_socket* socket, guac_composite_mode mode, const guac_layer* layer, int x, int y, cairo_surface_t* surface, int quality, int lossless); +/** + * Returns whether the given user supports the "msg" instruction. + * + * @param user + * The Guacamole user to check for support of the "msg" instruction. + * + * @return + * Non-zero if the user supports the "msg" instruction, otherwise zero. + */ +int guac_user_supports_msg(guac_user* user); + /** * Returns whether the given user supports the "required" instruction. * diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c index 1c53c200..3421af50 100644 --- a/src/libguac/protocol.c +++ b/src/libguac/protocol.c @@ -65,6 +65,7 @@ guac_protocol_version_mapping guac_protocol_version_table[] = { { GUAC_PROTOCOL_VERSION_1_0_0, "VERSION_1_0_0" }, { GUAC_PROTOCOL_VERSION_1_1_0, "VERSION_1_1_0" }, { GUAC_PROTOCOL_VERSION_1_3_0, "VERSION_1_3_0" }, + { GUAC_PROTOCOL_VERSION_1_5_0, "VERSION_1_5_0" }, { GUAC_PROTOCOL_VERSION_UNKNOWN, NULL } }; @@ -658,6 +659,23 @@ int guac_protocol_send_log(guac_socket* socket, const char* format, ...) { } +int guac_protocol_send_msg(guac_socket* socket, guac_message_type msg, + const char** args) { + + int ret_val; + + guac_socket_instruction_begin(socket); + ret_val = + guac_socket_write_string(socket, "3.msg,") + || __guac_socket_write_length_int(socket, msg) + || guac_socket_write_array(socket, args) + || guac_socket_write_string(socket, ";"); + + guac_socket_instruction_end(socket); + return ret_val; + +} + int guac_protocol_send_file(guac_socket* socket, const guac_stream* stream, const char* mimetype, const char* name) { diff --git a/src/libguac/tests/protocol/guac_protocol_version.c b/src/libguac/tests/protocol/guac_protocol_version.c index 37f42e0f..f9b2fdf2 100644 --- a/src/libguac/tests/protocol/guac_protocol_version.c +++ b/src/libguac/tests/protocol/guac_protocol_version.c @@ -27,11 +27,11 @@ */ void test_guac_protocol__version_to_string() { - guac_protocol_version version_a = GUAC_PROTOCOL_VERSION_1_3_0; + guac_protocol_version version_a = GUAC_PROTOCOL_VERSION_1_5_0; guac_protocol_version version_b = GUAC_PROTOCOL_VERSION_1_0_0; guac_protocol_version version_c = GUAC_PROTOCOL_VERSION_UNKNOWN; - CU_ASSERT_STRING_EQUAL(guac_protocol_version_to_string(version_a), "VERSION_1_3_0"); + CU_ASSERT_STRING_EQUAL(guac_protocol_version_to_string(version_a), "VERSION_1_5_0"); CU_ASSERT_STRING_EQUAL(guac_protocol_version_to_string(version_b), "VERSION_1_0_0"); CU_ASSERT_PTR_NULL(guac_protocol_version_to_string(version_c)); diff --git a/src/libguac/user-handlers.c b/src/libguac/user-handlers.c index c5cad9df..db1e7b8b 100644 --- a/src/libguac/user-handlers.c +++ b/src/libguac/user-handlers.c @@ -64,6 +64,7 @@ __guac_instruction_handler_mapping __guac_handshake_handler_map[] = { {"video", __guac_handshake_video_handler}, {"image", __guac_handshake_image_handler}, {"timezone", __guac_handshake_timezone_handler}, + {"name", __guac_handshake_name_handler}, {NULL, NULL} }; @@ -676,6 +677,23 @@ int __guac_handshake_image_handler(guac_user* user, int argc, char** argv) { } +int __guac_handshake_name_handler(guac_user* user, int argc, char** argv) { + + /* Free any past value for the user's name */ + free((char *) user->info.name); + + /* If a value is provided for the name, copy it into guac_user. */ + if (argc > 0 && strcmp(argv[0], "")) + user->info.name = (const char*) strdup(argv[0]); + + /* No or empty value was provided, so make sure this is NULLed out. */ + else + user->info.name = NULL; + + return 0; + +} + int __guac_handshake_timezone_handler(guac_user* user, int argc, char** argv) { /* Free any past value */ diff --git a/src/libguac/user-handlers.h b/src/libguac/user-handlers.h index a51a3f89..7f82302b 100644 --- a/src/libguac/user-handlers.h +++ b/src/libguac/user-handlers.h @@ -218,6 +218,13 @@ __guac_instruction_handler __guac_handshake_video_handler; */ __guac_instruction_handler __guac_handshake_image_handler; +/** + * Internal handler function that is called when the name instruction is + * received during the handshake process, specifying the name of the Guacamole + * user establishing the connection. + */ +__guac_instruction_handler __guac_handshake_name_handler; + /** * Internal handler function that is called when the timezone instruction is * received during the handshake process, specifying the timezone of the diff --git a/src/libguac/user-handshake.c b/src/libguac/user-handshake.c index 4e0fa446..0863325f 100644 --- a/src/libguac/user-handshake.c +++ b/src/libguac/user-handshake.c @@ -296,6 +296,7 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) { user->info.audio_mimetypes = NULL; user->info.image_mimetypes = NULL; user->info.video_mimetypes = NULL; + user->info.name = NULL; user->info.timezone = NULL; /* Count number of arguments. */ @@ -370,7 +371,8 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) { guac_free_mimetypes((char **) user->info.image_mimetypes); guac_free_mimetypes((char **) user->info.video_mimetypes); - /* Free timezone info. */ + /* Free name and timezone info. */ + free((char *) user->info.name); free((char *) user->info.timezone); guac_parser_free(parser); diff --git a/src/libguac/user.c b/src/libguac/user.c index 4320ca77..d16f43b9 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -316,6 +316,15 @@ void guac_user_stream_webp(guac_user* user, guac_socket* socket, } +int guac_user_supports_msg(guac_user* user) { + + if (user == NULL) + return 0; + + return (user->info.protocol_version >= GUAC_PROTOCOL_VERSION_1_5_0); + +} + int guac_user_supports_required(guac_user* user) { if (user == NULL)