[libsoup/carlosgc/websockets-null-char] WebSockets: allow null characters in text messages data



commit 109bb2f692c746bc63a0ade8737b584aecb0b1ad
Author: Carlos Garcia Campos <cgarcia igalia com>
Date:   Thu Jun 27 16:03:21 2019 +0200

    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.

 docs/reference/libsoup-2.4-sections.txt |   1 +
 libsoup/soup-websocket-connection.c     | 121 ++++++++++++++++++++++++++++++--
 libsoup/soup-websocket-connection.h     |   4 ++
 tests/websocket-test.c                  |  23 +++++-
 4 files changed, 141 insertions(+), 8 deletions(-)
---
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt
index 8836f2de..ec95d20e 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -1322,6 +1322,7 @@ soup_websocket_connection_get_state
 SoupWebsocketDataType
 soup_websocket_connection_send_text
 soup_websocket_connection_send_binary
+soup_websocket_connection_send_message
 SoupWebsocketCloseCode
 soup_websocket_connection_close
 soup_websocket_connection_get_close_code
diff --git a/libsoup/soup-websocket-connection.c b/libsoup/soup-websocket-connection.c
index 66bd6871..67a98731 100644
--- a/libsoup/soup-websocket-connection.c
+++ b/libsoup/soup-websocket-connection.c
@@ -156,6 +156,82 @@ static void queue_frame (SoupWebsocketConnection *self, SoupWebsocketQueueFlags
 
 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)
 {
@@ -635,7 +711,7 @@ receive_close (SoupWebsocketConnection *self,
                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;
@@ -780,9 +856,8 @@ process_contents (SoupWebsocketConnection *self,
                /* 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");
 
@@ -1722,7 +1797,9 @@ soup_websocket_connection_get_close_data (SoupWebsocketConnection *self)
  * @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.
@@ -1740,7 +1817,7 @@ soup_websocket_connection_send_text (SoupWebsocketConnection *self,
        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);
 }
@@ -1770,6 +1847,38 @@ soup_websocket_connection_send_binary (SoupWebsocketConnection *self,
        send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, 0x02, data, length);
 }
 
+/**
+ * soup_websocket_connection_send_message:
+ * @self: the WebSocket
+ * @type: the type of message contents
+ * @message: the message data as #GBytes
+ *
+ * Send a message of the given @type to the peer. Note that this method,
+ * allows to send text messages containing %NULL characters.
+ *
+ * The message is queued to be sent and will be sent when the main loop
+ * is run.
+ *
+ * Since: 2.68
+ */
+void
+soup_websocket_connection_send_message (SoupWebsocketConnection *self,
+                                        SoupWebsocketDataType type,
+                                        GBytes *message)
+{
+        gconstpointer data;
+        gsize length;
+
+        g_return_if_fail (SOUP_IS_WEBSOCKET_CONNECTION (self));
+        g_return_if_fail (soup_websocket_connection_get_state (self) == SOUP_WEBSOCKET_STATE_OPEN);
+        g_return_if_fail (message != NULL);
+
+        data = g_bytes_get_data (message, &length);
+        g_return_if_fail (type != SOUP_WEBSOCKET_DATA_TEXT || utf8_validate ((const char *)data, length));
+
+        send_message (self, SOUP_WEBSOCKET_QUEUE_NORMAL, (int)type, data, length);
+}
+
 /**
  * soup_websocket_connection_close:
  * @self: the WebSocket
diff --git a/libsoup/soup-websocket-connection.h b/libsoup/soup-websocket-connection.h
index bbd79e4d..d761c424 100644
--- a/libsoup/soup-websocket-connection.h
+++ b/libsoup/soup-websocket-connection.h
@@ -102,6 +102,10 @@ SOUP_AVAILABLE_IN_2_50
 void                soup_websocket_connection_send_binary    (SoupWebsocketConnection *self,
                                                              gconstpointer data,
                                                              gsize length);
+SOUP_AVAILABLE_IN_2_68
+void                soup_websocket_connection_send_message   (SoupWebsocketConnection *self,
+                                                             SoupWebsocketDataType type,
+                                                             GBytes *message);
 
 SOUP_AVAILABLE_IN_2_50
 void                soup_websocket_connection_close          (SoupWebsocketConnection *self,
diff --git a/tests/websocket-test.c b/tests/websocket-test.c
index 1f2781af..619b0b29 100644
--- a/tests/websocket-test.c
+++ b/tests/websocket-test.c
@@ -376,11 +376,13 @@ test_handshake_unsupported_extension (Test *test,
 }
 
 #define TEST_STRING "this is a test"
+#define TEST_STRING_WITH_NULL "this is\0 a test"
 
 static void
 test_send_client_to_server (Test *test,
                             gconstpointer data)
 {
+       GBytes *sent;
        GBytes *received = NULL;
        const char *contents;
        gsize len;
@@ -395,14 +397,23 @@ test_send_client_to_server (Test *test,
        contents = g_bytes_get_data (received, &len);
        g_assert_cmpstr (contents, ==, TEST_STRING);
        g_assert_cmpint (len, ==, strlen (TEST_STRING));
+       g_clear_pointer (&received, g_bytes_unref);
 
-       g_bytes_unref (received);
+       sent = g_bytes_new_static (TEST_STRING_WITH_NULL, sizeof (TEST_STRING_WITH_NULL));
+       soup_websocket_connection_send_message (test->client, SOUP_WEBSOCKET_DATA_TEXT, sent);
+
+       WAIT_UNTIL (received != NULL);
+
+       g_assert (g_bytes_equal (sent, received));
+       g_clear_pointer (&sent, g_bytes_unref);
+       g_clear_pointer (&received, g_bytes_unref);
 }
 
 static void
 test_send_server_to_client (Test *test,
                             gconstpointer data)
 {
+       GBytes *sent;
        GBytes *received = NULL;
        const char *contents;
        gsize len;
@@ -417,8 +428,16 @@ test_send_server_to_client (Test *test,
        contents = g_bytes_get_data (received, &len);
        g_assert_cmpstr (contents, ==, TEST_STRING);
        g_assert_cmpint (len, ==, strlen (TEST_STRING));
+       g_clear_pointer (&received, g_bytes_unref);
 
-       g_bytes_unref (received);
+       sent = g_bytes_new_static (TEST_STRING_WITH_NULL, sizeof (TEST_STRING_WITH_NULL));
+        soup_websocket_connection_send_message (test->server, SOUP_WEBSOCKET_DATA_TEXT, sent);
+
+        WAIT_UNTIL (received != NULL);
+
+        g_assert (g_bytes_equal (sent, received));
+        g_clear_pointer (&sent, g_bytes_unref);
+        g_clear_pointer (&received, g_bytes_unref);
 }
 
 static void


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]