diff --git a/SOURCES/0001-WebSockets-ignore-any-messages-after-close-has-been-.patch b/SOURCES/0001-WebSockets-ignore-any-messages-after-close-has-been-.patch new file mode 100644 index 0000000..644cb53 --- /dev/null +++ b/SOURCES/0001-WebSockets-ignore-any-messages-after-close-has-been-.patch @@ -0,0 +1,95 @@ +From d9c729aa5a7991182fa7bdb8d94442f8f0cf055b Mon Sep 17 00:00:00 2001 +From: Carlos Garcia Campos +Date: Fri, 19 Jul 2019 14:56:05 +0200 +Subject: [PATCH] WebSockets: ignore any messages after close has been sent and + received + +We currently ignore data frames when close has been received, but we +should also ignore any frame after close has been sent and received. +Currently, if we receive two close frames we end up with the code and +reason of the second frame, while the RFC says: "The WebSocket +Connection Close Code is defined as the status code contained in the +first Close control frame received by the application implementing +this protocol." +--- + libsoup/soup-websocket-connection.c | 3 ++ + tests/websocket-test.c | 48 +++++++++++++++++++++++++++++ + 2 files changed, 51 insertions(+) + +diff --git libsoup/soup-websocket-connection.c libsoup/soup-websocket-connection.c +--- a/libsoup/soup-websocket-connection.c ++++ b/libsoup/soup-websocket-connection.c +@@ -690,6 +690,9 @@ + SoupWebsocketConnectionPrivate *pv = self->pv; + GBytes *message; + ++ if (pv->close_sent && pv->close_received) ++ return; ++ + if (control) { + /* Control frames must never be fragmented */ + if (!fin) { +--- a/tests/websocket-test.c ++++ b/tests/websocket-test.c +@@ -707,6 +707,49 @@ + } + + static gpointer ++close_after_close_server_thread (gpointer user_data) ++{ ++ Test *test = user_data; ++ gsize written; ++ const char frames[] = ++ "\x88\x09\x03\xe8""reason1" ++ "\x88\x09\x03\xe8""reason2"; ++ GError *error = NULL; ++ ++ g_mutex_lock (&test->mutex); ++ g_mutex_unlock (&test->mutex); ++ ++ g_output_stream_write_all (g_io_stream_get_output_stream (test->raw_server), ++ frames, sizeof (frames) -1, &written, NULL, &error); ++ g_assert_no_error (error); ++ g_assert_cmpuint (written, ==, sizeof (frames) - 1); ++ g_io_stream_close (test->raw_server, NULL, &error); ++ g_assert_no_error (error); ++ ++ return NULL; ++} ++ ++static void ++test_close_after_close (Test *test, ++ gconstpointer data) ++{ ++ GThread *thread; ++ ++ g_mutex_lock (&test->mutex); ++ ++ thread = g_thread_new ("close-after-close-thread", close_after_close_server_thread, test); ++ ++ soup_websocket_connection_close (test->client, SOUP_WEBSOCKET_CLOSE_NORMAL, "reason1"); ++ g_mutex_unlock (&test->mutex); ++ ++ g_thread_join (thread); ++ ++ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED); ++ g_assert_cmpuint (soup_websocket_connection_get_close_code (test->client), ==, SOUP_WEBSOCKET_CLOSE_NORMAL); ++ g_assert_cmpstr (soup_websocket_connection_get_close_data (test->client), ==, "reason1"); ++} ++ ++static gpointer + timeout_server_thread (gpointer user_data) + { + Test *test = user_data; +@@ -918,6 +961,11 @@ + test_message_after_closing, + teardown_soup_connection); + ++ g_test_add ("/websocket/direct/close-after-close", Test, NULL, ++ setup_half_direct_connection, ++ test_close_after_close, ++ teardown_direct_connection); ++ + + g_test_add ("/websocket/direct/protocol-negotiate", Test, NULL, NULL, + test_protocol_negotiate_direct, diff --git a/SOURCES/0002-WebSockets-allow-null-characters-in-text-messages-da.patch b/SOURCES/0002-WebSockets-allow-null-characters-in-text-messages-da.patch new file mode 100644 index 0000000..113e461 --- /dev/null +++ b/SOURCES/0002-WebSockets-allow-null-characters-in-text-messages-da.patch @@ -0,0 +1,152 @@ +From 109bb2f692c746bc63a0ade8737b584aecb0b1ad Mon Sep 17 00:00:00 2001 +From: Carlos Garcia Campos +Date: Thu, 27 Jun 2019 16:03:21 +0200 +Subject: [PATCH] WebSockets: allow null characters in text messages data + +RFC 6455 says that text messages should contains valid UTF-8, and null +characters valid according to RFC 3629. However, we are using +g_utf8_validate(), which considers null characters as errors, to +validate WebSockets text messages. This patch adds an internal +utf8_validate() function based on g_utf8_validate() but allowing null +characters and just returning a gboolean since we are always ignoring +the end parameter in case of errors. +soup_websocket_connection_send_text() assumes the given text is null +terminated, so we need a new public function to allow sending text +messages containing null characters. This patch adds +soup_websocket_connection_send_message() that receives a +SoupWebsocketDataType and GBytes, which is consistent with +SoupWebsocketConnection::message signal. + +For RHEL backport, drop the addition of soup_websocket_connection_send_message() +as we don't need it and don't want to expose new API. +diff --git libsoup/soup-websocket-connection.c libsoup/soup-websocket-connection.c +index 66bd6871..67a98731 100644 +--- a/libsoup/soup-websocket-connection.c ++++ b/libsoup/soup-websocket-connection.c +@@ -155,6 +155,82 @@ + + static void protocol_error_and_close (SoupWebsocketConnection *self); + ++/* Code below is based on g_utf8_validate() implementation, ++ * but handling NULL characters as valid, as expected by ++ * WebSockets and compliant with RFC 3629. ++ */ ++#define VALIDATE_BYTE(mask, expect) \ ++ G_STMT_START { \ ++ if (G_UNLIKELY((*(guchar *)p & (mask)) != (expect))) \ ++ return FALSE; \ ++ } G_STMT_END ++ ++/* see IETF RFC 3629 Section 4 */ ++static gboolean ++utf8_validate (const char *str, ++ gsize max_len) ++ ++{ ++ const gchar *p; ++ ++ for (p = str; ((p - str) < max_len); p++) { ++ if (*(guchar *)p < 128) ++ /* done */; ++ else { ++ if (*(guchar *)p < 0xe0) { /* 110xxxxx */ ++ if (G_UNLIKELY (max_len - (p - str) < 2)) ++ return FALSE; ++ ++ if (G_UNLIKELY (*(guchar *)p < 0xc2)) ++ return FALSE; ++ } else { ++ if (*(guchar *)p < 0xf0) { /* 1110xxxx */ ++ if (G_UNLIKELY (max_len - (p - str) < 3)) ++ return FALSE; ++ ++ switch (*(guchar *)p++ & 0x0f) { ++ case 0: ++ VALIDATE_BYTE(0xe0, 0xa0); /* 0xa0 ... 0xbf */ ++ break; ++ case 0x0d: ++ VALIDATE_BYTE(0xe0, 0x80); /* 0x80 ... 0x9f */ ++ break; ++ default: ++ VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ ++ } ++ } else if (*(guchar *)p < 0xf5) { /* 11110xxx excluding out-of-range */ ++ if (G_UNLIKELY (max_len - (p - str) < 4)) ++ return FALSE; ++ ++ switch (*(guchar *)p++ & 0x07) { ++ case 0: ++ VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ ++ if (G_UNLIKELY((*(guchar *)p & 0x30) == 0)) ++ return FALSE; ++ break; ++ case 4: ++ VALIDATE_BYTE(0xf0, 0x80); /* 0x80 ... 0x8f */ ++ break; ++ default: ++ VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ ++ } ++ p++; ++ VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ ++ } else { ++ return FALSE; ++ } ++ } ++ ++ p++; ++ VALIDATE_BYTE(0xc0, 0x80); /* 10xxxxxx */ ++ } ++ } ++ ++ return TRUE; ++} ++ ++#undef VALIDATE_BYTE ++ + static void + frame_free (gpointer data) + { +@@ -629,7 +705,7 @@ + data += 2; + len -= 2; + +- if (!g_utf8_validate ((char *)data, len, NULL)) { ++ if (!utf8_validate ((const char *)data, len)) { + g_debug ("received non-UTF8 close data: %d '%.*s' %d", (int)len, (int)len, (char *)data, (int)data[0]); + protocol_error_and_close (self); + return; +@@ -777,9 +853,8 @@ + /* Actually deliver the message? */ + if (fin) { + if (pv->message_opcode == 0x01 && +- !g_utf8_validate((char *)pv->message_data->data, +- pv->message_data->len, +- NULL)) { ++ !utf8_validate((const char *)pv->message_data->data, ++ pv->message_data->len)) { + + g_debug ("received invalid non-UTF8 text data"); + +@@ -1699,7 +1774,9 @@ + * @self: the WebSocket + * @text: the message contents + * +- * Send a text (UTF-8) message to the peer. ++ * Send a %NULL-terminated text (UTF-8) message to the peer. If you need ++ * to send text messages containing %NULL characters use ++ * soup_websocket_connection_send_message() instead. + * + * The message is queued to be sent and will be sent when the main loop + * is run. +@@ -1717,7 +1794,7 @@ + g_return_if_fail (text != NULL); + + length = strlen (text); +- g_return_if_fail (g_utf8_validate (text, length, NULL)); ++ g_return_if_fail (utf8_validate (text, length)); + + send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, 0x01, (const guint8 *) text, length); + } +-- +2.26.2 + diff --git a/SOURCES/0003-WebSockets-only-poll-IO-stream-when-needed.patch b/SOURCES/0003-WebSockets-only-poll-IO-stream-when-needed.patch new file mode 100644 index 0000000..820d0c4 --- /dev/null +++ b/SOURCES/0003-WebSockets-only-poll-IO-stream-when-needed.patch @@ -0,0 +1,299 @@ +From 35f1bac5ff9ec694e64b65e51f0e7a3226aa3aaf Mon Sep 17 00:00:00 2001 +From: Carlos Garcia Campos +Date: Wed, 28 Aug 2019 10:51:18 +0200 +Subject: [PATCH] WebSockets: only poll IO stream when needed + +Instead of having two pollable sources constantly running, always try to +read/write without blocking and start polling if the operation returns +G_IO_ERROR_WOULD_BLOCK. This patch also fixes test +/websocket/direct/close-after-close that was passing but not actually +testing what we wanted, because the client close was never sent. When +the mutex is released, the frame has been queued, but not sent. + +diff --git libsoup/soup-websocket-connection.c libsoup/soup-websocket-connection.c +index 345040fe..6afbbe67 100644 +--- a/libsoup/soup-websocket-connection.c ++++ b/libsoup/soup-websocket-connection.c +@@ -147,6 +147,7 @@ + }; + + #define MAX_INCOMING_PAYLOAD_SIZE_DEFAULT 128 * 1024 ++#define READ_BUFFER_SIZE 1024 + + G_DEFINE_TYPE_WITH_PRIVATE (SoupWebsocketConnection, soup_websocket_connection, G_TYPE_OBJECT) + +@@ -155,6 +156,11 @@ + + static void protocol_error_and_close (SoupWebsocketConnection *self); + ++static gboolean on_web_socket_input (GObject *pollable_stream, ++ gpointer user_data); ++static gboolean on_web_socket_output (GObject *pollable_stream, ++ gpointer user_data); ++ + /* Code below is based on g_utf8_validate() implementation, + * but handling NULL characters as valid, as expected by + * WebSockets and compliant with RFC 3629. +@@ -283,7 +289,20 @@ + } + + static void +-stop_input (SoupWebsocketConnection *self) ++soup_websocket_connection_start_input_source (SoupWebsocketConnection *self) ++{ ++ SoupWebsocketConnectionPrivate *pv = self->pv; ++ ++ if (pv->input_source) ++ return; ++ ++ pv->input_source = g_pollable_input_stream_create_source (pv->input, NULL); ++ g_source_set_callback (pv->input_source, (GSourceFunc)on_web_socket_input, self, NULL); ++ g_source_attach (pv->input_source, pv->main_context); ++} ++ ++static void ++soup_websocket_connection_stop_input_source (SoupWebsocketConnection *self) + { + SoupWebsocketConnectionPrivate *pv = self->pv; + +@@ -296,7 +315,20 @@ + } + + static void +-stop_output (SoupWebsocketConnection *self) ++soup_websocket_connection_start_output_source (SoupWebsocketConnection *self) ++{ ++ SoupWebsocketConnectionPrivate *pv = self->pv; ++ ++ if (pv->output_source) ++ return; ++ ++ pv->output_source = g_pollable_output_stream_create_source (pv->output, NULL); ++ g_source_set_callback (pv->output_source, (GSourceFunc)on_web_socket_output, self, NULL); ++ g_source_attach (pv->output_source, pv->main_context); ++} ++ ++static void ++soup_websocket_connection_stop_output_source (SoupWebsocketConnection *self) + { + SoupWebsocketConnectionPrivate *pv = self->pv; + +@@ -341,8 +373,8 @@ + close_io_stop_timeout (self); + + if (!pv->io_closing) { +- stop_input (self); +- stop_output (self); ++ soup_websocket_connection_stop_input_source (self); ++ soup_websocket_connection_stop_output_source (self); + pv->io_closing = TRUE; + g_debug ("closing io stream"); + g_io_stream_close_async (pv->io_stream, G_PRIORITY_DEFAULT, +@@ -359,7 +391,7 @@ + GSocket *socket; + GError *error = NULL; + +- stop_output (self); ++ soup_websocket_connection_stop_output_source (self); + + if (G_IS_SOCKET_CONNECTION (pv->io_stream)) { + socket = g_socket_connection_get_socket (G_SOCKET_CONNECTION (pv->io_stream)); +@@ -612,9 +644,6 @@ + self->pv->connection_type == SOUP_WEBSOCKET_CONNECTION_SERVER ? "server" : "client", + payload_len, self->pv->max_incoming_payload_size); + emit_error_and_close (self, error, TRUE); +- +- /* The input is in an invalid state now */ +- stop_input (self); + } + + static void +@@ -981,32 +1010,31 @@ + ; + } + +-static gboolean +-on_web_socket_input (GObject *pollable_stream, +- gpointer user_data) ++static void ++soup_websocket_connection_read (SoupWebsocketConnection *self) + { +- SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data); + SoupWebsocketConnectionPrivate *pv = self->pv; + GError *error = NULL; + gboolean end = FALSE; + gssize count; + gsize len; + ++ soup_websocket_connection_stop_input_source (self); ++ + do { + len = pv->incoming->len; +- g_byte_array_set_size (pv->incoming, len + 1024); ++ g_byte_array_set_size (pv->incoming, len + READ_BUFFER_SIZE); + + count = g_pollable_input_stream_read_nonblocking (pv->input, + pv->incoming->data + len, +- 1024, NULL, &error); +- ++ READ_BUFFER_SIZE, NULL, &error); + if (count < 0) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { + g_error_free (error); + count = 0; + } else { + emit_error_and_close (self, error, TRUE); +- return TRUE; ++ return; + } + } else if (count == 0) { + end = TRUE; +@@ -1026,16 +1054,24 @@ + } + + close_io_stream (self); ++ return; + } + +- return TRUE; ++ soup_websocket_connection_start_input_source (self); + } + + static gboolean +-on_web_socket_output (GObject *pollable_stream, +- gpointer user_data) ++on_web_socket_input (GObject *pollable_stream, ++ gpointer user_data) ++{ ++ soup_websocket_connection_read (SOUP_WEBSOCKET_CONNECTION (user_data)); ++ ++ return G_SOURCE_REMOVE; ++} ++ ++static void ++soup_websocket_connection_write (SoupWebsocketConnection *self) + { +- SoupWebsocketConnection *self = SOUP_WEBSOCKET_CONNECTION (user_data); + SoupWebsocketConnectionPrivate *pv = self->pv; + const guint8 *data; + GError *error = NULL; +@@ -1043,19 +1079,18 @@ + gssize count; + gsize len; + ++ soup_websocket_connection_stop_output_source (self); ++ + if (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_CLOSED) { + g_debug ("Ignoring message since the connection is closed"); +- stop_output (self); +- return TRUE; ++ return; + } + + frame = g_queue_peek_head (&pv->outgoing); + + /* No more frames to send */ +- if (frame == NULL) { +- stop_output (self); +- return TRUE; +- } ++ if (frame == NULL) ++ return; + + data = g_bytes_get_data (frame->data, &len); + g_assert (len > 0); +@@ -1075,7 +1110,7 @@ + frame->pending = TRUE; + } else { + emit_error_and_close (self, error, TRUE); +- return FALSE; ++ return; + } + } + +@@ -1093,23 +1128,21 @@ + } + } + frame_free (frame); ++ ++ if (g_queue_is_empty (&pv->outgoing)) ++ return; + } + +- return TRUE; ++ soup_websocket_connection_start_output_source (self); + } + +-static void +-start_output (SoupWebsocketConnection *self) ++static gboolean ++on_web_socket_output (GObject *pollable_stream, ++ gpointer user_data) + { +- SoupWebsocketConnectionPrivate *pv = self->pv; +- +- if (pv->output_source) +- return; ++ soup_websocket_connection_write (SOUP_WEBSOCKET_CONNECTION (user_data)); + +- g_debug ("starting output source"); +- pv->output_source = g_pollable_output_stream_create_source (pv->output, NULL); +- g_source_set_callback (pv->output_source, (GSourceFunc)on_web_socket_output, self, NULL); +- g_source_attach (pv->output_source, pv->main_context); ++ return G_SOURCE_REMOVE; + } + + static void +@@ -1150,7 +1183,7 @@ + g_queue_push_tail (&pv->outgoing, frame); + } + +- start_output (self); ++ soup_websocket_connection_write (self); + } + + static void +@@ -1175,9 +1208,7 @@ + pv->output = G_POLLABLE_OUTPUT_STREAM (os); + g_return_if_fail (g_pollable_output_stream_can_poll (pv->output)); + +- pv->input_source = g_pollable_input_stream_create_source (pv->input, NULL); +- g_source_set_callback (pv->input_source, (GSourceFunc)on_web_socket_input, self, NULL); +- g_source_attach (pv->input_source, pv->main_context); ++ soup_websocket_connection_start_input_source (self); + } + + static void +diff --git tests/websocket-test.c tests/websocket-test.c +index 146fdf82..26d064df 100644 +--- a/tests/websocket-test.c ++++ b/tests/websocket-test.c +@@ -733,6 +733,7 @@ + const char frames[] = + "\x88\x09\x03\xe8""reason1" + "\x88\x09\x03\xe8""reason2"; ++ GSocket *socket; + GError *error = NULL; + + g_mutex_lock (&test->mutex); +@@ -742,7 +743,8 @@ + frames, sizeof (frames) -1, &written, NULL, &error); + g_assert_no_error (error); + g_assert_cmpuint (written, ==, sizeof (frames) - 1); +- g_io_stream_close (test->raw_server, NULL, &error); ++ socket = g_socket_connection_get_socket (G_SOCKET_CONNECTION (test->raw_server)); ++ g_socket_shutdown (socket, FALSE, TRUE, &error); + g_assert_no_error (error); + + return NULL; +@@ -766,6 +768,7 @@ + WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED); + g_assert_cmpuint (soup_websocket_connection_get_close_code (test->client), ==, SOUP_WEBSOCKET_CLOSE_NORMAL); + g_assert_cmpstr (soup_websocket_connection_get_close_data (test->client), ==, "reason1"); ++ g_io_stream_close (test->raw_server, NULL, NULL); + } + + static gpointer +-- +2.26.2 + diff --git a/SPECS/libsoup.spec b/SPECS/libsoup.spec index 3765db9..4a288cd 100644 --- a/SPECS/libsoup.spec +++ b/SPECS/libsoup.spec @@ -2,13 +2,17 @@ Name: libsoup Version: 2.62.3 -Release: 1%{?dist} +Release: 2%{?dist} Summary: Soup, an HTTP library implementation License: LGPLv2 URL: https://wiki.gnome.org/Projects/libsoup Source0: https://download.gnome.org/sources/%{name}/2.62/%{name}-%{version}.tar.xz +Patch0001: 0001-WebSockets-ignore-any-messages-after-close-has-been-.patch +Patch0002: 0002-WebSockets-allow-null-characters-in-text-messages-da.patch +Patch0003: 0003-WebSockets-only-poll-IO-stream-when-needed.patch + BuildRequires: chrpath BuildRequires: glib2-devel >= %{glib2_version} BuildRequires: glib-networking @@ -82,6 +86,9 @@ chrpath --delete $RPM_BUILD_ROOT%{_libdir}/*.so %{_datadir}/vala/vapi/libsoup-2.4.vapi %changelog +* Thu Aug 27 2020 Martin Pitt - 2.62.3-2 +- Some WebSocket fixes to unbreak cockpit-desktop (rhbz#1872270) + * Fri Aug 10 2018 Kalev Lember - 2.62.3-1 - Update to 2.62.3