Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/libguac/guacamole/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -1155,5 +1155,44 @@ guac_protocol_version guac_protocol_string_to_version(const char* version_string
*/
const char* guac_protocol_version_to_string(guac_protocol_version version);

#endif
/**
* Sends a usbdisconnect instruction over the given guac_socket connection,
* requesting that the client disconnect the specified USB device.
*
* @param socket
* The guac_socket connection to use.
*
* @param device_id
* The unique identifier of the USB device that should be disconnected.
*
* @return
* Zero on success, non-zero on error.
*/
int guac_protocol_send_usbdisconnect(guac_socket* socket,
const char* device_id);

/**
* Sends a usbdata instruction over the given guac_socket connection,
* sending USB data to a specific endpoint on a client-side USB device.
*
* @param socket
* The guac_socket connection to use.
*
* @param device_id
* The unique identifier of the USB device.
*
* @param endpoint_number
* The USB endpoint number identifying the logical communication channel
* for this data transfer. The valid endpoint numbers for a device are
* provided in the interface_data parameter during usbconnect.
*
* @param data
* The base64-encoded data to send to the USB device.
*
* @return
* Zero on success, non-zero on error.
*/
int guac_protocol_send_usbdata(guac_socket* socket, const char* device_id,
int endpoint_number, const char* data);

#endif
92 changes: 91 additions & 1 deletion src/libguac/guacamole/user-fntypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -499,5 +499,95 @@ typedef int guac_user_get_handler(guac_user* user, guac_object* object,
typedef int guac_user_put_handler(guac_user* user, guac_object* object,
guac_stream* stream, char* mimetype, char* name);

#endif
/**
* Handler for Guacamole USB connect events, invoked when a "usbconnect"
* instruction has been received from a user. This indicates that the user
* has connected a USB device via WebUSB and it is available for redirection.
*
* @param device_id
* The unique identifier for the USB device. Required.
*
* @param vendor_id
* The vendor ID of the USB device. Required.
*
* @param product_id
* The product ID of the USB device. Required.
*
* @param device_name
* The human-readable name of the device. Required (may be empty string).
*
* @param serial_number
* The serial number of the device. Optional (may be NULL or empty string
* if not available).
*
* @param device_class
* The USB device class. Required.
*
* @param device_subclass
* The USB device subclass. Required.
*
* @param device_protocol
* The USB device protocol. Required.
*
* @param interface_data
* Encoded string containing interface and endpoint information. Required.
* Format: "ifaceNum:class:subclass:protocol:ep1Num:ep1Dir:ep1Type:ep1Size;ep2...,iface2..."
* where multiple endpoints within an interface are separated by semicolons
* and multiple interfaces are separated by commas.
Comment on lines +532 to +536
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of more question from me, here, regarding this...

First, are the format and fields of this interface_data parameter something standard an well-known/documented when dealing with USB devices? Is this data that will be provided automatically by the web browser, or something that it would be expected that someone would know how and what to assemble for it? I can kind of guess what each of the fields means, but a few are a bit obscure.

Somewhat related, it looks like some of the data in this interface_data field is the same as the other parameters to the function (class, subclass, protocol). How does that work out between the parameters provided in the function and the interface data? Should they always be identical? Or are there situations where the individual interfaces will differ from the overall "device" being connected?

I ask for two reasons:

  1. Curiosity, so I understand better how it works.
  2. To make sure we're not sending duplicate data, both for efficiency but also because of the risk that it'll be mis-matched.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format ifaceNum:class:subclass:protocol:ep1Num:ep1Dir:ep1Type:ep1Size;ep2...,iface2... is not a USB standard. It's a custom encoding I set up for this Guacamole protocol.

The data itself comes from standard USB descriptors that the browser's WebUSB API provides automatically. We can see in ManagedUSB.js in our client PR that we're just reading properties that WebUSB gives us:

alt.interfaceClass              // Standard USB class code
alt.interfaceSubclass           // Standard USB subclass code  
alt.interfaceProtocol           // Standard USB protocol code
ep.endpointNumber               // Standard USB endpoint number
ep.direction                    // 'in' or 'out'
ep.type                         // 'bulk', 'interrupt', 'isochronous'
ep.packetSize                   // Max packet size in bytes

These are all standard USB descriptor fields defined in the USB specification. We're just encoding them into a string format for sending over the Guacamole protocol.

As you pointed out thoguh we can see that USB has class/subclass/protocol at two levels.

These describe the overall device and are often set to 0 for composite devices:

  • device_class, device_subclass, device_protocol

Composite devices like webcams with microphones commonly use:

  • Device class 0xEF (Miscellaneous Device)
  • Device subclass 0x02 (Common Class)
  • Device protocol 0x01 (Interface Association Descriptor)

These describe each individual interface and is where the actual functionality is usually defined:

  • interfaceClass, interfaceSubclass, interfaceProtocol

HID devices like keyboards/mice commonly have:

  • Device class 0x00 (defined at interface level)
  • Interface class 0x03 (HID)
  • Interface subclass 0x01 (Boot Interface)
  • Interface protocol 0x01 (Keyboard) or 0x02 (Mouse)
  • Interface 0: class=0x03 (HID), subclass=0x01 (Boot), protocol=0x01 (Keyboard)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay - I'd suggest this all of this - both the encoding of the fields for Guacamole, and the way composite devices are handled - get documented somewhere - I'm not really sure if the code is the best place, or the manual, but I think it'd be useful for folks looking to implement USB functionality with Guacamole to know how we're handling this data.

*
* @return
* Zero if the USB connect event was handled successfully, or non-zero if
* an error occurred.
*/
typedef int guac_user_usbconnect_handler(guac_user* user, const char* device_id,
int vendor_id, int product_id, const char* device_name,
const char* serial_number, int device_class, int device_subclass,
int device_protocol, const char* interface_data);

/**
* Handler for Guacamole USB data events, invoked when a "usbdata" instruction
* has been received from a user. This carries data from a client-side USB
* device to be processed on the server side.
*
* @param user
* The user that sent the USB data.
*
* @param device_id
* The unique identifier for the USB device.
*
* @param endpoint_number
* The USB endpoint number that the data originated from. Endpoint
* numbers correspond to the endpoints defined in the device's
* interface_data from the usbconnect instruction.
*
* @param data
* The base64-encoded USB data.
*
* @param transfer_type
* The type of USB transfer (bulk, interrupt, isochronous, control).
*
* @return
* Zero if the USB data was handled successfully, or non-zero if an error
* occurred.
*/
typedef int guac_user_usbdata_handler(guac_user* user, const char* device_id,
int endpoint_number, const char* data, const char* transfer_type);

/**
* Handler for Guacamole USB disconnect events, invoked when a "usbdisconnect"
* instruction has been received from a user. This indicates that the user
* has disconnected a USB device that was previously available for redirection.
*
* @param user
* The user that disconnected the USB device.
*
* @param device_id
* The unique identifier for the USB device that was disconnected.
*
* @return
* Zero if the USB disconnect event was handled successfully, or non-zero
* if an error occurred.
*/
typedef int guac_user_usbdisconnect_handler(guac_user* user, const char* device_id);

#endif
61 changes: 60 additions & 1 deletion src/libguac/guacamole/user.h
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,66 @@ struct guac_user {
*/
guac_user_touch_handler* touch_handler;

/**
* Handler for USB connect events sent by the Guacamole web-client.
*
* The handler takes the user that connected the device, the device ID
* (required), vendor ID (required), product ID (required), device name
* (required), serial number (optional - may be NULL or empty), device class
* (required), device subclass (required), device protocol (required), and
* interface data (required).
*
* Example:
* @code
* int usbconnect_handler(guac_user* user, const char* device_id,
* int vendor_id, int product_id, const char* device_name,
* const char* serial_number, int device_class,
* int device_subclass, int device_protocol,
* const char* interface_data);
*
* int guac_user_init(guac_user* user, int argc, char** argv) {
* user->usbconnect_handler = usbconnect_handler;
* }
* @endcode
*/
guac_user_usbconnect_handler* usbconnect_handler;

/**
* Handler for USB data events sent by the Guacamole web-client.
*
* The handler takes the user that sent the data, the device ID,
* endpoint number, the base64-encoded data, and the transfer type.
*
* Example:
* @code
* int usbdata_handler(guac_user* user, const char* device_id,
* int endpoint_number, const char* data,
* const char* transfer_type);
*
* int guac_user_init(guac_user* user, int argc, char** argv) {
* user->usbdata_handler = usbdata_handler;
* }
* @endcode
*/
guac_user_usbdata_handler* usbdata_handler;

/**
* Handler for USB disconnect events sent by the Guacamole web-client.
*
* The handler takes the user that disconnected the device and the
* device ID.
*
* Example:
* @code
* int usbdisconnect_handler(guac_user* user, const char* device_id);
*
* int guac_user_init(guac_user* user, int argc, char** argv) {
* user->usbdisconnect_handler = usbdisconnect_handler;
* }
* @endcode
*/
guac_user_usbdisconnect_handler* usbdisconnect_handler;

};

/**
Expand Down Expand Up @@ -1002,4 +1062,3 @@ int guac_user_parse_args_boolean(guac_user* user, const char** arg_names,
const char** argv, int index, int default_value);

#endif

35 changes: 35 additions & 0 deletions src/libguac/protocol.c
Original file line number Diff line number Diff line change
Expand Up @@ -1406,3 +1406,38 @@ const char* guac_protocol_version_to_string(guac_protocol_version version) {

}

int guac_protocol_send_usbdisconnect(guac_socket* socket,
const char* device_id) {

int ret_val;

guac_socket_instruction_begin(socket);
ret_val =
guac_socket_write_string(socket, "13.usbdisconnect,")
|| __guac_socket_write_length_string(socket, device_id)
|| guac_socket_write_string(socket, ";");

guac_socket_instruction_end(socket);
return ret_val;

}

int guac_protocol_send_usbdata(guac_socket* socket, const char* device_id,
int endpoint_number, const char* data) {

int ret_val;

guac_socket_instruction_begin(socket);
ret_val =
guac_socket_write_string(socket, "7.usbdata,")
|| __guac_socket_write_length_string(socket, device_id)
|| guac_socket_write_string(socket, ",")
|| __guac_socket_write_length_int(socket, endpoint_number)
|| guac_socket_write_string(socket, ",")
|| __guac_socket_write_length_string(socket, data)
|| guac_socket_write_string(socket, ";");

guac_socket_instruction_end(socket);
return ret_val;

}
81 changes: 80 additions & 1 deletion src/libguac/user-handlers.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ __guac_instruction_handler_mapping __guac_instruction_handler_map[] = {
{"audio", __guac_handle_audio},
{"argv", __guac_handle_argv},
{"nop", __guac_handle_nop},
{"usbconnect", __guac_handle_usbconnect},
{"usbdata", __guac_handle_usbdata},
{"usbdisconnect", __guac_handle_usbdisconnect},
{NULL, NULL}
};

Expand Down Expand Up @@ -627,6 +630,83 @@ int __guac_handle_disconnect(guac_user* user, int argc, char** argv) {
return 0;
}

int __guac_handle_usbconnect(guac_user* user, int argc, char** argv) {

if (argc != 9) {
guac_user_log(user, GUAC_LOG_WARNING, "Received \"usbconnect\" "
"instruction with wrong number of arguments (expected 9, got %d).",
argc);
return 0;
}

if (user->usbconnect_handler)
return user->usbconnect_handler(
user,
argv[0], /* device_id */
atoi(argv[1]), /* vendor_id */
atoi(argv[2]), /* product_id */
argv[3], /* device_name */
argv[4], /* serial_number */
atoi(argv[5]), /* device_class */
atoi(argv[6]), /* device_subclass */
atoi(argv[7]), /* device_protocol */
argv[8] /* interface_data */
);

guac_user_log(user, GUAC_LOG_DEBUG, "USB redirection not supported, "
"ignoring usbconnect instruction.");
return 0;

}

int __guac_handle_usbdata(guac_user* user, int argc, char** argv) {

if (argc != 4) {
guac_user_log(user, GUAC_LOG_WARNING, "Received \"usbdata\" "
"instruction with wrong number of arguments (expected 4, got %d).",
argc);
return 0;
}

if (user->usbdata_handler) {
/* Decode base64 data */
guac_protocol_decode_base64(argv[2]);
return user->usbdata_handler(
user,
argv[0], /* device_id */
atoi(argv[1]), /* endpoint_number */
argv[2], /* data (now decoded) */
argv[3] /* transfer_type */
);
}

guac_user_log(user, GUAC_LOG_DEBUG, "USB redirection not supported, "
"ignoring usbdata instruction.");
return 0;

}

int __guac_handle_usbdisconnect(guac_user* user, int argc, char** argv) {

if (argc != 1) {
guac_user_log(user, GUAC_LOG_WARNING, "Received \"usbdisconnect\" "
"instruction with wrong number of arguments (expected 1, got %d).",
argc);
return 0;
}

if (user->usbdisconnect_handler)
return user->usbdisconnect_handler(
user,
argv[0] /* device_id */
);

guac_user_log(user, GUAC_LOG_DEBUG, "USB redirection not supported, "
"ignoring usbdisconnect instruction.");
return 0;

}

/* Guacamole handshake handler functions. */

int __guac_handshake_size_handler(guac_user* user, int argc, char** argv) {
Expand Down Expand Up @@ -777,4 +857,3 @@ int __guac_user_call_opcode_handler(__guac_instruction_handler_mapping* map,
return 0;

}

21 changes: 21 additions & 0 deletions src/libguac/user-handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,27 @@ __guac_instruction_handler __guac_handshake_name_handler;
*/
__guac_instruction_handler __guac_handshake_timezone_handler;

/**
* Internal initial handler for the usbconnect instruction. When a usbconnect
* instruction is received, this handler will be called. The user's
* usbconnect handler will be invoked if defined.
*/
__guac_instruction_handler __guac_handle_usbconnect;

/**
* Internal initial handler for the usbdata instruction. When a usbdata
* instruction is received, this handler will be called. The user's
* usbdata handler will be invoked if defined.
*/
__guac_instruction_handler __guac_handle_usbdata;

/**
* Internal initial handler for the usbdisconnect instruction. When a
* usbdisconnect instruction is received, this handler will be called.
* The user's usbdisconnect handler will be invoked if defined.
*/
__guac_instruction_handler __guac_handle_usbdisconnect;

/**
* Instruction handler mapping table. This is a NULL-terminated array of
* __guac_instruction_handler_mapping structures, each mapping an opcode
Expand Down