Coverage Report

Created: 2025-07-09 06:18

/src/libsoup/libsoup/websocket/soup-websocket-extension-deflate.c
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2
/*
3
 * soup-websocket-extension-deflate.c
4
 *
5
 * Copyright (C) 2019 Igalia S.L.
6
 *
7
 * This library is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU Library General Public
9
 * License as published by the Free Software Foundation; either
10
 * version 2 of the License, or (at your option) any later version.
11
 *
12
 * This library is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15
 * Library General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Library General Public License
18
 * along with this library; see the file COPYING.LIB.  If not, write to
19
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20
 * Boston, MA 02110-1301, USA.
21
 */
22
23
24
#ifdef HAVE_CONFIG_H
25
#include <config.h>
26
#endif
27
28
#include "soup-websocket-extension-deflate.h"
29
#include <zlib.h>
30
31
typedef struct {
32
        z_stream zstream;
33
        gboolean no_context_takeover;
34
} Deflater;
35
36
typedef struct {
37
        z_stream zstream;
38
        gboolean uncompress_ongoing;
39
} Inflater;
40
41
0
#define BUFFER_SIZE 4096
42
43
typedef enum {
44
        PARAM_SERVER_NO_CONTEXT_TAKEOVER   = 1 << 0,
45
        PARAM_CLIENT_NO_CONTEXT_TAKEOVER   = 1 << 1,
46
        PARAM_SERVER_MAX_WINDOW_BITS       = 1 << 2,
47
        PARAM_CLIENT_MAX_WINDOW_BITS       = 1 << 3
48
} ParamFlags;
49
50
typedef struct {
51
        ParamFlags flags;
52
        gushort server_max_window_bits;
53
        gushort client_max_window_bits;
54
} Params;
55
56
struct _SoupWebsocketExtensionDeflate {
57
  SoupWebsocketExtension parent;
58
};
59
60
typedef struct {
61
        Params params;
62
63
        gboolean enabled;
64
65
        Deflater deflater;
66
        Inflater inflater;
67
} SoupWebsocketExtensionDeflatePrivate;
68
69
/**
70
 * SoupWebsocketExtensionDeflate:
71
 *
72
 * A SoupWebsocketExtensionDeflate is a [class@WebsocketExtension]
73
 * implementing permessage-deflate (RFC 7692).
74
 *
75
 * This extension is used by default in a [class@Session] when [class@WebsocketExtensionManager]
76
 * feature is present, and always used by [class@Server].
77
 */
78
79
G_DEFINE_FINAL_TYPE_WITH_PRIVATE (SoupWebsocketExtensionDeflate, soup_websocket_extension_deflate, SOUP_TYPE_WEBSOCKET_EXTENSION)
80
81
static void
82
soup_websocket_extension_deflate_init (SoupWebsocketExtensionDeflate *basic)
83
0
{
84
0
}
85
86
static void
87
soup_websocket_extension_deflate_finalize (GObject *object)
88
0
{
89
0
        SoupWebsocketExtensionDeflatePrivate *priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (object));
90
91
0
  if (priv->enabled) {
92
0
    deflateEnd (&priv->deflater.zstream);
93
0
    inflateEnd (&priv->inflater.zstream);
94
0
  }
95
96
0
        G_OBJECT_CLASS (soup_websocket_extension_deflate_parent_class)->finalize (object);
97
0
}
98
99
static gboolean
100
parse_window_bits (const char *value,
101
                   gushort    *out)
102
0
{
103
0
        guint64 int_value;
104
0
        char *end = NULL;
105
106
0
        if (!value || !*value)
107
0
                return FALSE;
108
109
0
        int_value = g_ascii_strtoull (value, &end, 10);
110
0
        if (*end != '\0')
111
0
                return FALSE;
112
113
0
        if (int_value < 8 || int_value > 15)
114
0
                return FALSE;
115
116
0
        *out = (gushort)int_value;
117
0
        return TRUE;
118
0
}
119
120
static gboolean
121
return_invalid_param_error (GError    **error,
122
                            const char *param)
123
0
{
124
0
        g_set_error (error,
125
0
                     SOUP_WEBSOCKET_ERROR,
126
0
                     SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
127
0
                     "Invalid parameter '%s' in permessage-deflate extension header",
128
0
                     param);
129
0
        return FALSE;
130
0
}
131
132
static gboolean
133
return_invalid_param_value_error (GError    **error,
134
                                  const char *param)
135
0
{
136
0
        g_set_error (error,
137
0
                     SOUP_WEBSOCKET_ERROR,
138
0
                     SOUP_WEBSOCKET_ERROR_BAD_HANDSHAKE,
139
0
                     "Invalid value of parameter '%s' in permessage-deflate extension header",
140
0
                     param);
141
0
        return FALSE;
142
0
}
143
144
static gboolean
145
parse_params (GHashTable *params,
146
              Params     *out,
147
              GError    **error)
148
0
{
149
0
        GHashTableIter iter;
150
0
        gpointer key, value;
151
152
0
        g_hash_table_iter_init (&iter, params);
153
0
        while (g_hash_table_iter_next (&iter, &key, &value)) {
154
0
                if (g_str_equal ((char *)key, "server_no_context_takeover")) {
155
0
                        if (value)
156
0
                                return return_invalid_param_value_error(error, "server_no_context_takeover");
157
158
0
                        out->flags |= PARAM_SERVER_NO_CONTEXT_TAKEOVER;
159
0
                } else if (g_str_equal ((char *)key, "client_no_context_takeover")) {
160
0
                        if (value)
161
0
                                return return_invalid_param_value_error(error, "client_no_context_takeover");
162
163
0
                        out->flags |= PARAM_CLIENT_NO_CONTEXT_TAKEOVER;
164
0
                } else if (g_str_equal ((char *)key, "server_max_window_bits")) {
165
0
                        if (!parse_window_bits ((char *)value, &out->server_max_window_bits))
166
0
                                return return_invalid_param_value_error(error, "server_max_window_bits");
167
168
0
                        out->flags |= PARAM_SERVER_MAX_WINDOW_BITS;
169
0
                } else if (g_str_equal ((char *)key, "client_max_window_bits")) {
170
0
                        if (value) {
171
0
                                if (!parse_window_bits ((char *)value, &out->client_max_window_bits))
172
0
                                        return return_invalid_param_value_error(error, "client_max_window_bits");
173
0
                        } else {
174
0
                                out->client_max_window_bits = 15;
175
0
                        }
176
0
                        out->flags |= PARAM_CLIENT_MAX_WINDOW_BITS;
177
0
                } else {
178
0
                        return return_invalid_param_error (error, (char *)key);
179
0
                }
180
0
        }
181
182
0
        return TRUE;
183
0
}
184
185
static gboolean
186
soup_websocket_extension_deflate_configure (SoupWebsocketExtension     *extension,
187
                                            SoupWebsocketConnectionType connection_type,
188
                                            GHashTable                 *params,
189
                                            GError                    **error)
190
0
{
191
0
        gushort deflater_max_window_bits;
192
0
        gushort inflater_max_window_bits;
193
0
        SoupWebsocketExtensionDeflatePrivate *priv;
194
195
0
        priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension));
196
197
0
        if (params && !parse_params (params, &priv->params, error))
198
0
                return FALSE;
199
200
0
        switch (connection_type) {
201
0
        case SOUP_WEBSOCKET_CONNECTION_CLIENT:
202
0
                priv->deflater.no_context_takeover = priv->params.flags & PARAM_CLIENT_NO_CONTEXT_TAKEOVER;
203
0
                deflater_max_window_bits = priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS ? priv->params.client_max_window_bits : 15;
204
0
                inflater_max_window_bits = priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS ? priv->params.server_max_window_bits : 15;
205
0
                break;
206
0
        case SOUP_WEBSOCKET_CONNECTION_SERVER:
207
0
                priv->deflater.no_context_takeover = priv->params.flags & PARAM_SERVER_NO_CONTEXT_TAKEOVER;
208
0
                deflater_max_window_bits = priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS ? priv->params.server_max_window_bits : 15;
209
0
                inflater_max_window_bits = priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS ? priv->params.client_max_window_bits : 15;
210
0
                break;
211
0
        default:
212
0
                g_assert_not_reached ();
213
0
        }
214
215
        /* zlib is unable to compress with window_bits=8, so use 9
216
         * instead. This is compatible with decompressing using
217
         * window_bits=8.
218
         */
219
0
        deflater_max_window_bits = MAX (deflater_max_window_bits, 9);
220
221
        /* In case of failing to initialize zlib deflater/inflater,
222
         * we return TRUE without setting enabled = TRUE, so that the
223
         * hanshake doesn't fail.
224
         */
225
0
        if (deflateInit2 (&priv->deflater.zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -deflater_max_window_bits, 8, Z_DEFAULT_STRATEGY) != Z_OK)
226
0
                return TRUE;
227
228
0
        if (inflateInit2 (&priv->inflater.zstream, -inflater_max_window_bits) != Z_OK) {
229
0
    deflateEnd (&priv->deflater.zstream);
230
0
                return TRUE;
231
0
  }
232
233
0
        priv->enabled = TRUE;
234
235
0
        return TRUE;
236
0
}
237
238
static char *
239
soup_websocket_extension_deflate_get_request_params (SoupWebsocketExtension *extension)
240
0
{
241
0
        return g_strdup ("; client_max_window_bits");
242
0
}
243
244
static char *
245
soup_websocket_extension_deflate_get_response_params (SoupWebsocketExtension *extension)
246
0
{
247
0
        GString *params;
248
0
        SoupWebsocketExtensionDeflatePrivate *priv;
249
250
0
        priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension));
251
0
  if (!priv->enabled)
252
0
    return NULL;
253
254
0
        if (priv->params.flags == 0)
255
0
                return NULL;
256
257
0
        params = g_string_new (NULL);
258
259
0
        if (priv->params.flags & PARAM_SERVER_NO_CONTEXT_TAKEOVER)
260
0
                params = g_string_append (params, "; server_no_context_takeover");
261
0
        if (priv->params.flags & PARAM_CLIENT_NO_CONTEXT_TAKEOVER)
262
0
                params = g_string_append (params, "; client_no_context_takeover");
263
0
        if (priv->params.flags & PARAM_SERVER_MAX_WINDOW_BITS)
264
0
                g_string_append_printf (params, "; server_max_window_bits=%u", priv->params.server_max_window_bits);
265
0
        if (priv->params.flags & PARAM_CLIENT_MAX_WINDOW_BITS)
266
0
    g_string_append_printf (params, "; client_max_window_bits=%u", priv->params.client_max_window_bits);
267
268
0
        return g_string_free (params, FALSE);
269
0
}
270
271
static void
272
deflater_reset (Deflater *deflater)
273
0
{
274
0
        if (deflater->no_context_takeover)
275
0
                deflateReset (&deflater->zstream);
276
0
}
277
278
static GBytes *
279
soup_websocket_extension_deflate_process_outgoing_message (SoupWebsocketExtension *extension,
280
                                                           guint8                 *header,
281
                                                           GBytes                 *payload,
282
                                                           GError                **error)
283
0
{
284
0
        const guint8 *payload_data;
285
0
        gsize payload_length;
286
0
        guint max_length;
287
0
        gboolean control;
288
0
        GByteArray *buffer;
289
0
        gsize bytes_written;
290
0
        int result;
291
0
        gboolean in_sync_flush;
292
0
        SoupWebsocketExtensionDeflatePrivate *priv;
293
294
0
        priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension));
295
296
0
        if (!priv->enabled)
297
0
                return payload;
298
299
0
        control = header[0] & 0x08;
300
301
        /* Do not compress control frames */
302
0
        if (control)
303
0
                return payload;
304
305
0
        payload_data = g_bytes_get_data (payload, &payload_length);
306
0
        if (payload_length == 0)
307
0
                return payload;
308
309
        /* Mark the frame as compressed using reserved bit 1 (0x40) */
310
0
        header[0] |= 0x40;
311
312
0
        buffer = g_byte_array_new ();
313
0
        max_length = deflateBound(&priv->deflater.zstream, payload_length);
314
315
0
        priv->deflater.zstream.next_in = (void *)payload_data;
316
0
        priv->deflater.zstream.avail_in = payload_length;
317
318
0
        bytes_written = 0;
319
0
        priv->deflater.zstream.avail_out = 0;
320
321
0
        do {
322
0
                gsize write_remaining;
323
324
0
                if (priv->deflater.zstream.avail_out == 0) {
325
0
                        guint write_position;
326
327
0
                        priv->deflater.zstream.avail_out = max_length;
328
0
                        write_position = buffer->len;
329
0
                        g_byte_array_set_size (buffer, buffer->len + max_length);
330
0
                        priv->deflater.zstream.next_out = buffer->data + write_position;
331
332
                        /* Use a fixed value for buffer increments */
333
0
                        max_length = BUFFER_SIZE;
334
0
                }
335
336
0
                write_remaining = buffer->len - bytes_written;
337
0
                in_sync_flush = priv->deflater.zstream.avail_in == 0;
338
0
                result = deflate (&priv->deflater.zstream, in_sync_flush ? Z_SYNC_FLUSH : Z_NO_FLUSH);
339
0
                bytes_written += write_remaining - priv->deflater.zstream.avail_out;
340
0
        } while (result == Z_OK);
341
342
0
        g_bytes_unref (payload);
343
344
0
        if (result != Z_BUF_ERROR || bytes_written < 4) {
345
0
                g_set_error_literal (error,
346
0
                                     SOUP_WEBSOCKET_ERROR,
347
0
                                     SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR,
348
0
                                     "Failed to compress outgoing frame");
349
0
                g_byte_array_unref (buffer);
350
0
                deflater_reset (&priv->deflater);
351
0
                return NULL;
352
0
        }
353
354
        /* Remove 4 octets (that are 0x00 0x00 0xff 0xff) from the tail end. */
355
0
        g_byte_array_set_size (buffer, bytes_written - 4);
356
357
0
        deflater_reset (&priv->deflater);
358
359
0
        return g_byte_array_free_to_bytes (buffer);
360
0
}
361
362
static GBytes *
363
soup_websocket_extension_deflate_process_incoming_message (SoupWebsocketExtension *extension,
364
                                                           guint8                 *header,
365
                                                           GBytes                 *payload,
366
                                                           GError                **error)
367
0
{
368
0
        const guint8 *payload_data;
369
0
        gsize payload_length;
370
0
        gboolean fin, control, compressed;
371
0
        GByteArray *buffer;
372
0
        gsize bytes_read, bytes_written;
373
0
        int result;
374
0
        gboolean tail_added = FALSE;
375
0
        SoupWebsocketExtensionDeflatePrivate *priv;
376
377
0
        priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension));
378
379
0
        if (!priv->enabled)
380
0
                return payload;
381
382
0
        control = header[0] & 0x08;
383
384
        /* Do not uncompress control frames */
385
0
        if (control)
386
0
                return payload;
387
388
0
        compressed = header[0] & 0x40;
389
0
        if (!priv->inflater.uncompress_ongoing && !compressed)
390
0
                return payload;
391
392
0
        if (priv->inflater.uncompress_ongoing && compressed) {
393
0
                g_set_error_literal (error,
394
0
                                     SOUP_WEBSOCKET_ERROR,
395
0
                                     SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR,
396
0
                                     "Received a non-first frame with RSV1 flag set");
397
0
                g_bytes_unref (payload);
398
0
                return NULL;
399
0
        }
400
401
        /* Remove the compressed flag */
402
0
        header[0] &= ~0x40;
403
404
0
        fin = header[0] & 0x80;
405
0
        payload_data = g_bytes_get_data (payload, &payload_length);
406
0
        if (payload_length == 0 && ((!priv->inflater.uncompress_ongoing && fin) || (priv->inflater.uncompress_ongoing && !fin)))
407
0
                return payload;
408
409
0
        priv->inflater.uncompress_ongoing = !fin;
410
411
0
        buffer = g_byte_array_new ();
412
413
0
        bytes_read = 0;
414
0
        priv->inflater.zstream.next_in = (void *)payload_data;
415
0
        priv->inflater.zstream.avail_in = payload_length;
416
417
0
        bytes_written = 0;
418
0
        priv->inflater.zstream.avail_out = 0;
419
420
0
        do {
421
0
                gsize read_remaining;
422
0
                gsize write_remaining;
423
424
0
                if (priv->inflater.zstream.avail_out == 0) {
425
0
                        guint current_position;
426
427
0
                        priv->inflater.zstream.avail_out = BUFFER_SIZE;
428
0
                        current_position = buffer->len;
429
0
                        g_byte_array_set_size (buffer, buffer->len + BUFFER_SIZE);
430
0
                        priv->inflater.zstream.next_out = buffer->data + current_position;
431
0
                }
432
433
0
                if (priv->inflater.zstream.avail_in == 0 && !tail_added && fin) {
434
                        /* Append 4 octets of 0x00 0x00 0xff 0xff to the tail end */
435
0
                        priv->inflater.zstream.next_in = (void *)"\x00\x00\xff\xff";
436
0
                        priv->inflater.zstream.avail_in = 4;
437
0
                        bytes_read = 0;
438
0
                        tail_added = TRUE;
439
0
                }
440
441
0
                read_remaining = tail_added ? 4 : payload_length - bytes_read;
442
0
                write_remaining = buffer->len - bytes_written;
443
0
                result = inflate (&priv->inflater.zstream, tail_added ? Z_FINISH : Z_NO_FLUSH);
444
0
                bytes_read += read_remaining - priv->inflater.zstream.avail_in;
445
0
                bytes_written += write_remaining - priv->inflater.zstream.avail_out;
446
0
                if (!tail_added && result == Z_STREAM_END) {
447
                        /* Received a block with BFINAL set to 1. Reset decompression state. */
448
0
                        result = inflateReset (&priv->inflater.zstream);
449
0
                }
450
451
0
                if ((!fin && bytes_read == payload_length) || (fin && tail_added && bytes_read == 4))
452
0
                        break;
453
0
        } while (result == Z_OK || result == Z_BUF_ERROR);
454
455
0
        g_bytes_unref (payload);
456
457
0
        if (result != Z_OK && result != Z_BUF_ERROR) {
458
0
                priv->inflater.uncompress_ongoing = FALSE;
459
0
                g_set_error_literal (error,
460
0
                                     SOUP_WEBSOCKET_ERROR,
461
0
                                     SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR,
462
0
                                     "Failed to uncompress incoming frame");
463
0
                g_byte_array_unref (buffer);
464
465
0
                return NULL;
466
0
        }
467
468
0
        g_byte_array_set_size (buffer, bytes_written);
469
470
0
        return g_byte_array_free_to_bytes (buffer);
471
0
}
472
473
static void
474
soup_websocket_extension_deflate_class_init (SoupWebsocketExtensionDeflateClass *klass)
475
0
{
476
0
        SoupWebsocketExtensionClass *extension_class = SOUP_WEBSOCKET_EXTENSION_CLASS (klass);
477
0
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
478
479
0
        extension_class->name = "permessage-deflate";
480
481
0
        extension_class->configure = soup_websocket_extension_deflate_configure;
482
0
        extension_class->get_request_params = soup_websocket_extension_deflate_get_request_params;
483
0
        extension_class->get_response_params = soup_websocket_extension_deflate_get_response_params;
484
0
        extension_class->process_outgoing_message = soup_websocket_extension_deflate_process_outgoing_message;
485
0
        extension_class->process_incoming_message = soup_websocket_extension_deflate_process_incoming_message;
486
487
0
        object_class->finalize = soup_websocket_extension_deflate_finalize;
488
0
}