/src/libsoup/libsoup/websocket/soup-websocket.c
Line | Count | Source |
1 | | /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; 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-message-headers-private.h" |
31 | | #include "soup-server-message.h" |
32 | | #include "soup-websocket-extension.h" |
33 | | |
34 | 0 | #define FIXED_DIGEST_LEN 20 |
35 | | |
36 | | /** |
37 | | * SoupWebsocketConnection: |
38 | | * |
39 | | * The WebSocket Protocol |
40 | | * |
41 | | * Provides support for the [WebSocket](http://tools.ietf.org/html/rfc6455) |
42 | | * protocol. |
43 | | * |
44 | | * To connect to a WebSocket server, create a [class@Session] and call |
45 | | * [method@Session.websocket_connect_async]. To accept WebSocket |
46 | | * connections, create a [class@Server] and add a handler to it with |
47 | | * [method@Server.add_websocket_handler]. |
48 | | * |
49 | | * (Lower-level support is available via |
50 | | * [func@websocket_client_prepare_handshake] and |
51 | | * [func@websocket_client_verify_handshake], for handling the client side of the |
52 | | * WebSocket handshake, and [func@websocket_server_process_handshake] for |
53 | | * handling the server side.) |
54 | | * |
55 | | * [class@WebsocketConnection] handles the details of WebSocket communication. You |
56 | | * can use [method@WebsocketConnection.send_text] and |
57 | | * [method@WebsocketConnection.send_binary] to send data, and the |
58 | | * [signal@WebsocketConnection::message] signal to receive data. |
59 | | * ([class@WebsocketConnection] currently only supports asynchronous I/O.) |
60 | | */ |
61 | | |
62 | | /** |
63 | | * SOUP_WEBSOCKET_ERROR: |
64 | | * |
65 | | * A [struct@GLib.Error] domain for WebSocket-related errors. |
66 | | * |
67 | | * Used with [error@WebsocketError]. |
68 | | */ |
69 | | |
70 | | /** |
71 | | * SoupWebsocketError: |
72 | | * @SOUP_WEBSOCKET_ERROR_FAILED: a generic error |
73 | | * @SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET: attempted to handshake with a |
74 | | * server that does not appear to understand WebSockets. |
75 | | * @SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE: the WebSocket handshake failed |
76 | | * because some detail was invalid (eg, incorrect accept key). |
77 | | * @SOUP_WEBSOCKET_ERROR_BAD_ORIGIN: the WebSocket handshake failed |
78 | | * because the "Origin" header was not an allowed value. |
79 | | * |
80 | | * WebSocket-related errors. |
81 | | */ |
82 | | |
83 | | /** |
84 | | * SoupWebsocketConnectionType: |
85 | | * @SOUP_WEBSOCKET_CONNECTION_UNKNOWN: unknown/invalid connection |
86 | | * @SOUP_WEBSOCKET_CONNECTION_CLIENT: a client-side connection |
87 | | * @SOUP_WEBSOCKET_CONNECTION_SERVER: a server-side connection |
88 | | * |
89 | | * The type of a [class@WebsocketConnection]. |
90 | | */ |
91 | | |
92 | | /** |
93 | | * SoupWebsocketDataType: |
94 | | * @SOUP_WEBSOCKET_DATA_TEXT: UTF-8 text |
95 | | * @SOUP_WEBSOCKET_DATA_BINARY: binary data |
96 | | * |
97 | | * The type of data contained in a [signal@WebsocketConnection::message] signal. |
98 | | */ |
99 | | |
100 | | /** |
101 | | * SoupWebsocketCloseCode: |
102 | | * @SOUP_WEBSOCKET_CLOSE_NORMAL: a normal, non-error close |
103 | | * @SOUP_WEBSOCKET_CLOSE_GOING_AWAY: the client/server is going away |
104 | | * @SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR: a protocol error occurred |
105 | | * @SOUP_WEBSOCKET_CLOSE_UNSUPPORTED_DATA: the endpoint received data |
106 | | * of a type that it does not support. |
107 | | * @SOUP_WEBSOCKET_CLOSE_NO_STATUS: reserved value indicating that |
108 | | * no close code was present; must not be sent. |
109 | | * @SOUP_WEBSOCKET_CLOSE_ABNORMAL: reserved value indicating that |
110 | | * the connection was closed abnormally; must not be sent. |
111 | | * @SOUP_WEBSOCKET_CLOSE_BAD_DATA: the endpoint received data that |
112 | | * was invalid (eg, non-UTF-8 data in a text message). |
113 | | * @SOUP_WEBSOCKET_CLOSE_POLICY_VIOLATION: generic error code |
114 | | * indicating some sort of policy violation. |
115 | | * @SOUP_WEBSOCKET_CLOSE_TOO_BIG: the endpoint received a message |
116 | | * that is too big to process. |
117 | | * @SOUP_WEBSOCKET_CLOSE_NO_EXTENSION: the client is closing the |
118 | | * connection because the server failed to negotiate a required |
119 | | * extension. |
120 | | * @SOUP_WEBSOCKET_CLOSE_SERVER_ERROR: the server is closing the |
121 | | * connection because it was unable to fulfill the request. |
122 | | * @SOUP_WEBSOCKET_CLOSE_TLS_HANDSHAKE: reserved value indicating that |
123 | | * the TLS handshake failed; must not be sent. |
124 | | * |
125 | | * Pre-defined close codes that can be passed to |
126 | | * [method@WebsocketConnection.close] or received from |
127 | | * [method@WebsocketConnection.get_close_code]. |
128 | | * |
129 | | * However, other codes are also allowed. |
130 | | */ |
131 | | |
132 | | /** |
133 | | * SoupWebsocketState: |
134 | | * @SOUP_WEBSOCKET_STATE_OPEN: the connection is ready to send messages |
135 | | * @SOUP_WEBSOCKET_STATE_CLOSING: the connection is in the process of |
136 | | * closing down; messages may be received, but not sent |
137 | | * @SOUP_WEBSOCKET_STATE_CLOSED: the connection is completely closed down |
138 | | * |
139 | | * The state of the WebSocket connection. |
140 | | */ |
141 | | |
142 | | G_DEFINE_QUARK (soup-websocket-error-quark, soup_websocket_error) |
143 | | |
144 | | static gboolean |
145 | | validate_key (const char *key) |
146 | 0 | { |
147 | 0 | guchar buf[18]; |
148 | 0 | int state = 0; |
149 | 0 | guint save = 0; |
150 | | |
151 | | /* The spec requires us to check that the key is "a |
152 | | * base64-encoded value that, when decoded, is 16 bytes in |
153 | | * length". |
154 | | */ |
155 | 0 | if (strlen (key) != 24) |
156 | 0 | return FALSE; |
157 | 0 | if (g_base64_decode_step (key, 24, buf, &state, &save) != 16) |
158 | 0 | return FALSE; |
159 | 0 | return TRUE; |
160 | 0 | } |
161 | | |
162 | | static char * |
163 | | compute_accept_key (const char *key) |
164 | 0 | { |
165 | 0 | gsize digest_len = FIXED_DIGEST_LEN; |
166 | 0 | guchar digest[FIXED_DIGEST_LEN]; |
167 | 0 | GChecksum *checksum; |
168 | |
|
169 | 0 | if (!key) |
170 | 0 | return NULL; |
171 | | |
172 | 0 | checksum = g_checksum_new (G_CHECKSUM_SHA1); |
173 | 0 | g_return_val_if_fail (checksum != NULL, NULL); |
174 | | |
175 | 0 | g_checksum_update (checksum, (guchar *)key, -1); |
176 | | |
177 | | /* magic from: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 */ |
178 | 0 | g_checksum_update (checksum, (guchar *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", -1); |
179 | |
|
180 | 0 | g_checksum_get_digest (checksum, digest, &digest_len); |
181 | 0 | g_checksum_free (checksum); |
182 | |
|
183 | 0 | g_assert (digest_len == FIXED_DIGEST_LEN); |
184 | | |
185 | 0 | return g_base64_encode (digest, digest_len); |
186 | 0 | } |
187 | | |
188 | | static gboolean |
189 | | choose_subprotocol (SoupServerMessage *msg, |
190 | | const char **server_protocols, |
191 | | const char **chosen_protocol) |
192 | 0 | { |
193 | 0 | const char *client_protocols_str; |
194 | 0 | char **client_protocols; |
195 | 0 | int i, j; |
196 | |
|
197 | 0 | if (chosen_protocol) |
198 | 0 | *chosen_protocol = NULL; |
199 | |
|
200 | 0 | if (!server_protocols) |
201 | 0 | return TRUE; |
202 | | |
203 | 0 | client_protocols_str = |
204 | 0 | soup_message_headers_get_one_common (soup_server_message_get_request_headers (msg), |
205 | 0 | SOUP_HEADER_SEC_WEBSOCKET_PROTOCOL); |
206 | 0 | if (!client_protocols_str) |
207 | 0 | return TRUE; |
208 | | |
209 | 0 | client_protocols = g_strsplit_set (client_protocols_str, ", ", -1); |
210 | 0 | if (!client_protocols || !client_protocols[0]) { |
211 | 0 | g_strfreev (client_protocols); |
212 | 0 | return FALSE; |
213 | 0 | } |
214 | | |
215 | 0 | for (i = 0; server_protocols[i] != NULL; i++) { |
216 | 0 | for (j = 0; client_protocols[j] != NULL; j++) { |
217 | 0 | if (g_str_equal (server_protocols[i], client_protocols[j])) { |
218 | 0 | g_strfreev (client_protocols); |
219 | 0 | if (chosen_protocol) |
220 | 0 | *chosen_protocol = server_protocols[i]; |
221 | 0 | return TRUE; |
222 | 0 | } |
223 | 0 | } |
224 | 0 | } |
225 | | |
226 | 0 | g_strfreev (client_protocols); |
227 | 0 | return FALSE; |
228 | 0 | } |
229 | | |
230 | | /** |
231 | | * soup_websocket_client_prepare_handshake: |
232 | | * @msg: a #SoupMessage |
233 | | * @origin: (nullable): the "Origin" header to set |
234 | | * @protocols: (nullable) (array zero-terminated=1): list of |
235 | | * protocols to offer |
236 | | * @supported_extensions: (nullable) (element-type GObject.TypeClass): list |
237 | | * of supported extension types |
238 | | * |
239 | | * Adds the necessary headers to @msg to request a WebSocket |
240 | | * handshake including supported WebSocket extensions. |
241 | | * |
242 | | * The message body and non-WebSocket-related headers are |
243 | | * not modified. |
244 | | * |
245 | | * This is a low-level function; if you use |
246 | | * [method@Session.websocket_connect_async] to create a WebSocket connection, it |
247 | | * will call this for you. |
248 | | */ |
249 | | void |
250 | | soup_websocket_client_prepare_handshake (SoupMessage *msg, |
251 | | const char *origin, |
252 | | char **protocols, |
253 | | GPtrArray *supported_extensions) |
254 | 0 | { |
255 | 0 | guint32 raw[4]; |
256 | 0 | char *key; |
257 | |
|
258 | 0 | g_return_if_fail (SOUP_IS_MESSAGE (msg)); |
259 | | |
260 | 0 | soup_message_headers_replace_common (soup_message_get_request_headers (msg), SOUP_HEADER_UPGRADE, "websocket", SOUP_HEADER_VALUE_TRUSTED); |
261 | 0 | soup_message_headers_append_common (soup_message_get_request_headers (msg), SOUP_HEADER_CONNECTION, "Upgrade", SOUP_HEADER_VALUE_TRUSTED); |
262 | |
|
263 | 0 | raw[0] = g_random_int (); |
264 | 0 | raw[1] = g_random_int (); |
265 | 0 | raw[2] = g_random_int (); |
266 | 0 | raw[3] = g_random_int (); |
267 | 0 | key = g_base64_encode ((const guchar *)raw, sizeof (raw)); |
268 | 0 | soup_message_headers_replace_common (soup_message_get_request_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_KEY, key, SOUP_HEADER_VALUE_TRUSTED); |
269 | 0 | g_free (key); |
270 | |
|
271 | 0 | soup_message_headers_replace_common (soup_message_get_request_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_VERSION, "13", SOUP_HEADER_VALUE_TRUSTED); |
272 | |
|
273 | 0 | if (origin) |
274 | 0 | soup_message_headers_replace_common (soup_message_get_request_headers (msg), SOUP_HEADER_ORIGIN, origin, SOUP_HEADER_VALUE_UNTRUSTED); |
275 | |
|
276 | 0 | if (protocols && *protocols) { |
277 | 0 | char *protocols_str; |
278 | |
|
279 | 0 | protocols_str = g_strjoinv (", ", protocols); |
280 | 0 | if (*protocols_str) |
281 | 0 | soup_message_headers_replace_common (soup_message_get_request_headers (msg), |
282 | 0 | SOUP_HEADER_SEC_WEBSOCKET_PROTOCOL, protocols_str, SOUP_HEADER_VALUE_TRUSTED); |
283 | 0 | g_free (protocols_str); |
284 | 0 | } |
285 | |
|
286 | 0 | if (supported_extensions && supported_extensions->len > 0) { |
287 | 0 | guint i; |
288 | 0 | GString *extensions; |
289 | |
|
290 | 0 | extensions = g_string_new (NULL); |
291 | |
|
292 | 0 | for (i = 0; i < supported_extensions->len; i++) { |
293 | 0 | SoupWebsocketExtensionClass *extension_class = (SoupWebsocketExtensionClass *)supported_extensions->pdata[i]; |
294 | |
|
295 | 0 | if (soup_message_is_feature_disabled (msg, G_TYPE_FROM_CLASS (extension_class))) |
296 | 0 | continue; |
297 | | |
298 | 0 | if (i != 0) |
299 | 0 | extensions = g_string_append (extensions, ", "); |
300 | 0 | extensions = g_string_append (extensions, extension_class->name); |
301 | |
|
302 | 0 | if (extension_class->get_request_params) { |
303 | 0 | SoupWebsocketExtension *websocket_extension; |
304 | 0 | gchar *params; |
305 | |
|
306 | 0 | websocket_extension = g_object_new (G_TYPE_FROM_CLASS (extension_class), NULL); |
307 | 0 | params = soup_websocket_extension_get_request_params (websocket_extension); |
308 | 0 | if (params) { |
309 | 0 | extensions = g_string_append (extensions, params); |
310 | 0 | g_free (params); |
311 | 0 | } |
312 | 0 | g_object_unref (websocket_extension); |
313 | 0 | } |
314 | 0 | } |
315 | |
|
316 | 0 | if (extensions->len > 0) { |
317 | 0 | soup_message_headers_replace_common (soup_message_get_request_headers (msg), |
318 | 0 | SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS, |
319 | 0 | extensions->str, SOUP_HEADER_VALUE_UNTRUSTED); |
320 | 0 | } else { |
321 | 0 | soup_message_headers_remove_common (soup_message_get_request_headers (msg), |
322 | 0 | SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS); |
323 | 0 | } |
324 | 0 | g_string_free (extensions, TRUE); |
325 | 0 | } |
326 | 0 | } |
327 | | |
328 | | static gboolean |
329 | | websocket_extension_class_equal (gconstpointer a, |
330 | | gconstpointer b) |
331 | 0 | { |
332 | 0 | return g_str_equal (((const SoupWebsocketExtensionClass *)a)->name, (const char *)b); |
333 | 0 | } |
334 | | |
335 | | static GHashTable * |
336 | | extract_extension_names_from_request (SoupMessage *msg) |
337 | 0 | { |
338 | 0 | const char *extensions; |
339 | 0 | GSList *extension_list, *l; |
340 | 0 | GHashTable *return_value = NULL; |
341 | |
|
342 | 0 | extensions = soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS); |
343 | 0 | if (!extensions || !*extensions) |
344 | 0 | return NULL; |
345 | | |
346 | 0 | extension_list = soup_header_parse_list (extensions); |
347 | 0 | for (l = extension_list; l != NULL; l = g_slist_next (l)) { |
348 | 0 | char *extension = (char *)l->data; |
349 | 0 | char *p, *end; |
350 | |
|
351 | 0 | while (g_ascii_isspace (*extension)) |
352 | 0 | extension++; |
353 | |
|
354 | 0 | if (!*extension) |
355 | 0 | continue; |
356 | | |
357 | 0 | p = strstr (extension, ";"); |
358 | 0 | end = p ? p : extension + strlen (extension); |
359 | 0 | while (end > extension && g_ascii_isspace (*(end - 1))) |
360 | 0 | end--; |
361 | 0 | *end = '\0'; |
362 | |
|
363 | 0 | if (!return_value) |
364 | 0 | return_value = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); |
365 | 0 | g_hash_table_add (return_value, g_strdup (extension)); |
366 | 0 | } |
367 | |
|
368 | 0 | soup_header_free_list (extension_list); |
369 | |
|
370 | 0 | return return_value; |
371 | 0 | } |
372 | | |
373 | | static gboolean |
374 | | process_extensions (const char *extensions, |
375 | | SoupMessage *msg, |
376 | | GPtrArray *supported_extensions, |
377 | | GList **accepted_extensions, |
378 | | GError **error) |
379 | 0 | { |
380 | 0 | GSList *extension_list, *l; |
381 | 0 | GHashTable *requested_extensions = NULL; |
382 | 0 | gboolean is_server = msg == NULL; |
383 | |
|
384 | 0 | if (!supported_extensions || supported_extensions->len == 0) { |
385 | 0 | if (is_server) |
386 | 0 | return TRUE; |
387 | | |
388 | 0 | g_set_error_literal (error, |
389 | 0 | SOUP_WEBSOCKET_ERROR, |
390 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
391 | 0 | _("Server requested unsupported extension")); |
392 | 0 | return FALSE; |
393 | 0 | } |
394 | | |
395 | 0 | if (!is_server) |
396 | 0 | requested_extensions = extract_extension_names_from_request (msg); |
397 | |
|
398 | 0 | extension_list = soup_header_parse_list (extensions); |
399 | 0 | for (l = extension_list; l != NULL; l = g_slist_next (l)) { |
400 | 0 | char *extension = (char *)l->data; |
401 | 0 | char *p, *end; |
402 | 0 | guint index; |
403 | 0 | GHashTable *params = NULL; |
404 | 0 | SoupWebsocketExtension *websocket_extension; |
405 | |
|
406 | 0 | while (g_ascii_isspace (*extension)) |
407 | 0 | extension++; |
408 | |
|
409 | 0 | if (!*extension) { |
410 | 0 | g_set_error (error, |
411 | 0 | SOUP_WEBSOCKET_ERROR, |
412 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
413 | 0 | is_server ? |
414 | 0 | _("Incorrect WebSocket ā%sā header") : |
415 | 0 | _("Server returned incorrect ā%sā key"), |
416 | 0 | "Sec-WebSocket-Extensions"); |
417 | 0 | if (accepted_extensions) |
418 | 0 | g_clear_list (accepted_extensions, g_object_unref); |
419 | 0 | g_clear_pointer (&requested_extensions, g_hash_table_destroy); |
420 | 0 | soup_header_free_list (extension_list); |
421 | |
|
422 | 0 | return FALSE; |
423 | 0 | } |
424 | | |
425 | 0 | p = strstr (extension, ";"); |
426 | 0 | end = p ? p : extension + strlen (extension); |
427 | 0 | while (end > extension && g_ascii_isspace (*(end - 1))) |
428 | 0 | end--; |
429 | 0 | *end = '\0'; |
430 | |
|
431 | 0 | if (requested_extensions && !g_hash_table_contains (requested_extensions, extension)) { |
432 | 0 | g_set_error_literal (error, |
433 | 0 | SOUP_WEBSOCKET_ERROR, |
434 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
435 | 0 | _("Server requested unsupported extension")); |
436 | 0 | if (accepted_extensions) |
437 | 0 | g_clear_list (accepted_extensions, g_object_unref); |
438 | 0 | g_clear_pointer (&requested_extensions, g_hash_table_destroy); |
439 | 0 | soup_header_free_list (extension_list); |
440 | |
|
441 | 0 | return FALSE; |
442 | 0 | } |
443 | | |
444 | 0 | if (!g_ptr_array_find_with_equal_func (supported_extensions, extension, websocket_extension_class_equal, &index)) { |
445 | 0 | if (is_server) |
446 | 0 | continue; |
447 | | |
448 | 0 | g_set_error_literal (error, |
449 | 0 | SOUP_WEBSOCKET_ERROR, |
450 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
451 | 0 | _("Server requested unsupported extension")); |
452 | 0 | if (accepted_extensions) |
453 | 0 | g_clear_list (accepted_extensions, g_object_unref); |
454 | 0 | g_clear_pointer (&requested_extensions, g_hash_table_destroy); |
455 | 0 | soup_header_free_list (extension_list); |
456 | |
|
457 | 0 | return FALSE; |
458 | 0 | } |
459 | | |
460 | | /* If we are just checking headers in server side |
461 | | * and there's no parameters, it's enough to know |
462 | | * the extension is supported. |
463 | | */ |
464 | 0 | if (is_server && !accepted_extensions && !p) |
465 | 0 | continue; |
466 | | |
467 | 0 | websocket_extension = g_object_new (G_TYPE_FROM_CLASS (supported_extensions->pdata[index]), NULL); |
468 | 0 | if (accepted_extensions) |
469 | 0 | *accepted_extensions = g_list_prepend (*accepted_extensions, websocket_extension); |
470 | |
|
471 | 0 | if (p) { |
472 | 0 | params = soup_header_parse_semi_param_list_strict (p + 1); |
473 | 0 | if (!params) { |
474 | 0 | g_set_error (error, |
475 | 0 | SOUP_WEBSOCKET_ERROR, |
476 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
477 | 0 | is_server ? |
478 | 0 | _("Duplicated parameter in ā%sā WebSocket extension header") : |
479 | 0 | _("Server returned a duplicated parameter in ā%sā WebSocket extension header"), |
480 | 0 | extension); |
481 | 0 | if (accepted_extensions) |
482 | 0 | g_clear_list (accepted_extensions, g_object_unref); |
483 | 0 | else |
484 | 0 | g_object_unref (websocket_extension); |
485 | 0 | g_clear_pointer (&requested_extensions, g_hash_table_destroy); |
486 | 0 | soup_header_free_list (extension_list); |
487 | |
|
488 | 0 | return FALSE; |
489 | 0 | } |
490 | 0 | } |
491 | | |
492 | 0 | if (!soup_websocket_extension_configure (websocket_extension, |
493 | 0 | is_server ? SOUP_WEBSOCKET_CONNECTION_SERVER : SOUP_WEBSOCKET_CONNECTION_CLIENT, |
494 | 0 | params, |
495 | 0 | error)) { |
496 | 0 | g_clear_pointer (¶ms, g_hash_table_destroy); |
497 | 0 | if (accepted_extensions) |
498 | 0 | g_clear_list (accepted_extensions, g_object_unref); |
499 | 0 | else |
500 | 0 | g_object_unref (websocket_extension); |
501 | 0 | g_clear_pointer (&requested_extensions, g_hash_table_destroy); |
502 | 0 | soup_header_free_list (extension_list); |
503 | |
|
504 | 0 | return FALSE; |
505 | 0 | } |
506 | 0 | g_clear_pointer (¶ms, g_hash_table_destroy); |
507 | 0 | if (!accepted_extensions) |
508 | 0 | g_object_unref (websocket_extension); |
509 | 0 | } |
510 | | |
511 | 0 | soup_header_free_list (extension_list); |
512 | 0 | g_clear_pointer (&requested_extensions, g_hash_table_destroy); |
513 | |
|
514 | 0 | if (accepted_extensions) |
515 | 0 | *accepted_extensions = g_list_reverse (*accepted_extensions); |
516 | |
|
517 | 0 | return TRUE; |
518 | 0 | } |
519 | | |
520 | | /** |
521 | | * soup_websocket_server_check_handshake: |
522 | | * @msg: #SoupServerMessage containing the client side of a WebSocket handshake |
523 | | * @origin: (nullable): expected Origin header |
524 | | * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket |
525 | | * protocols. |
526 | | * @supported_extensions: (nullable) (element-type GObject.TypeClass): list |
527 | | * of supported extension types |
528 | | * @error: return location for a #GError |
529 | | * |
530 | | * Examines the method and request headers in @msg and determines |
531 | | * whether @msg contains a valid handshake request. |
532 | | * |
533 | | * If @origin is non-%NULL, then only requests containing a matching |
534 | | * "Origin" header will be accepted. If @protocols is non-%NULL, then |
535 | | * only requests containing a compatible "Sec-WebSocket-Protocols" |
536 | | * header will be accepted. If @supported_extensions is non-%NULL, then |
537 | | * only requests containing valid supported extensions in |
538 | | * "Sec-WebSocket-Extensions" header will be accepted. |
539 | | * |
540 | | * Normally [func@websocket_server_process_handshake] |
541 | | * will take care of this for you, and if you use |
542 | | * [method@Server.add_websocket_handler] to handle accepting WebSocket |
543 | | * connections, it will call that for you. However, this function may |
544 | | * be useful if you need to perform more complicated validation; eg, |
545 | | * accepting multiple different Origins, or handling different protocols |
546 | | * depending on the path. |
547 | | * |
548 | | * Returns: %TRUE if @msg contained a valid WebSocket handshake, |
549 | | * %FALSE and an error if not. |
550 | | */ |
551 | | gboolean |
552 | | soup_websocket_server_check_handshake (SoupServerMessage *msg, |
553 | | const char *expected_origin, |
554 | | char **protocols, |
555 | | GPtrArray *supported_extensions, |
556 | | GError **error) |
557 | 0 | { |
558 | 0 | const char *origin; |
559 | 0 | const char *key; |
560 | 0 | const char *extensions; |
561 | 0 | SoupMessageHeaders *request_headers; |
562 | |
|
563 | 0 | g_return_val_if_fail (SOUP_IS_SERVER_MESSAGE (msg), FALSE); |
564 | | |
565 | 0 | if (soup_server_message_get_method (msg) != SOUP_METHOD_GET) { |
566 | 0 | g_set_error_literal (error, |
567 | 0 | SOUP_WEBSOCKET_ERROR, |
568 | 0 | SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, |
569 | 0 | _("WebSocket handshake expected")); |
570 | 0 | return FALSE; |
571 | 0 | } |
572 | | |
573 | 0 | request_headers = soup_server_message_get_request_headers (msg); |
574 | 0 | if (!soup_message_headers_header_equals_common (request_headers, SOUP_HEADER_UPGRADE, "websocket") || |
575 | 0 | !soup_message_headers_header_contains_common (request_headers, SOUP_HEADER_CONNECTION, "upgrade")) { |
576 | 0 | g_set_error_literal (error, |
577 | 0 | SOUP_WEBSOCKET_ERROR, |
578 | 0 | SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, |
579 | 0 | _("WebSocket handshake expected")); |
580 | 0 | return FALSE; |
581 | 0 | } |
582 | | |
583 | 0 | if (!soup_message_headers_header_equals_common (request_headers, SOUP_HEADER_SEC_WEBSOCKET_VERSION, "13")) { |
584 | 0 | g_set_error_literal (error, |
585 | 0 | SOUP_WEBSOCKET_ERROR, |
586 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
587 | 0 | _("Unsupported WebSocket version")); |
588 | 0 | return FALSE; |
589 | 0 | } |
590 | | |
591 | 0 | key = soup_message_headers_get_one_common (request_headers, SOUP_HEADER_SEC_WEBSOCKET_KEY); |
592 | 0 | if (key == NULL || !validate_key (key)) { |
593 | 0 | g_set_error_literal (error, |
594 | 0 | SOUP_WEBSOCKET_ERROR, |
595 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
596 | 0 | _("Invalid WebSocket key")); |
597 | 0 | return FALSE; |
598 | 0 | } |
599 | | |
600 | 0 | if (expected_origin) { |
601 | 0 | origin = soup_message_headers_get_one_common (request_headers, SOUP_HEADER_ORIGIN); |
602 | 0 | if (!origin || g_ascii_strcasecmp (origin, expected_origin) != 0) { |
603 | 0 | g_set_error (error, |
604 | 0 | SOUP_WEBSOCKET_ERROR, |
605 | 0 | SOUP_WEBSOCKET_ERROR_BAD_ORIGIN, |
606 | 0 | _("Incorrect WebSocket ā%sā header"), "Origin"); |
607 | 0 | return FALSE; |
608 | 0 | } |
609 | 0 | } |
610 | | |
611 | 0 | if (!choose_subprotocol (msg, (const char **) protocols, NULL)) { |
612 | 0 | g_set_error_literal (error, |
613 | 0 | SOUP_WEBSOCKET_ERROR, |
614 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
615 | 0 | _("Unsupported WebSocket subprotocol")); |
616 | 0 | return FALSE; |
617 | 0 | } |
618 | | |
619 | 0 | extensions = soup_message_headers_get_list_common (request_headers, SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS); |
620 | 0 | if (extensions && *extensions) { |
621 | 0 | if (!process_extensions (extensions, NULL, supported_extensions, NULL, error)) |
622 | 0 | return FALSE; |
623 | 0 | } |
624 | | |
625 | 0 | return TRUE; |
626 | 0 | } |
627 | | |
628 | 0 | #define RESPONSE_FORBIDDEN "<html><head><title>400 Forbidden</title></head>\r\n" \ |
629 | 0 | "<body>Received invalid WebSocket request</body></html>\r\n" |
630 | | |
631 | | static void |
632 | | respond_handshake_forbidden (SoupServerMessage *msg) |
633 | 0 | { |
634 | 0 | soup_server_message_set_status (msg, SOUP_STATUS_FORBIDDEN, NULL); |
635 | 0 | soup_message_headers_append_common (soup_server_message_get_response_headers (msg), |
636 | 0 | SOUP_HEADER_CONNECTION, "close", SOUP_HEADER_VALUE_TRUSTED); |
637 | 0 | soup_server_message_set_response (msg, "text/html", SOUP_MEMORY_COPY, |
638 | 0 | RESPONSE_FORBIDDEN, strlen (RESPONSE_FORBIDDEN)); |
639 | 0 | } |
640 | | |
641 | 0 | #define RESPONSE_BAD "<html><head><title>400 Bad Request</title></head>\r\n" \ |
642 | 0 | "<body>Received invalid WebSocket request: %s</body></html>\r\n" |
643 | | |
644 | | static void |
645 | | respond_handshake_bad (SoupServerMessage *msg, |
646 | | const char *why) |
647 | 0 | { |
648 | 0 | char *text; |
649 | |
|
650 | 0 | text = g_strdup_printf (RESPONSE_BAD, why); |
651 | 0 | soup_server_message_set_status (msg, SOUP_STATUS_BAD_REQUEST, NULL); |
652 | 0 | soup_message_headers_append_common (soup_server_message_get_response_headers (msg), |
653 | 0 | SOUP_HEADER_CONNECTION, "close", SOUP_HEADER_VALUE_TRUSTED); |
654 | 0 | soup_server_message_set_response (msg, "text/html", SOUP_MEMORY_TAKE, |
655 | 0 | text, strlen (text)); |
656 | 0 | } |
657 | | |
658 | | /** |
659 | | * soup_websocket_server_process_handshake: |
660 | | * @msg: #SoupServerMessage containing the client side of a WebSocket handshake |
661 | | * @expected_origin: (nullable): expected Origin header |
662 | | * @protocols: (nullable) (array zero-terminated=1): allowed WebSocket |
663 | | * protocols. |
664 | | * @supported_extensions: (nullable) (element-type GObject.TypeClass): list |
665 | | * of supported extension types |
666 | | * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a |
667 | | * #GList of [class@WebsocketExtension] objects |
668 | | * |
669 | | * Examines the method and request headers in @msg and (assuming @msg |
670 | | * contains a valid handshake request), fills in the handshake |
671 | | * response. |
672 | | * |
673 | | * If @expected_origin is non-%NULL, then only requests containing a matching |
674 | | * "Origin" header will be accepted. If @protocols is non-%NULL, then |
675 | | * only requests containing a compatible "Sec-WebSocket-Protocols" |
676 | | * header will be accepted. If @supported_extensions is non-%NULL, then |
677 | | * only requests containing valid supported extensions in |
678 | | * "Sec-WebSocket-Extensions" header will be accepted. The accepted extensions |
679 | | * will be returned in @accepted_extensions parameter if non-%NULL. |
680 | | * |
681 | | * This is a low-level function; if you use |
682 | | * [method@Server.add_websocket_handler] to handle accepting WebSocket |
683 | | * connections, it will call this for you. |
684 | | * |
685 | | * Returns: %TRUE if @msg contained a valid WebSocket handshake |
686 | | * request and was updated to contain a handshake response. %FALSE if not. |
687 | | */ |
688 | | gboolean |
689 | | soup_websocket_server_process_handshake (SoupServerMessage *msg, |
690 | | const char *expected_origin, |
691 | | char **protocols, |
692 | | GPtrArray *supported_extensions, |
693 | | GList **accepted_extensions) |
694 | 0 | { |
695 | 0 | const char *chosen_protocol = NULL; |
696 | 0 | const char *key; |
697 | 0 | const char *extensions; |
698 | 0 | char *accept_key; |
699 | 0 | GError *error = NULL; |
700 | 0 | SoupMessageHeaders *request_headers; |
701 | 0 | SoupMessageHeaders *response_headers; |
702 | |
|
703 | 0 | g_return_val_if_fail (SOUP_IS_SERVER_MESSAGE (msg), FALSE); |
704 | 0 | g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE); |
705 | | |
706 | 0 | if (!soup_websocket_server_check_handshake (msg, expected_origin, protocols, supported_extensions, &error)) { |
707 | 0 | if (g_error_matches (error, |
708 | 0 | SOUP_WEBSOCKET_ERROR, |
709 | 0 | SOUP_WEBSOCKET_ERROR_BAD_ORIGIN)) |
710 | 0 | respond_handshake_forbidden (msg); |
711 | 0 | else |
712 | 0 | respond_handshake_bad (msg, error->message); |
713 | 0 | g_error_free (error); |
714 | 0 | return FALSE; |
715 | 0 | } |
716 | | |
717 | 0 | soup_server_message_set_status (msg, SOUP_STATUS_SWITCHING_PROTOCOLS, NULL); |
718 | 0 | response_headers = soup_server_message_get_response_headers (msg); |
719 | 0 | soup_message_headers_replace_common (response_headers, SOUP_HEADER_UPGRADE, "websocket", SOUP_HEADER_VALUE_TRUSTED); |
720 | 0 | soup_message_headers_append_common (response_headers, SOUP_HEADER_CONNECTION, "Upgrade", SOUP_HEADER_VALUE_TRUSTED); |
721 | |
|
722 | 0 | request_headers = soup_server_message_get_request_headers (msg); |
723 | 0 | key = soup_message_headers_get_one_common (request_headers, SOUP_HEADER_SEC_WEBSOCKET_KEY); |
724 | 0 | accept_key = compute_accept_key (key); |
725 | 0 | soup_message_headers_append_common (response_headers, SOUP_HEADER_SEC_WEBSOCKET_ACCEPT, accept_key, SOUP_HEADER_VALUE_TRUSTED); |
726 | 0 | g_free (accept_key); |
727 | |
|
728 | 0 | choose_subprotocol (msg, (const char **) protocols, &chosen_protocol); |
729 | 0 | if (chosen_protocol) |
730 | 0 | soup_message_headers_append_common (response_headers, SOUP_HEADER_SEC_WEBSOCKET_PROTOCOL, chosen_protocol, SOUP_HEADER_VALUE_TRUSTED); |
731 | |
|
732 | 0 | extensions = soup_message_headers_get_list_common (request_headers, SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS); |
733 | 0 | if (extensions && *extensions) { |
734 | 0 | GList *websocket_extensions = NULL; |
735 | 0 | GList *l; |
736 | |
|
737 | 0 | process_extensions (extensions, NULL, supported_extensions, &websocket_extensions, NULL); |
738 | 0 | if (websocket_extensions) { |
739 | 0 | GString *response_extensions; |
740 | |
|
741 | 0 | response_extensions = g_string_new (NULL); |
742 | |
|
743 | 0 | for (l = websocket_extensions; l && l->data; l = g_list_next (l)) { |
744 | 0 | SoupWebsocketExtension *websocket_extension; |
745 | 0 | gchar *params; |
746 | |
|
747 | 0 | websocket_extension = (SoupWebsocketExtension *)l->data; |
748 | 0 | if (response_extensions->len > 0) |
749 | 0 | response_extensions = g_string_append (response_extensions, ", "); |
750 | 0 | response_extensions = g_string_append (response_extensions, SOUP_WEBSOCKET_EXTENSION_GET_CLASS (websocket_extension)->name); |
751 | 0 | params = soup_websocket_extension_get_response_params (websocket_extension); |
752 | 0 | if (params) { |
753 | 0 | response_extensions = g_string_append (response_extensions, params); |
754 | 0 | g_free (params); |
755 | 0 | } |
756 | 0 | } |
757 | |
|
758 | 0 | if (response_extensions->len > 0) { |
759 | 0 | soup_message_headers_replace_common (response_headers, |
760 | 0 | SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS, |
761 | 0 | response_extensions->str, SOUP_HEADER_VALUE_UNTRUSTED); |
762 | 0 | } else { |
763 | 0 | soup_message_headers_remove_common (response_headers, |
764 | 0 | SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS); |
765 | 0 | } |
766 | 0 | g_string_free (response_extensions, TRUE); |
767 | |
|
768 | 0 | if (accepted_extensions) |
769 | 0 | *accepted_extensions = websocket_extensions; |
770 | 0 | else |
771 | 0 | g_list_free_full (websocket_extensions, g_object_unref); |
772 | 0 | } |
773 | 0 | } |
774 | |
|
775 | 0 | return TRUE; |
776 | 0 | } |
777 | | |
778 | | /** |
779 | | * soup_websocket_client_verify_handshake: |
780 | | * @msg: #SoupMessage containing both client and server sides of a |
781 | | * WebSocket handshake |
782 | | * @supported_extensions: (nullable) (element-type GObject.TypeClass): list |
783 | | * of supported extension types |
784 | | * @accepted_extensions: (out) (optional) (element-type SoupWebsocketExtension): a |
785 | | * #GList of [class@WebsocketExtension] objects |
786 | | * @error: return location for a #GError |
787 | | * |
788 | | * Looks at the response status code and headers in @msg and |
789 | | * determines if they contain a valid WebSocket handshake response |
790 | | * (given the handshake request in @msg's request headers). |
791 | | * |
792 | | * If @supported_extensions is non-%NULL, extensions included in the |
793 | | * response "Sec-WebSocket-Extensions" are verified too. Accepted |
794 | | * extensions are returned in @accepted_extensions parameter if non-%NULL. |
795 | | * |
796 | | * This is a low-level function; if you use |
797 | | * [method@Session.websocket_connect_async] to create a WebSocket |
798 | | * connection, it will call this for you. |
799 | | * |
800 | | * Returns: %TRUE if @msg contains a completed valid WebSocket |
801 | | * handshake, %FALSE and an error if not. |
802 | | */ |
803 | | gboolean |
804 | | soup_websocket_client_verify_handshake (SoupMessage *msg, |
805 | | GPtrArray *supported_extensions, |
806 | | GList **accepted_extensions, |
807 | | GError **error) |
808 | 0 | { |
809 | 0 | const char *protocol, *request_protocols, *extensions, *accept_key; |
810 | 0 | char *expected_accept_key; |
811 | 0 | gboolean key_ok; |
812 | |
|
813 | 0 | g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE); |
814 | 0 | g_return_val_if_fail (accepted_extensions == NULL || *accepted_extensions == NULL, FALSE); |
815 | 0 | g_return_val_if_fail (error == NULL || *error == NULL, FALSE); |
816 | | |
817 | 0 | if (soup_message_get_status (msg) == SOUP_STATUS_BAD_REQUEST) { |
818 | 0 | g_set_error_literal (error, |
819 | 0 | SOUP_WEBSOCKET_ERROR, |
820 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
821 | 0 | _("Server rejected WebSocket handshake")); |
822 | 0 | return FALSE; |
823 | 0 | } |
824 | | |
825 | 0 | if (soup_message_get_status (msg) != SOUP_STATUS_SWITCHING_PROTOCOLS) { |
826 | 0 | g_set_error_literal (error, |
827 | 0 | SOUP_WEBSOCKET_ERROR, |
828 | 0 | SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, |
829 | 0 | _("Server ignored WebSocket handshake")); |
830 | 0 | return FALSE; |
831 | 0 | } |
832 | | |
833 | 0 | if (!soup_message_headers_header_equals_common (soup_message_get_response_headers (msg), SOUP_HEADER_UPGRADE, "websocket") || |
834 | 0 | !soup_message_headers_header_contains_common (soup_message_get_response_headers (msg), SOUP_HEADER_CONNECTION, "upgrade")) { |
835 | 0 | g_set_error_literal (error, |
836 | 0 | SOUP_WEBSOCKET_ERROR, |
837 | 0 | SOUP_WEBSOCKET_ERROR_NOT_WEBSOCKET, |
838 | 0 | _("Server ignored WebSocket handshake")); |
839 | 0 | return FALSE; |
840 | 0 | } |
841 | | |
842 | 0 | protocol = soup_message_headers_get_one_common (soup_message_get_response_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_PROTOCOL); |
843 | 0 | if (protocol) { |
844 | 0 | request_protocols = soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_PROTOCOL); |
845 | 0 | if (!request_protocols || |
846 | 0 | !soup_header_contains_case_sensitive (request_protocols, protocol)) { |
847 | 0 | g_set_error_literal (error, |
848 | 0 | SOUP_WEBSOCKET_ERROR, |
849 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
850 | 0 | _("Server requested unsupported protocol")); |
851 | 0 | return FALSE; |
852 | 0 | } |
853 | 0 | } |
854 | | |
855 | 0 | extensions = soup_message_headers_get_list_common (soup_message_get_response_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_EXTENSIONS); |
856 | 0 | if (extensions && *extensions) { |
857 | 0 | if (!process_extensions (extensions, msg, supported_extensions, accepted_extensions, error)) |
858 | 0 | return FALSE; |
859 | 0 | } |
860 | | |
861 | 0 | accept_key = soup_message_headers_get_one_common (soup_message_get_response_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_ACCEPT); |
862 | 0 | expected_accept_key = compute_accept_key (soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_SEC_WEBSOCKET_KEY)); |
863 | 0 | key_ok = (accept_key && expected_accept_key && |
864 | 0 | !g_ascii_strcasecmp (accept_key, expected_accept_key)); |
865 | 0 | g_free (expected_accept_key); |
866 | 0 | if (!key_ok) { |
867 | 0 | g_set_error (error, |
868 | 0 | SOUP_WEBSOCKET_ERROR, |
869 | 0 | SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE, |
870 | 0 | _("Server returned incorrect ā%sā key"), |
871 | 0 | "Sec-WebSocket-Accept"); |
872 | 0 | return FALSE; |
873 | 0 | } |
874 | | |
875 | 0 | return TRUE; |
876 | 0 | } |