1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * soup-websocket.c: This file was originally part of Cockpit.
4 *
5 * Copyright 2013, 2014 Red Hat, Inc.
6 *
7 * Cockpit is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU Lesser General Public License as published by
9 * the Free Software Foundation; either version 2.1 of the License, or
10 * (at your option) any later version.
11 *
12 * Cockpit is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public License
18 * along with this library; If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "config.h"
22
23 #include <stdlib.h>
24 #include <string.h>
25 #include <glib/gi18n-lib.h>
26
27 #include "soup-websocket.h"
28 #include "soup-headers.h"
29 #include "soup-message-private.h"
30 #include "soup-websocket-extension.h"
31
32 #define FIXED_DIGEST_LEN 20
33
34 /**
35 * SECTION:soup-websocket
36 * @short_description: The WebSocket Protocol
37 * @see_also: soup_session_websocket_connect_async(),
38 * soup_server_add_websocket_handler()
39 *
40 * #SoupWebsocketConnection provides support for the <ulink
41 * url="http://tools.ietf.org/html/rfc6455">WebSocket</ulink> protocol.
42 *
43 * To connect to a WebSocket server, create a #SoupSession and call
44 * soup_session_websocket_connect_async(). To accept WebSocket
45 * connections, create a #SoupServer and add a handler to it with
46 * soup_server_add_websocket_handler().
47 *
48 * (Lower-level support is available via
49 * soup_websocket_client_prepare_handshake() and
50 * soup_websocket_client_verify_handshake(), for handling the client
51 * side of the WebSocket handshake, and
52 * soup_websocket_server_process_handshake() for handling the server
53 * side.)
54 *
55 * #SoupWebsocketConnection handles the details of WebSocket
56 * communication. You can use soup_websocket_connection_send_text()
57 * and soup_websocket_connection_send_binary() to send data, and the
58 * #SoupWebsocketConnection::message signal to receive data.
59 * (#SoupWebsocketConnection currently only supports asynchronous
60 * I/O.)
61 *
62 * Since: 2.50
63 */
64
65 /**
66 * SOUP_WEBSOCKET_ERROR:
67 *
68 * A #GError domain for WebSocket-related errors. Used with
69 * #SoupWebsocketError.
70 *
71 * Since: 2.50
72 */
73
74 /**
75 * SoupWebsocketError:
76 * @SOUP_WEBSOCKET_ERROR_FAILED: a generic error
77 * @SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET: attempted to handshake with a
78 * server that does not appear to understand WebSockets.
79 * @SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE: the WebSocket handshake failed
80 * because some detail was invalid (eg, incorrect accept key).
81 * @SOUP_WEBSOCKET_ERROR_BAD_ORIGIN: the WebSocket handshake failed
82 * because the "Origin" header was not an allowed value.
83 *
84 * WebSocket-related errors.
85 *
86 * Since: 2.50
87 */
88
89 /**
90 * SoupWebsocketConnectionType:
91 * @SOUP_WEBSOCKET_CONNECTION_UNKNOWN: unknown/invalid connection
92 * @SOUP_WEBSOCKET_CONNECTION_CLIENT: a client-side connection
93 * @SOUP_WEBSOCKET_CONNECTION_SERVER: a server-side connection
94 *
95 * The type of a #SoupWebsocketConnection.
96 *
97 * Since: 2.50
98 */
99
100 /**
101 * SoupWebsocketDataType:
102 * @SOUP_WEBSOCKET_DATA_TEXT: UTF-8 text
103 * @SOUP_WEBSOCKET_DATA_BINARY: binary data
104 *
105 * The type of data contained in a #SoupWebsocketConnection::message
106 * signal.
107 *
108 * Since: 2.50
109 */
110
111 /**
112 * SoupWebsocketCloseCode:
113 * @SOUP_WEBSOCKET_CLOSE_NORMAL: a normal, non-error close
114 * @SOUP_WEBSOCKET_CLOSE_GOING_AWAY: the client/server is going away
115 * @SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR: a protocol error occurred
116 * @SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA: the endpoint received data
117 * of a type that it does not support.
118 * @SOUP_WEBSOCKET_CLOSE_NO_STATUS: reserved value indicating that
119 * no close code was present; must not be sent.
120 * @SOUP_WEBSOCKET_CLOSE_ABNORMAL: reserved value indicating that
121 * the connection was closed abnormally; must not be sent.
122 * @SOUP_WEBSOCKET_CLOSE_BAD_DATA: the endpoint received data that
123 * was invalid (eg, non-UTF-8 data in a text message).
124 * @SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION: generic error code
125 * indicating some sort of policy violation.
126 * @SOUP_WEBSOCKET_CLOSE_TOO_BIG: the endpoint received a message
127 * that is too big to process.
128 * @SOUP_WEBSOCKET_CLOSE_NO_EXTENSION: the client is closing the
129 * connection because the server failed to negotiate a required
130 * extension.
131 * @SOUP_WEBSOCKET_CLOSE_SERVER_ERROR: the server is closing the
132 * connection because it was unable to fulfill the request.
133 * @SOUP_WEBSOCKET_CLOSE_TLS_HANDSHAKE: reserved value indicating that
134 * the TLS handshake failed; must not be sent.
135 *
136 * Pre-defined close codes that can be passed to
137 * soup_websocket_connection_close() or received from
138 * soup_websocket_connection_get_close_code(). (However, other codes
139 * are also allowed.)
140 *
141 * Since: 2.50
142 */
143
144 /**
145 * SoupWebsocketState:
146 * @SOUP_WEBSOCKET_STATE_OPEN: the connection is ready to send messages
147 * @SOUP_WEBSOCKET_STATE_CLOSING: the connection is in the process of
148 * closing down; messages may be received, but not sent
149 * @SOUP_WEBSOCKET_STATE_CLOSED: the connection is completely closed down
150 *
151 * The state of the WebSocket connection.
152 *
153 * Since: 2.50
154 */
155
156 GQuark
soup_websocket_error_get_quark(void)157 soup_websocket_error_get_quark (void)
158 {
159 return g_quark_from_static_string ("web-socket-error-quark");
160 }
161
162 static gboolean
validate_key(const char * key)163 validate_key (const char *key)
164 {
165 guchar buf[18];
166 int state = 0;
167 guint save = 0;
168
169 /* The spec requires us to check that the key is "a
170 * base64-encoded value that, when decoded, is 16 bytes in
171 * length".
172 */
173 if (strlen (key) != 24)
174 return FALSE;
175 if (g_base64_decode_step (key, 24, buf, &state, &save) != 16)
176 return FALSE;
177 return TRUE;
178 }
179
180 static char *
compute_accept_key(const char * key)181 compute_accept_key (const char *key)
182 {
183 gsize digest_len = FIXED_DIGEST_LEN;
184 guchar digest[FIXED_DIGEST_LEN];
185 GChecksum *checksum;
186
187 if (!key)
188 return NULL;
189
190 checksum = g_checksum_new (G_CHECKSUM_SHA1);
191 g_return_val_if_fail (checksum != NULL, NULL);
192
193 g_checksum_update (checksum, (guchar *)key, -1);
194
195 /* magic from: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 */
196 g_checksum_update (checksum, (guchar *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", -1);
197
198 g_checksum_get_digest (checksum, digest, &digest_len);
199 g_checksum_free (checksum);
200
201 g_assert (digest_len == FIXED_DIGEST_LEN);
202
203 return g_base64_encode (digest, digest_len);
204 }
205
206 static gboolean
choose_subprotocol(SoupMessage * msg,const char ** server_protocols,const char ** chosen_protocol)207 choose_subprotocol (SoupMessage *msg,
208 const char **server_protocols,
209 const char **chosen_protocol)
210 {
211 const char *client_protocols_str;
212 char **client_protocols;
213 int i, j;
214
215 if (chosen_protocol)
216 *chosen_protocol = NULL;
217
218 if (!server_protocols)
219 return TRUE;
220
221 client_protocols_str = soup_message_headers_get_one (msg->request_headers,
222 "Sec-Websocket-Protocol");
223 if (!client_protocols_str)
224 return TRUE;
225
226 client_protocols = g_strsplit_set (client_protocols_str, ", ", -1);
227 if (!client_protocols || !client_protocols[0]) {
228 g_strfreev (client_protocols);
229 return TRUE;
230 }
231
232 for (i = 0; server_protocols[i] != NULL; i++) {
233 for (j = 0; client_protocols[j] != NULL; j++) {
234 if (g_str_equal (server_protocols[i], client_protocols[j])) {
235 g_strfreev (client_protocols);
236 if (chosen_protocol)
237 *chosen_protocol = server_protocols[i];
238 return TRUE;
239 }
240 }
241 }
242
243 g_strfreev (client_protocols);
244 return FALSE;
245 }
246
247 /**
248 * soup_websocket_client_prepare_handshake:
249 * @msg: a #SoupMessage
250 * @origin: (allow-none): the "Origin" header to set
251 * @protocols: (allow-none) (array zero-terminated=1): list of
252 * protocols to offer
253 *
254 * Adds the necessary headers to @msg to request a WebSocket
255 * handshake. The message body and non-WebSocket-related headers are
256 * not modified.
257 *
258 * Use soup_websocket_client_prepare_handshake_with_extensions() if you
259 * want to include "Sec-WebSocket-Extensions" header in the request.
260 *
261 * This is a low-level function; if you use
262 * soup_session_websocket_connect_async() to create a WebSocket
263 * connection, it will call this for you.
264 *
265 * Since: 2.50
266 */
267 void
soup_websocket_client_prepare_handshake(SoupMessage * msg,const char * origin,char ** protocols)268 soup_websocket_client_prepare_handshake (SoupMessage *msg,
269 const char *origin,
270 char **protocols)
271 {
272 soup_websocket_client_prepare_handshake_with_extensions (msg, origin, protocols, NULL);
273 }
274
275 /**
276 * soup_websocket_client_prepare_handshake_with_extensions:
277 * @msg: a #SoupMessage
278 * @origin: (nullable): the "Origin" header to set
279 * @protocols: (nullable) (array zero-terminated=1): list of
280 * protocols to offer
281 * @supported_extensions: (nullable) (element-type GObject.TypeClass): list
282 * of supported extension types
283 *
284 * Adds the necessary headers to @msg to request a WebSocket
285 * handshake including supported WebSocket extensions.
286 * The message body and non-WebSocket-related headers are
287 * not modified.
288 *
289 * This is a low-level function; if you use
290 * soup_session_websocket_connect_async() to create a WebSocket
291 * connection, it will call this for you.
292 *
293 * Since: 2.68
294 */
295 void
soup_websocket_client_prepare_handshake_with_extensions(SoupMessage * msg,const char * origin,char ** protocols,GPtrArray * supported_extensions)296 soup_websocket_client_prepare_handshake_with_extensions (SoupMessage *msg,
297 const char *origin,
298 char **protocols,
299 GPtrArray *supported_extensions)
300 {
301 guint32 raw[4];
302 char *key;
303
304 g_return_if_fail (SOUP_IS_MESSAGE (msg));
305
306 soup_message_headers_replace (msg->request_headers, "Upgrade", "websocket");
307 soup_message_headers_append (msg->request_headers, "Connection", "Upgrade");
308
309 raw[0] = g_random_int ();
310 raw[1] = g_random_int ();
311 raw[2] = g_random_int ();
312 raw[3] = g_random_int ();
313 key = g_base64_encode ((const guchar *)raw, sizeof (raw));
314 soup_message_headers_replace (msg->request_headers, "Sec-WebSocket-Key", key);
315 g_free (key);
316
317 soup_message_headers_replace (msg->request_headers, "Sec-WebSocket-Version", "13");
318
319 if (origin)
320 soup_message_headers_replace (msg->request_headers, "Origin", origin);
321
322 if (protocols) {
323 char *protocols_str;
324
325 protocols_str = g_strjoinv (", ", protocols);
326 soup_message_headers_replace (msg->request_headers,
327 "Sec-WebSocket-Protocol", protocols_str);
328 g_free (protocols_str);
329 }
330
331 if (supported_extensions && supported_extensions->len > 0) {
332 guint i;
333 GString *extensions;
334
335 extensions = g_string_new (NULL);
336
337 for (i = 0; i < supported_extensions->len; i++) {
338 SoupWebsocketExtensionClass *extension_class = (SoupWebsocketExtensionClass *)supported_extensions->pdata[i];
339
340 if (soup_message_is_feature_disabled (msg, G_TYPE_FROM_CLASS (extension_class)))
341 continue;
342
343 if (i != 0)
344 extensions = g_string_append (extensions, ", ");
345 extensions = g_string_append (extensions, extension_class->name);
346
347 if (extension_class->get_request_params) {
348 SoupWebsocketExtension *websocket_extension;
349 gchar *params;
350
351 websocket_extension = g_object_new (G_TYPE_FROM_CLASS (extension_class), NULL);
352 params = soup_websocket_extension_get_request_params (websocket_extension);
353 if (params) {
354 extensions = g_string_append (extensions, params);
355 g_free (params);
356 }
357 g_object_unref (websocket_extension);
358 }
359 }
360
361 if (extensions->len > 0) {
362 soup_message_headers_replace (msg->request_headers,
363 "Sec-WebSocket-Extensions",
364 extensions->str);
365 } else {
366 soup_message_headers_remove (msg->request_headers,
367 "Sec-WebSocket-Extensions");
368 }
369 g_string_free (extensions, TRUE);
370 }
371 }
372
373 /**
374 * soup_websocket_server_check_handshake:
375 * @msg: #SoupMessage containing the client side of a WebSocket handshake
376 * @origin: (allow-none): expected Origin header
377 * @protocols: (allow-none) (array zero-terminated=1): allowed WebSocket
378 * protocols.
379 * @error: return location for a #GError
380 *
381 * Examines the method and request headers in @msg and determines
382 * whether @msg contains a valid handshake request.
383 *
384 * If @origin is non-%NULL, then only requests containing a matching
385 * "Origin" header will be accepted. If @protocols is non-%NULL, then
386 * only requests containing a compatible "Sec-WebSocket-Protocols"
387 * header will be accepted.
388 *
389 * Requests containing "Sec-WebSocket-Extensions" header will be
390 * accepted even if the header is not valid. To check a request
391 * with extensions you need to use
392 * soup_websocket_server_check_handshake_with_extensions() and provide
393 * the list of supported extension types.
394 *
395 * Normally soup_websocket_server_process_handshake() will take care
396 * of this for you, and if you use soup_server_add_websocket_handler()
397 * to handle accepting WebSocket connections, it will call that for
398 * you. However, this function may be useful if you need to perform
399 * more complicated validation; eg, accepting multiple different Origins,
400 * or handling different protocols depending on the path.
401 *
402 * Returns: %TRUE if @msg contained a valid WebSocket handshake,
403 * %FALSE and an error if not.
404 *
405 * Since: 2.50
406 */
407 gboolean
soup_websocket_server_check_handshake(SoupMessage * msg,const char * expected_origin,char ** protocols,GError ** error)408 soup_websocket_server_check_handshake (SoupMessage *msg,
409 const char *expected_origin,
410 char **protocols,
411 GError **error)
412 {
413 return soup_websocket_server_check_handshake_with_extensions (msg, expected_origin, protocols, NULL, error);
414 }
415
416 static gboolean
websocket_extension_class_equal(gconstpointer a,gconstpointer b)417 websocket_extension_class_equal (gconstpointer a,
418 gconstpointer b)
419 {
420 return g_str_equal (((const SoupWebsocketExtensionClass *)a)->name, (const char *)b);
421 }
422
423 static GHashTable *
extract_extension_names_from_request(SoupMessage * msg)424 extract_extension_names_from_request (SoupMessage *msg)
425 {
426 const char *extensions;
427 GSList *extension_list, *l;
428 GHashTable *return_value = NULL;
429
430 extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions");
431 if (!extensions || !*extensions)
432 return NULL;
433
434 extension_list = soup_header_parse_list (extensions);
435 for (l = extension_list; l != NULL; l = g_slist_next (l)) {
436 char *extension = (char *)l->data;
437 char *p, *end;
438
439 while (g_ascii_isspace (*extension))
440 extension++;
441
442 if (!*extension)
443 continue;
444
445 p = strstr (extension, ";");
446 end = p ? p : extension + strlen (extension);
447 while (end > extension && g_ascii_isspace (*(end - 1)))
448 end--;
449 *end = '\0';
450
451 if (!return_value)
452 return_value = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
453 g_hash_table_add (return_value, g_strdup (extension));
454 }
455
456 soup_header_free_list (extension_list);
457
458 return return_value;
459 }
460
461 static gboolean
process_extensions(SoupMessage * msg,const char * extensions,gboolean is_server,GPtrArray * supported_extensions,GList ** accepted_extensions,GError ** error)462 process_extensions (SoupMessage *msg,
463 const char *extensions,
464 gboolean is_server,
465 GPtrArray *supported_extensions,
466 GList **accepted_extensions,
467 GError **error)
468 {
469 GSList *extension_list, *l;
470 GHashTable *requested_extensions = NULL;
471
472 if (!supported_extensions || supported_extensions->len == 0) {
473 if (is_server)
474 return TRUE;
475
476 g_set_error_literal (error,
477 SOUP_WEBSOCKET_ERROR,
478 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
479 _("Server requested unsupported extension"));
480 return FALSE;
481 }
482
483 if (!is_server)
484 requested_extensions = extract_extension_names_from_request (msg);
485
486 extension_list = soup_header_parse_list (extensions);
487 for (l = extension_list; l != NULL; l = g_slist_next (l)) {
488 char *extension = (char *)l->data;
489 char *p, *end;
490 guint index;
491 GHashTable *params = NULL;
492 SoupWebsocketExtension *websocket_extension;
493
494 while (g_ascii_isspace (*extension))
495 extension++;
496
497 if (!*extension) {
498 g_set_error (error,
499 SOUP_WEBSOCKET_ERROR,
500 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
501 is_server ?
502 _("Incorrect WebSocket “%s” header") :
503 _("Server returned incorrect “%s” key"),
504 "Sec-WebSocket-Extensions");
505 if (accepted_extensions)
506 g_list_free_full (*accepted_extensions, g_object_unref);
507 g_clear_pointer (&requested_extensions, g_hash_table_destroy);
508 soup_header_free_list (extension_list);
509
510 return FALSE;
511 }
512
513 p = strstr (extension, ";");
514 end = p ? p : extension + strlen (extension);
515 while (end > extension && g_ascii_isspace (*(end - 1)))
516 end--;
517 *end = '\0';
518
519 if (requested_extensions && !g_hash_table_contains (requested_extensions, extension)) {
520 g_set_error_literal (error,
521 SOUP_WEBSOCKET_ERROR,
522 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
523 _("Server requested unsupported extension"));
524 if (accepted_extensions)
525 g_list_free_full (*accepted_extensions, g_object_unref);
526 g_clear_pointer (&requested_extensions, g_hash_table_destroy);
527 soup_header_free_list (extension_list);
528
529 return FALSE;
530 }
531
532 if (!g_ptr_array_find_with_equal_func (supported_extensions, extension, websocket_extension_class_equal, &index)) {
533 if (is_server)
534 continue;
535
536 g_set_error_literal (error,
537 SOUP_WEBSOCKET_ERROR,
538 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
539 _("Server requested unsupported extension"));
540 if (accepted_extensions)
541 g_list_free_full (*accepted_extensions, g_object_unref);
542 g_clear_pointer (&requested_extensions, g_hash_table_destroy);
543 soup_header_free_list (extension_list);
544
545 return FALSE;
546 }
547
548 /* If we are just checking headers in server side
549 * and there's no parameters, it's enough to know
550 * the extension is supported.
551 */
552 if (is_server && !accepted_extensions && !p)
553 continue;
554
555 websocket_extension = g_object_new (G_TYPE_FROM_CLASS (supported_extensions->pdata[index]), NULL);
556 if (accepted_extensions)
557 *accepted_extensions = g_list_prepend (*accepted_extensions, websocket_extension);
558
559 if (p) {
560 params = soup_header_parse_semi_param_list_strict (p + 1);
561 if (!params) {
562 g_set_error (error,
563 SOUP_WEBSOCKET_ERROR,
564 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
565 is_server ?
566 _("Duplicated parameter in “%s” WebSocket extension header") :
567 _("Server returned a duplicated parameter in “%s” WebSocket extension header"),
568 extension);
569 if (accepted_extensions)
570 g_list_free_full (*accepted_extensions, g_object_unref);
571 else
572 g_object_unref (websocket_extension);
573 g_clear_pointer (&requested_extensions, g_hash_table_destroy);
574 soup_header_free_list (extension_list);
575
576 return FALSE;
577 }
578 }
579
580 if (!soup_websocket_extension_configure (websocket_extension,
581 is_server ? SOUP_WEBSOCKET_CONNECTION_SERVER : SOUP_WEBSOCKET_CONNECTION_CLIENT,
582 params,
583 error)) {
584 g_clear_pointer (¶ms, g_hash_table_destroy);
585 if (accepted_extensions)
586 g_list_free_full (*accepted_extensions, g_object_unref);
587 else
588 g_object_unref (websocket_extension);
589 g_clear_pointer (&requested_extensions, g_hash_table_destroy);
590 soup_header_free_list (extension_list);
591
592 return FALSE;
593 }
594 g_clear_pointer (¶ms, g_hash_table_destroy);
595 if (!accepted_extensions)
596 g_object_unref (websocket_extension);
597 }
598
599 soup_header_free_list (extension_list);
600 g_clear_pointer (&requested_extensions, g_hash_table_destroy);
601
602 if (accepted_extensions)
603 *accepted_extensions = g_list_reverse (*accepted_extensions);
604
605 return TRUE;
606 }
607
608 /**
609 * soup_websocket_server_check_handshake_with_extensions:
610 * @msg: #SoupMessage containing the client side of a WebSocket handshake
611 * @origin: (nullable): expected Origin header
612 * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket
613 * protocols.
614 * @supported_extensions: (nullable) (element-type GObject.TypeClass): list
615 * of supported extension types
616 * @error: return location for a #GError
617 *
618 * Examines the method and request headers in @msg and determines
619 * whether @msg contains a valid handshake request.
620 *
621 * If @origin is non-%NULL, then only requests containing a matching
622 * "Origin" header will be accepted. If @protocols is non-%NULL, then
623 * only requests containing a compatible "Sec-WebSocket-Protocols"
624 * header will be accepted. If @supported_extensions is non-%NULL, then
625 * only requests containing valid supported extensions in
626 * "Sec-WebSocket-Extensions" header will be accepted.
627 *
628 * Normally soup_websocket_server_process_handshake_with_extensioins()
629 * will take care of this for you, and if you use
630 * soup_server_add_websocket_handler() to handle accepting WebSocket
631 * connections, it will call that for you. However, this function may
632 * be useful if you need to perform more complicated validation; eg,
633 * accepting multiple different Origins, or handling different protocols
634 * depending on the path.
635 *
636 * Returns: %TRUE if @msg contained a valid WebSocket handshake,
637 * %FALSE and an error if not.
638 *
639 * Since: 2.68
640 */
641 gboolean
soup_websocket_server_check_handshake_with_extensions(SoupMessage * msg,const char * expected_origin,char ** protocols,GPtrArray * supported_extensions,GError ** error)642 soup_websocket_server_check_handshake_with_extensions (SoupMessage *msg,
643 const char *expected_origin,
644 char **protocols,
645 GPtrArray *supported_extensions,
646 GError **error)
647 {
648 const char *origin;
649 const char *key;
650 const char *extensions;
651
652 g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE);
653
654 if (msg->method != SOUP_METHOD_GET) {
655 g_set_error_literal (error,
656 SOUP_WEBSOCKET_ERROR,
657 SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
658 _("WebSocket handshake expected"));
659 return FALSE;
660 }
661
662 if (!soup_message_headers_header_equals (msg->request_headers, "Upgrade", "websocket") ||
663 !soup_message_headers_header_contains (msg->request_headers, "Connection", "upgrade")) {
664 g_set_error_literal (error,
665 SOUP_WEBSOCKET_ERROR,
666 SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
667 _("WebSocket handshake expected"));
668 return FALSE;
669 }
670
671 if (!soup_message_headers_header_equals (msg->request_headers, "Sec-WebSocket-Version", "13")) {
672 g_set_error_literal (error,
673 SOUP_WEBSOCKET_ERROR,
674 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
675 _("Unsupported WebSocket version"));
676 return FALSE;
677 }
678
679 key = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key");
680 if (key == NULL || !validate_key (key)) {
681 g_set_error_literal (error,
682 SOUP_WEBSOCKET_ERROR,
683 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
684 _("Invalid WebSocket key"));
685 return FALSE;
686 }
687
688 if (expected_origin) {
689 origin = soup_message_headers_get_one (msg->request_headers, "Origin");
690 if (!origin || g_ascii_strcasecmp (origin, expected_origin) != 0) {
691 g_set_error (error,
692 SOUP_WEBSOCKET_ERROR,
693 SOUP_WEBSOCKET_ERROR_BAD_ORIGIN,
694 _("Incorrect WebSocket “%s” header"), "Origin");
695 return FALSE;
696 }
697 }
698
699 if (!choose_subprotocol (msg, (const char **) protocols, NULL)) {
700 g_set_error_literal (error,
701 SOUP_WEBSOCKET_ERROR,
702 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
703 _("Unsupported WebSocket subprotocol"));
704 return FALSE;
705 }
706
707 extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions");
708 if (extensions && *extensions) {
709 if (!process_extensions (msg, extensions, TRUE, supported_extensions, NULL, error))
710 return FALSE;
711 }
712
713 return TRUE;
714 }
715
716 #define RESPONSE_FORBIDDEN "<html><head><title>400 Forbidden</title></head>\r\n" \
717 "<body>Received invalid WebSocket request</body></html>\r\n"
718
719 static void
respond_handshake_forbidden(SoupMessage * msg)720 respond_handshake_forbidden (SoupMessage *msg)
721 {
722 soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
723 soup_message_headers_append (msg->response_headers, "Connection", "close");
724 soup_message_set_response (msg, "text/html", SOUP_MEMORY_COPY,
725 RESPONSE_FORBIDDEN, strlen (RESPONSE_FORBIDDEN));
726 }
727
728 #define RESPONSE_BAD "<html><head><title>400 Bad Request</title></head>\r\n" \
729 "<body>Received invalid WebSocket request: %s</body></html>\r\n"
730
731 static void
respond_handshake_bad(SoupMessage * msg,const char * why)732 respond_handshake_bad (SoupMessage *msg, const char *why)
733 {
734 char *text;
735
736 text = g_strdup_printf (RESPONSE_BAD, why);
737 soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
738 soup_message_headers_append (msg->response_headers, "Connection", "close");
739 soup_message_set_response (msg, "text/html", SOUP_MEMORY_TAKE,
740 text, strlen (text));
741 }
742
743 /**
744 * soup_websocket_server_process_handshake:
745 * @msg: #SoupMessage containing the client side of a WebSocket handshake
746 * @expected_origin: (allow-none): expected Origin header
747 * @protocols: (allow-none) (array zero-terminated=1): allowed WebSocket
748 * protocols.
749 *
750 * Examines the method and request headers in @msg and (assuming @msg
751 * contains a valid handshake request), fills in the handshake
752 * response.
753 *
754 * If @expected_origin is non-%NULL, then only requests containing a matching
755 * "Origin" header will be accepted. If @protocols is non-%NULL, then
756 * only requests containing a compatible "Sec-WebSocket-Protocols"
757 * header will be accepted.
758 *
759 * Requests containing "Sec-WebSocket-Extensions" header will be
760 * accepted even if the header is not valid. To process a request
761 * with extensions you need to use
762 * soup_websocket_server_process_handshake_with_extensions() and provide
763 * the list of supported extension types.
764 *
765 * This is a low-level function; if you use
766 * soup_server_add_websocket_handler() to handle accepting WebSocket
767 * connections, it will call this for you.
768 *
769 * Returns: %TRUE if @msg contained a valid WebSocket handshake
770 * request and was updated to contain a handshake response. %FALSE if not.
771 *
772 * Since: 2.50
773 */
774 gboolean
soup_websocket_server_process_handshake(SoupMessage * msg,const char * expected_origin,char ** protocols)775 soup_websocket_server_process_handshake (SoupMessage *msg,
776 const char *expected_origin,
777 char **protocols)
778 {
779 return soup_websocket_server_process_handshake_with_extensions (msg, expected_origin, protocols, NULL, NULL);
780 }
781
782 /**
783 * soup_websocket_server_process_handshake_with_extensions:
784 * @msg: #SoupMessage containing the client side of a WebSocket handshake
785 * @expected_origin: (nullable): expected Origin header
786 * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket
787 * protocols.
788 * @supported_extensions: (nullable) (element-type GObject.TypeClass): list
789 * of supported extension types
790 * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a
791 * #GList of #SoupWebsocketExtension objects
792 *
793 * Examines the method and request headers in @msg and (assuming @msg
794 * contains a valid handshake request), fills in the handshake
795 * response.
796 *
797 * If @expected_origin is non-%NULL, then only requests containing a matching
798 * "Origin" header will be accepted. If @protocols is non-%NULL, then
799 * only requests containing a compatible "Sec-WebSocket-Protocols"
800 * header will be accepted. If @supported_extensions is non-%NULL, then
801 * only requests containing valid supported extensions in
802 * "Sec-WebSocket-Extensions" header will be accepted. The accepted extensions
803 * will be returned in @accepted_extensions parameter if non-%NULL.
804 *
805 * This is a low-level function; if you use
806 * soup_server_add_websocket_handler() to handle accepting WebSocket
807 * connections, it will call this for you.
808 *
809 * Returns: %TRUE if @msg contained a valid WebSocket handshake
810 * request and was updated to contain a handshake response. %FALSE if not.
811 *
812 * Since: 2.68
813 */
814 gboolean
soup_websocket_server_process_handshake_with_extensions(SoupMessage * msg,const char * expected_origin,char ** protocols,GPtrArray * supported_extensions,GList ** accepted_extensions)815 soup_websocket_server_process_handshake_with_extensions (SoupMessage *msg,
816 const char *expected_origin,
817 char **protocols,
818 GPtrArray *supported_extensions,
819 GList **accepted_extensions)
820 {
821 const char *chosen_protocol = NULL;
822 const char *key;
823 const char *extensions;
824 char *accept_key;
825 GError *error = NULL;
826
827 g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE);
828
829 if (!soup_websocket_server_check_handshake_with_extensions (msg, expected_origin, protocols, supported_extensions, &error)) {
830 if (g_error_matches (error,
831 SOUP_WEBSOCKET_ERROR,
832 SOUP_WEBSOCKET_ERROR_BAD_ORIGIN))
833 respond_handshake_forbidden (msg);
834 else
835 respond_handshake_bad (msg, error->message);
836 g_error_free (error);
837 return FALSE;
838 }
839
840 soup_message_set_status (msg, SOUP_STATUS_SWITCHING_PROTOCOLS);
841 soup_message_headers_replace (msg->response_headers, "Upgrade", "websocket");
842 soup_message_headers_append (msg->response_headers, "Connection", "Upgrade");
843
844 key = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key");
845 accept_key = compute_accept_key (key);
846 soup_message_headers_append (msg->response_headers, "Sec-WebSocket-Accept", accept_key);
847 g_free (accept_key);
848
849 choose_subprotocol (msg, (const char **) protocols, &chosen_protocol);
850 if (chosen_protocol)
851 soup_message_headers_append (msg->response_headers, "Sec-WebSocket-Protocol", chosen_protocol);
852
853 extensions = soup_message_headers_get_list (msg->request_headers, "Sec-WebSocket-Extensions");
854 if (extensions && *extensions) {
855 GList *websocket_extensions = NULL;
856 GList *l;
857
858 process_extensions (msg, extensions, TRUE, supported_extensions, &websocket_extensions, NULL);
859 if (websocket_extensions) {
860 GString *response_extensions;
861
862 response_extensions = g_string_new (NULL);
863
864 for (l = websocket_extensions; l && l->data; l = g_list_next (l)) {
865 SoupWebsocketExtension *websocket_extension;
866 gchar *params;
867
868 websocket_extension = (SoupWebsocketExtension *)l->data;
869 if (response_extensions->len > 0)
870 response_extensions = g_string_append (response_extensions, ", ");
871 response_extensions = g_string_append (response_extensions, SOUP_WEBSOCKET_EXTENSION_GET_CLASS (websocket_extension)->name);
872 params = soup_websocket_extension_get_response_params (websocket_extension);
873 if (params) {
874 response_extensions = g_string_append (response_extensions, params);
875 g_free (params);
876 }
877 }
878
879 if (response_extensions->len > 0) {
880 soup_message_headers_replace (msg->response_headers,
881 "Sec-WebSocket-Extensions",
882 response_extensions->str);
883 } else {
884 soup_message_headers_remove (msg->response_headers,
885 "Sec-WebSocket-Extensions");
886 }
887 g_string_free (response_extensions, TRUE);
888
889 if (accepted_extensions)
890 *accepted_extensions = websocket_extensions;
891 else
892 g_list_free_full (websocket_extensions, g_object_unref);
893 }
894 }
895
896 return TRUE;
897 }
898
899 /**
900 * soup_websocket_client_verify_handshake:
901 * @msg: #SoupMessage containing both client and server sides of a
902 * WebSocket handshake
903 * @error: return location for a #GError
904 *
905 * Looks at the response status code and headers in @msg and
906 * determines if they contain a valid WebSocket handshake response
907 * (given the handshake request in @msg's request headers).
908 *
909 * If the response contains the "Sec-WebSocket-Extensions" header,
910 * the handshake will be considered invalid. You need to use
911 * soup_websocket_client_verify_handshake_with_extensions() to handle
912 * responses with extensions.
913 *
914 * This is a low-level function; if you use
915 * soup_session_websocket_connect_async() to create a WebSocket
916 * connection, it will call this for you.
917 *
918 * Returns: %TRUE if @msg contains a completed valid WebSocket
919 * handshake, %FALSE and an error if not.
920 *
921 * Since: 2.50
922 */
923 gboolean
soup_websocket_client_verify_handshake(SoupMessage * msg,GError ** error)924 soup_websocket_client_verify_handshake (SoupMessage *msg,
925 GError **error)
926 {
927 return soup_websocket_client_verify_handshake_with_extensions (msg, NULL, NULL, error);
928 }
929
930 /**
931 * soup_websocket_client_verify_handshake_with_extensions:
932 * @msg: #SoupMessage containing both client and server sides of a
933 * WebSocket handshake
934 * @supported_extensions: (nullable) (element-type GObject.TypeClass): list
935 * of supported extension types
936 * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a
937 * #GList of #SoupWebsocketExtension objects
938 * @error: return location for a #GError
939 *
940 * Looks at the response status code and headers in @msg and
941 * determines if they contain a valid WebSocket handshake response
942 * (given the handshake request in @msg's request headers).
943 *
944 * If @supported_extensions is non-%NULL, extensions included in the
945 * response "Sec-WebSocket-Extensions" are verified too. Accepted
946 * extensions are returned in @accepted_extensions parameter if non-%NULL.
947 *
948 * This is a low-level function; if you use
949 * soup_session_websocket_connect_async() to create a WebSocket
950 * connection, it will call this for you.
951 *
952 * Returns: %TRUE if @msg contains a completed valid WebSocket
953 * handshake, %FALSE and an error if not.
954 *
955 * Since: 2.68
956 */
957 gboolean
soup_websocket_client_verify_handshake_with_extensions(SoupMessage * msg,GPtrArray * supported_extensions,GList ** accepted_extensions,GError ** error)958 soup_websocket_client_verify_handshake_with_extensions (SoupMessage *msg,
959 GPtrArray *supported_extensions,
960 GList **accepted_extensions,
961 GError **error)
962 {
963 const char *protocol, *request_protocols, *extensions, *accept_key;
964 char *expected_accept_key;
965 gboolean key_ok;
966
967 g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE);
968 g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE);
969 g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
970
971 if (msg->status_code == SOUP_STATUS_BAD_REQUEST) {
972 g_set_error_literal (error,
973 SOUP_WEBSOCKET_ERROR,
974 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
975 _("Server rejected WebSocket handshake"));
976 return FALSE;
977 }
978
979 if (msg->status_code != SOUP_STATUS_SWITCHING_PROTOCOLS) {
980 g_set_error_literal (error,
981 SOUP_WEBSOCKET_ERROR,
982 SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
983 _("Server ignored WebSocket handshake"));
984 return FALSE;
985 }
986
987 if (!soup_message_headers_header_equals (msg->response_headers, "Upgrade", "websocket") ||
988 !soup_message_headers_header_contains (msg->response_headers, "Connection", "upgrade")) {
989 g_set_error_literal (error,
990 SOUP_WEBSOCKET_ERROR,
991 SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET,
992 _("Server ignored WebSocket handshake"));
993 return FALSE;
994 }
995
996 protocol = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Protocol");
997 if (protocol) {
998 request_protocols = soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Protocol");
999 if (!request_protocols ||
1000 !soup_header_contains (request_protocols, protocol)) {
1001 g_set_error_literal (error,
1002 SOUP_WEBSOCKET_ERROR,
1003 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
1004 _("Server requested unsupported protocol"));
1005 return FALSE;
1006 }
1007 }
1008
1009 extensions = soup_message_headers_get_list (msg->response_headers, "Sec-WebSocket-Extensions");
1010 if (extensions && *extensions) {
1011 if (!process_extensions (msg, extensions, FALSE, supported_extensions, accepted_extensions, error))
1012 return FALSE;
1013 }
1014
1015 accept_key = soup_message_headers_get_one (msg->response_headers, "Sec-WebSocket-Accept");
1016 expected_accept_key = compute_accept_key (soup_message_headers_get_one (msg->request_headers, "Sec-WebSocket-Key"));
1017 key_ok = (accept_key && expected_accept_key &&
1018 !g_ascii_strcasecmp (accept_key, expected_accept_key));
1019 g_free (expected_accept_key);
1020 if (!key_ok) {
1021 g_set_error (error,
1022 SOUP_WEBSOCKET_ERROR,
1023 SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
1024 _("Server returned incorrect “%s” key"),
1025 "Sec-WebSocket-Accept");
1026 return FALSE;
1027 }
1028
1029 return TRUE;
1030 }
1031