• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 (&params, 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 (&params, 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