Commit 109bb2f6 authored by Carlos Garcia Campos's avatar Carlos Garcia Campos Committed by Carlos Garcia Campos

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.
parent fd794a95
Pipeline #92305 passed with stage
in 47 seconds
......@@ -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
......
......@@ -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
......
......@@ -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,
......
......@@ -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
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment