Coverage Report

Created: 2026-10-28 06:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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 (&params, 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 (&params, 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
}