Skip to content

Commit 5e30d6f

Browse files
committed
GUACAMOLE-2281: Add generic req/done instructions to libguac.
Adds two new protocol instructions in libguac: * "req" - id, name * "done" - id, status "req" starts a typed request/response exchange with a correlation id and a method name (by convention "<feature>.<verb>"). The peer settles it with a "done" carrying the same id and a status, typically "ok" or "error". The bodies of "req" and "done" ride a paired pipe whose name is the correlation id and whose mimetype is GUAC_USER_RPC_PAYLOAD_MIMETYPE ("application/x-guacamole-rpc-payload"). libguac buffers the body under that id and hands it to the user's req or done handler when the matching instruction arrives. Only one body can be in flight per user; senders should emit the pipe, its blobs, the end, and the trailing req or done back to back. The body is capped at GUAC_USER_MAX_RPC_BODY_LENGTH (64 KiB). Both send functions take guac_user, so each RPC exchange targets one specific user rather than broadcasting via the client socket. This avoids the N-done-per-req problem in shared sessions where every joined user would otherwise receive the same req. API surface: * guac_protocol_send_req(user, id, name, body, body_len) and guac_protocol_send_done(user, id, status, body, body_len) in protocol.h / protocol.c. Each allocates a stream against the user, ships the body over the paired pipe via user->socket, then sends the trailing instruction. Body may be NULL with body_len 0. * guac_user_req_handler and guac_user_done_handler typedefs in user-fntypes.h. Both handlers receive the assembled body via (void* body, int body_len), owned by libguac and valid only during the call. * req_handler and done_handler callback fields on guac_user in user.h. * __guac_handle_req and __guac_handle_done dispatchers in user-handlers.c, registered in the instruction handler map. __guac_handle_pipe intercepts the reserved mimetype and assembles the body into per-user pending fields. Also adds an optional destructor callback on guac_stream, called from guac_user_free_stream and guac_client_free_stream, and from guac_user_free and guac_client_free for any streams still open at disconnect. The RPC body assembly installs the destructor when it allocates, so an in-progress body that never reaches end is freed at disconnect. Existing handlers are unaffected since the field defaults to NULL. Unit tests cover the wire format for send_req and send_done, dispatch of inbound req and done, the single-slot pending semantics (end_handler drop-and-replace, req without preceding pipe), and the new stream destructor (per-free-stream, disconnect-cleanup, opt-in no-op when unset, and the regression case where __init_input_stream must reset destructor on slot reuse). The instructions carry no feature-specific semantics. The first consumer is WebAuthn passthrough using "webauthn.create" and "webauthn.get"; other RPC-style features can reuse the same plumbing.
1 parent e775052 commit 5e30d6f

15 files changed

Lines changed: 1571 additions & 11 deletions

src/libguac/client.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,21 @@ guac_stream* guac_client_alloc_stream(guac_client* client) {
139139
allocd_stream->ack_handler = NULL;
140140
allocd_stream->blob_handler = NULL;
141141
allocd_stream->end_handler = NULL;
142+
allocd_stream->destructor = NULL;
142143

143144
return allocd_stream;
144145

145146
}
146147

147148
void guac_client_free_stream(guac_client* client, guac_stream* stream) {
148149

150+
/* Run the registered destructor for the stream's data, if any */
151+
if (stream->destructor != NULL) {
152+
stream->destructor(stream->data);
153+
stream->destructor = NULL;
154+
stream->data = NULL;
155+
}
156+
149157
/* Mark stream as closed */
150158
int freed_index = stream->index;
151159
stream->index = GUAC_CLIENT_CLOSED_STREAM_INDEX;
@@ -289,6 +297,7 @@ guac_client* guac_client_alloc(void) {
289297

290298
for (i=0; i<GUAC_CLIENT_MAX_STREAMS; i++) {
291299
client->__output_streams[i].index = GUAC_CLIENT_CLOSED_STREAM_INDEX;
300+
client->__output_streams[i].destructor = NULL;
292301
}
293302

294303
/* Init locks */
@@ -344,6 +353,15 @@ void guac_client_free(guac_client* client) {
344353
guac_pool_free(client->__buffer_pool);
345354
guac_pool_free(client->__layer_pool);
346355

356+
/* Run any registered destructors for streams still open at
357+
* disconnect so their data pointers do not leak */
358+
for (int i = 0; i < GUAC_CLIENT_MAX_STREAMS; i++) {
359+
guac_stream* stream = &client->__output_streams[i];
360+
if (stream->index != GUAC_CLIENT_CLOSED_STREAM_INDEX
361+
&& stream->destructor != NULL)
362+
stream->destructor(stream->data);
363+
}
364+
347365
/* Free streams */
348366
guac_mem_free(client->__output_streams);
349367

src/libguac/guacamole/protocol.h

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@
2828
* @file protocol.h
2929
*/
3030

31+
#include "client-types.h"
3132
#include "layer-types.h"
3233
#include "object-types.h"
3334
#include "protocol-constants.h"
3435
#include "protocol-types.h"
3536
#include "socket-types.h"
3637
#include "stream-types.h"
3738
#include "timestamp-types.h"
39+
#include "user-types.h"
3840

3941
#include <cairo/cairo.h>
4042
#include <stdarg.h>
@@ -1119,6 +1121,75 @@ int guac_protocol_send_clipboard(guac_socket* socket, const guac_stream* stream,
11191121
*/
11201122
int guac_protocol_send_name(guac_socket* socket, const char* name);
11211123

1124+
/**
1125+
* Sends a "req" to the given user, starting a typed request/response
1126+
* exchange. The body ships first on a pipe named for the correlation id
1127+
* with the reserved GUAC_USER_RPC_PAYLOAD_MIMETYPE; the trailing "req"
1128+
* carries only the id and method name. The peer settles the request with
1129+
* a "done" carrying the same id.
1130+
*
1131+
* Callers on the same guac_user must serialize calls to this and
1132+
* guac_protocol_send_done so the four wire instructions (pipe, blobs,
1133+
* end, req or done) for one exchange are not interleaved with those of
1134+
* another. The receive side tracks at most one in-flight body per user;
1135+
* concurrent senders that interleave would race that slot.
1136+
*
1137+
* @param user
1138+
* The guac_user to send the request to. The body stream is
1139+
* allocated against this user and freed before this function
1140+
* returns.
1141+
*
1142+
* @param id
1143+
* Correlation identifier, typically a UUID string.
1144+
*
1145+
* @param name
1146+
* Method name identifying what kind of request this is.
1147+
*
1148+
* @param body
1149+
* The request body, opaque to libguac. May be NULL if body_len is
1150+
* zero.
1151+
*
1152+
* @param body_len
1153+
* Length of the request body in bytes. May be zero.
1154+
*
1155+
* @return
1156+
* Zero on success, non-zero on error.
1157+
*/
1158+
int guac_protocol_send_req(guac_user* user, const char* id,
1159+
const char* name, const void* body, int body_len);
1160+
1161+
/**
1162+
* Sends a "done" to the given user, settling a previously received "req"
1163+
* with a status. The body ships first on a pipe in the same way as
1164+
* guac_protocol_send_req, including the same per-user serialization
1165+
* requirement.
1166+
*
1167+
* @param user
1168+
* The guac_user to send the response to. The body stream is
1169+
* allocated against this user and freed before this function
1170+
* returns.
1171+
*
1172+
* @param id
1173+
* Correlation id of the request being settled.
1174+
*
1175+
* @param status
1176+
* Status of the request. By convention "ok" for success, "error"
1177+
* for a method-level failure (with details in the body), or
1178+
* "canceled" if the request was aborted.
1179+
*
1180+
* @param body
1181+
* The response body, opaque to libguac. May be NULL if body_len is
1182+
* zero.
1183+
*
1184+
* @param body_len
1185+
* Length of the response body in bytes. May be zero.
1186+
*
1187+
* @return
1188+
* Zero on success, non-zero on error.
1189+
*/
1190+
int guac_protocol_send_done(guac_user* user, const char* id,
1191+
const char* status, const void* body, int body_len);
1192+
11221193
/**
11231194
* Decodes the given base64-encoded string in-place. The base64 string must
11241195
* be NULL-terminated.

src/libguac/guacamole/stream.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,34 @@ struct guac_stream {
105105
*/
106106
guac_user_end_handler* end_handler;
107107

108+
/**
109+
* Optional cleanup callback for whatever the stream's data pointer
110+
* holds. Called from guac_user_free_stream when the stream is freed,
111+
* and from guac_user_free for any streams still open when the user
112+
* disconnects. NULL if no cleanup is needed.
113+
*
114+
* Example:
115+
* @code
116+
* static void my_data_free(void* data) {
117+
* my_data* d = (my_data*) data;
118+
* free(d->buffer);
119+
* free(d);
120+
* }
121+
*
122+
* int my_pipe_handler(guac_user* user, guac_stream* stream,
123+
* char* mimetype, char* name) {
124+
* my_data* d = malloc(sizeof(*d));
125+
* d->buffer = NULL;
126+
* stream->data = d;
127+
* stream->blob_handler = my_blob_handler;
128+
* stream->end_handler = my_end_handler;
129+
* stream->destructor = my_data_free;
130+
* return 0;
131+
* }
132+
* @endcode
133+
*/
134+
void (*destructor)(void* data);
135+
108136
};
109137

110138
#endif

src/libguac/guacamole/user-constants.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,21 @@
6464
*/
6565
#define GUAC_USER_STREAM_INDEX_MIMETYPE "application/vnd.glyptodon.guacamole.stream-index+json"
6666

67+
/**
68+
* Reserved pipe mimetype for the body of a "req" or "done" instruction.
69+
* The pipe's name is the correlation id; libguac buffers the body and
70+
* hands it to the user's req or done handler when the matching
71+
* instruction arrives. Only one body can be in flight per user, so
72+
* senders should emit the pipe, its blobs, the end, and the trailing
73+
* req or done back to back.
74+
*/
75+
#define GUAC_USER_RPC_PAYLOAD_MIMETYPE "application/x-guacamole-rpc-payload"
76+
77+
/**
78+
* Maximum size, in bytes, of a single "req" or "done" body buffered by
79+
* libguac. Larger bodies are rejected before the user's handler runs.
80+
*/
81+
#define GUAC_USER_MAX_RPC_BODY_LENGTH 65536
82+
6783
#endif
6884

src/libguac/guacamole/user-fntypes.h

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ typedef int guac_user_put_handler(guac_user* user, guac_object* object,
500500
guac_stream* stream, char* mimetype, char* name);
501501

502502
/**
503-
* Handler for Guacamole USB connect events, invoked when a "usbconnect"
503+
* Handler for Guacamole USB connect events, invoked when a "usbconnect"
504504
* instruction has been received from a user. This indicates that the user
505505
* has connected a USB device via WebUSB and it is available for redirection.
506506
*
@@ -540,10 +540,82 @@ typedef int guac_user_put_handler(guac_user* user, guac_object* object,
540540
* an error occurred.
541541
*/
542542
typedef int guac_user_usbconnect_handler(guac_user* user, const char* device_id,
543-
int vendor_id, int product_id, const char* device_name,
543+
int vendor_id, int product_id, const char* device_name,
544544
const char* serial_number, int device_class, int device_subclass,
545545
int device_protocol, const char* interface_data);
546546

547+
/**
548+
* Handler for Guacamole "req" events, invoked when a "req" instruction
549+
* has been received from a user. A "req" starts a typed request/response
550+
* exchange that is settled by a later "done" with the same id.
551+
*
552+
* The body comes in on a paired pipe with the correlation id as the name
553+
* and GUAC_USER_RPC_PAYLOAD_MIMETYPE as the mimetype. libguac buffers it
554+
* and hands it to the handler via body and body_len. The buffer belongs
555+
* to libguac and is only valid for this call. If the body is empty,
556+
* body is NULL and body_len is 0.
557+
*
558+
* Example:
559+
* @code
560+
* int req_handler(guac_user* user, char* id, char* name,
561+
* void* body, int body_len);
562+
*
563+
* int guac_user_init(guac_user* user, int argc, char** argv) {
564+
* user->req_handler = req_handler;
565+
* }
566+
* @endcode
567+
*
568+
* @param user
569+
* The user that sent the request.
570+
*
571+
* @param id
572+
* Correlation identifier of this request.
573+
*
574+
* @param name
575+
* Method name identifying what kind of request this is.
576+
*
577+
* @param body
578+
* The request body assembled from the paired pipe, or NULL if
579+
* empty.
580+
*
581+
* @param body_len
582+
* Length of the request body in bytes.
583+
*
584+
* @return
585+
* Zero on success, non-zero on error.
586+
*/
587+
typedef int guac_user_req_handler(guac_user* user, char* id, char* name,
588+
void* body, int body_len);
589+
590+
/**
591+
* Handler for Guacamole "done" events, invoked when a "done" instruction
592+
* has been received from a user. A "done" settles a previously issued
593+
* "req" with a status. The body comes in on a paired pipe in the same
594+
* way as the req handler.
595+
*
596+
* @param user
597+
* The user that sent the response.
598+
*
599+
* @param id
600+
* Correlation identifier of the request being settled.
601+
*
602+
* @param status
603+
* Status of the request. By convention "ok", "error", or
604+
* "canceled".
605+
*
606+
* @param body
607+
* The response body assembled from the paired pipe, or NULL if
608+
* empty.
609+
*
610+
* @param body_len
611+
* Length of the response body in bytes.
612+
*
613+
* @return
614+
* Zero on success, non-zero on error.
615+
*/
616+
typedef int guac_user_done_handler(guac_user* user, char* id, char* status,
617+
void* body, int body_len);
618+
547619
/**
548620
* Handler for Guacamole USB data events, invoked when a "usbdata" instruction
549621
* has been received from a user. This carries data from a client-side USB

src/libguac/guacamole/user.h

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,61 @@ struct guac_user {
598598
*/
599599
guac_user_usbdisconnect_handler* usbdisconnect_handler;
600600

601+
/**
602+
* Handler for "req" events sent by the Guacamole web-client. A "req"
603+
* starts a typed request/response exchange settled by a later "done"
604+
* with the same id. The request body, if any, is assembled by libguac
605+
* from the paired payload pipe and handed to this handler.
606+
*
607+
* Example:
608+
* @code
609+
* int req_handler(guac_user* user, char* id, char* name,
610+
* void* body, int body_len);
611+
*
612+
* int guac_user_init(guac_user* user, int argc, char** argv) {
613+
* user->req_handler = req_handler;
614+
* }
615+
* @endcode
616+
*/
617+
guac_user_req_handler* req_handler;
618+
619+
/**
620+
* Handler for "done" events sent by the Guacamole web-client,
621+
* settling a previously issued "req" with a status. The response
622+
* body, if any, is assembled by libguac from the paired payload pipe
623+
* and handed to this handler.
624+
*
625+
* Example:
626+
* @code
627+
* int done_handler(guac_user* user, char* id, char* status,
628+
* void* body, int body_len);
629+
*
630+
* int guac_user_init(guac_user* user, int argc, char** argv) {
631+
* user->done_handler = done_handler;
632+
* }
633+
* @endcode
634+
*/
635+
guac_user_done_handler* done_handler;
636+
637+
/**
638+
* Correlation id of the most recent payload pipe assembled, still
639+
* waiting for the matching req or done. malloc'd, NULL-terminated,
640+
* or NULL if nothing is buffered. Internal to libguac.
641+
*/
642+
char* __rpc_pending_id;
643+
644+
/**
645+
* Body bytes for __rpc_pending_id. malloc'd, not NULL-terminated,
646+
* undefined when __rpc_pending_id is NULL. Internal to libguac.
647+
*/
648+
char* __rpc_pending_body;
649+
650+
/**
651+
* Length of __rpc_pending_body in bytes. Undefined when
652+
* __rpc_pending_id is NULL. Internal to libguac.
653+
*/
654+
int __rpc_pending_body_len;
655+
601656
};
602657

603658
/**

0 commit comments

Comments
 (0)