/* * This file is part of Cockpit. * * Copyright (C) 2013 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ #include "config.h" #include "websocket.h" #include "websocketprivate.h" #include #include #include typedef struct { WebSocketConnection *client; WebSocketConnection *server; } Test; static void null_log_handler (const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data) { /* * HACK: Use g_test_expect_message() to quieten things down once this lands: * https://bugzilla.gnome.org/show_bug.cgi?id=710991 */ } #define WAIT_UNTIL(cond) \ G_STMT_START \ while (!(cond)) g_main_context_iteration (NULL, TRUE); \ G_STMT_END static void test_parse_url (void) { gboolean ret; GError *error = NULL; gchar *host; gchar *path; gchar *scheme; ret = _web_socket_util_parse_url ("scheme://host:port/path/part", &scheme, &host, &path, &error); g_assert_no_error (error); g_assert (ret == TRUE); g_assert_cmpstr (scheme, ==, "scheme"); g_assert_cmpstr (host, ==, "host:port"); g_assert_cmpstr (path, ==, "/path/part"); g_free (scheme); g_free (host); g_free (path); } static void test_parse_url_no_out (void) { gboolean ret; GError *error = NULL; ret = _web_socket_util_parse_url ("scheme://host:port/path/part", NULL, NULL, NULL, &error); g_assert_no_error (error); g_assert (ret == TRUE); } static void test_parse_url_bad (void) { const gchar *bads[] = { "/host:port/path/part", "http://@/", "http:///", "http://", }; gboolean ret; GError *error = NULL; gint i; for (i = 0; i < G_N_ELEMENTS (bads); i++) { ret = _web_socket_util_parse_url (bads[i], NULL, NULL, NULL, &error); g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); g_assert (ret == FALSE); g_clear_error (&error); } } static void test_parse_url_no_path (void) { gboolean ret; gchar *path; ret = _web_socket_util_parse_url ("scheme://host:port", NULL, NULL, &path, NULL); g_assert (ret == TRUE); g_assert_cmpstr (path, ==, "/"); g_free (path); } static void test_parse_url_with_user (void) { gboolean ret; gchar *host; ret = _web_socket_util_parse_url ("scheme://user:password@host", NULL, &host, NULL, NULL); g_assert (ret == TRUE); g_assert_cmpstr (host, ==, "host"); g_free (host); } static void test_parse_req (void) { const gchar *reqs[] = { "GET /path/part HTTP/1.0\r\n ", "GET /path/part HTTP/1.0\n ", "GET /path/part HTTP/1.0 \r\n ", }; gchar *path; gchar *method; gssize ret; gint i; for (i = 0; i < G_N_ELEMENTS (reqs); i++) { ret = web_socket_util_parse_req_line (reqs[i], strlen (reqs[i]), &method, &path); g_assert_cmpint (ret, ==, strlen (reqs[i]) - 2); g_assert_cmpstr (method, ==, "GET"); g_assert_cmpstr (path, ==, "/path/part"); g_free (method); g_free (path); } } static void test_parse_req_no_out (void) { const gchar *data = "GET /path/part HTTP/1.0\r\n "; gssize ret; ret = web_socket_util_parse_req_line (data, strlen (data), NULL, NULL); g_assert_cmpint (ret, ==, 25); } static void test_parse_req_not_enough (void) { const gchar *data = "GET /path/par"; gssize ret; ret = web_socket_util_parse_req_line (data, strlen (data), NULL, NULL); g_assert_cmpint (ret, ==, 0); } static void test_parse_req_bad (void) { const gchar *bads[] = { " GET /path/part HTTP/1.0\r\n ", "GET /path/part\r\n ", "GET /path/part HTTP/4.4\r\n ", "GET /path/part HTTP/1.0X\r\n ", "GET /path/part XXX/2\r\n ", "TESTONE\r\n ", }; gssize ret; gint i; for (i = 0; i < G_N_ELEMENTS (bads); i++) { ret = web_socket_util_parse_req_line (bads[i], strlen (bads[i]), NULL, NULL); g_assert_cmpint (ret, ==, -1); } } static void test_parse_status (void) { const gchar *lines[] = { "HTTP/1.0 101 Switching Protocols\r\n ", "HTTP/1.0 101 Switching Protocols\n ", "HTTP/1.1 101 Switching Protocols \r\n ", }; guint status; gchar *reason; gssize ret; gint i; for (i = 0; i < G_N_ELEMENTS (lines); i++) { ret = web_socket_util_parse_status_line (lines[i], strlen (lines[i]), NULL, &status, &reason); g_assert_cmpint (ret, ==, strlen (lines[i]) - 2); g_assert_cmpuint (status, ==, 101); g_assert_cmpstr (reason, ==, "Switching Protocols"); g_free (reason); } } static void test_parse_status_no_out (void) { const gchar *line = "HTTP/1.0 101 Switching Protocols\r\n "; gssize ret; ret = web_socket_util_parse_status_line (line, strlen (line), NULL, NULL, NULL); g_assert_cmpint (ret, ==, strlen (line) - 2); } static void test_parse_status_not_enough (void) { const gchar *data = "HTTP/"; gssize ret; ret = web_socket_util_parse_status_line (data, strlen (data), NULL, NULL, NULL); g_assert_cmpint (ret, ==, 0); } static void test_parse_status_bad (void) { const gchar *lines[] = { " HTTP/1.0 101 Switching Protocols\r\n ", "HTTP/1.0 101\r\n ", "HTTP/1.1 1A01 Switching Protocols \r\n ", "TESTONE\r\n ", }; gssize ret; gint i; for (i = 0; i < G_N_ELEMENTS (lines); i++) { ret = web_socket_util_parse_status_line (lines[i], strlen (lines[i]), NULL, NULL, NULL); g_assert_cmpint (ret, ==, -1); } } static void test_parse_version_1_0 (void) { gchar *version; const gchar *line; gssize ret; line = "HTTP/1.0 101 Switching Protocols\r\n "; ret = web_socket_util_parse_status_line (line, strlen (line), &version, NULL, NULL); g_assert_cmpint (ret, ==, strlen (line) - 2); g_assert_cmpstr (version, ==, "HTTP/1.0"); g_free (version); } static void test_parse_version_1_1 (void) { gchar *version; const gchar *line; gssize ret; line = "HTTP/1.1 101 Switching Protocols\r\n "; ret = web_socket_util_parse_status_line (line, strlen (line), &version, NULL, NULL); g_assert_cmpint (ret, ==, strlen (line) - 2); g_assert_cmpstr (version, ==, "HTTP/1.1"); g_free (version); } static void test_parse_headers (void) { const gchar *input[] = { "Header1: value3\r\n" "Header2: field\r\n" "Head3: Another \r\n" "Host:http://cockpit-project.org\r\n" "\r\n" "BODY ", }; GHashTable *headers; gssize ret; gint i; for (i = 0; i < G_N_ELEMENTS (input); i++) { ret = web_socket_util_parse_headers (input[i], strlen (input[i]), &headers); g_assert_cmpint (ret, ==, strlen (input[i]) - 6); g_assert_cmpstr (g_hash_table_lookup (headers, "header1"), ==, "value3"); g_assert_cmpstr (g_hash_table_lookup (headers, "Header2"), ==, "field"); g_assert_cmpstr (g_hash_table_lookup (headers, "hEAD3"), ==, "Another"); g_assert_cmpstr (g_hash_table_lookup (headers, "Host"), ==, "http://cockpit-project.org"); g_assert (g_hash_table_lookup (headers, "Something else") == NULL); g_hash_table_unref (headers); } } static void test_parse_headers_no_out (void) { const gchar *input = "Header1: value3\r\n" "Header2: field\r\n" "Head3: Another \r\n" "\r\n" "BODY "; gssize ret; ret = web_socket_util_parse_headers (input, strlen (input), NULL); g_assert_cmpint (ret, ==, strlen (input) - 6); } static void test_parse_headers_not_enough (void) { const gchar *input = "Header1: value3\r\n" "Header2: field\r\n" "Head3: Anothe"; gssize ret; ret = web_socket_util_parse_headers (input, strlen (input), NULL); g_assert_cmpint (ret, ==, 0); } static void test_parse_headers_bad (void) { const gchar *input[] = { "Header1 value3\r\n" "\r\n" "BODY ", }; gssize ret; gint i; g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "received invalid header line*"); for (i = 0; i < G_N_ELEMENTS (input); i++) { ret = web_socket_util_parse_headers (input[i], strlen (input[i]), NULL); g_assert_cmpint (ret, ==, -1); } } static void test_header_equals (void) { GHashTable *headers = web_socket_util_new_headers (); g_hash_table_insert (headers, g_strdup ("Blah"), g_strdup ("VALUE")); g_assert (_web_socket_util_header_equals (headers, "blah", "Value")); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "received invalid or missing Blah header*"); g_assert (!_web_socket_util_header_equals (headers, "Blah", "test")); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "received invalid or missing Extra header*"); g_assert (!_web_socket_util_header_equals (headers, "Extra", "test")); g_hash_table_unref (headers); } static void test_header_contains (void) { GHashTable *headers = web_socket_util_new_headers (); g_hash_table_insert (headers, g_strdup ("Blah"), g_strdup ("one two three")); g_assert (_web_socket_util_header_contains (headers, "blah", "one")); g_assert (_web_socket_util_header_contains (headers, "blah", "two")); g_assert (_web_socket_util_header_contains (headers, "blah", "three")); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "received invalid or missing Blah header*"); g_assert (!_web_socket_util_header_contains (headers, "Blah", "thre")); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "received invalid or missing Blah header*"); g_assert (!_web_socket_util_header_contains (headers, "Blah", "four")); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "received invalid or missing Extra header*"); g_assert (!_web_socket_util_header_contains (headers, "Extra", "test")); g_hash_table_unref (headers); } static void test_header_empty (void) { GHashTable *headers = web_socket_util_new_headers (); g_hash_table_insert (headers, g_strdup ("Empty"), g_strdup ("")); g_hash_table_insert (headers, g_strdup ("Blah"), g_strdup ("value")); g_assert (_web_socket_util_header_empty (headers, "empty")); g_assert (_web_socket_util_header_empty (headers, "Another")); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "received unsupported Blah header*"); g_assert (!_web_socket_util_header_empty (headers, "Blah")); g_hash_table_unref (headers); } static void create_iostream_pair (GIOStream **io1, GIOStream **io2) { GSocket *socket1, *socket2; GError *error = NULL; int fds[2]; if (socketpair (PF_UNIX, SOCK_STREAM, 0, fds) < 0) g_assert_not_reached (); socket1 = g_socket_new_from_fd (fds[0], &error); g_assert_no_error (error); socket2 = g_socket_new_from_fd (fds[1], &error); g_assert_no_error (error); *io1 = G_IO_STREAM (g_socket_connection_factory_create_connection (socket1)); *io2 = G_IO_STREAM (g_socket_connection_factory_create_connection (socket2)); g_object_unref (socket1); g_object_unref (socket2); } static gboolean on_error_not_reached (WebSocketConnection *ws, GError *error, gpointer user_data) { /* At this point we know this will fail, but is informative */ g_assert_no_error (error); return TRUE; } static gboolean on_error_copy (WebSocketConnection *ws, GError *error, gpointer user_data) { GError **copy = user_data; g_assert (*copy == NULL); *copy = g_error_copy (error); return TRUE; } static void setup_pair (Test *test, gconstpointer data) { GIOStream *ioc; GIOStream *ios; create_iostream_pair (&ioc, &ios); test->server = web_socket_server_new_for_stream ("ws://localhost/unix", NULL, NULL, ios, NULL, NULL); test->client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, ioc); g_signal_connect (test->server, "error", G_CALLBACK (on_error_not_reached), NULL); g_object_unref (ioc); g_object_unref (ios); } static void teardown (Test *test, gconstpointer data) { g_clear_object (&test->client); g_clear_object (&test->server); } static void on_text_message (WebSocketConnection *ws, WebSocketDataType type, GBytes *message, gpointer user_data) { GBytes **receive = user_data; g_assert_cmpint (type, ==, WEB_SOCKET_DATA_TEXT); g_assert (*receive == NULL); g_assert (message != NULL); *receive = g_bytes_ref (message); } static void on_close_set_flag (WebSocketConnection *ws, gpointer user_data) { gboolean *flag = user_data; g_assert (*flag == FALSE); *flag = TRUE; } static void on_open_set_flag (WebSocketConnection *ws, gpointer user_data) { gboolean *flag = user_data; g_assert (*flag == FALSE); *flag = TRUE; } static void test_handshake (Test *test, gconstpointer data) { gboolean open_event_client = FALSE; gboolean open_event_server = FALSE; GHashTable *headers; g_signal_connect (test->client, "open", G_CALLBACK (on_open_set_flag), &open_event_client); g_signal_connect (test->server, "open", G_CALLBACK (on_open_set_flag), &open_event_server); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_OPEN); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN); headers = web_socket_client_get_headers (WEB_SOCKET_CLIENT (test->client)); g_assert (headers != NULL); #if 0 GHashTableIter iter; gpointer key, value; g_hash_table_iter_init (&iter, headers); while (g_hash_table_iter_next (&iter, &key, &value)) g_printerr ("headers: %s %s\n", (gchar *)key, (gchar *)value); #endif g_assert_cmpstr (g_hash_table_lookup (headers, "connection"), ==, "Upgrade"); g_assert (open_event_client); g_assert (open_event_server); } static void test_send_client_to_server (Test *test, gconstpointer data) { GBytes *sent = NULL; GBytes *received = NULL; const gchar *contents; gsize len; g_signal_connect (test->server, "message", G_CALLBACK (on_text_message), &received); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_OPEN); sent = g_bytes_new ("this is a test", 14); web_socket_connection_send (test->client, WEB_SOCKET_DATA_TEXT, NULL, sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (sent, received)); /* Received messages should be null terminated (outside of len) */ contents = g_bytes_get_data (received, &len); g_assert (contents[len] == '\0'); g_bytes_unref (sent); g_bytes_unref (received); } static void test_send_server_to_client (Test *test, gconstpointer data) { GBytes *sent = NULL; GBytes *received = NULL; const gchar *contents; gsize len; g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN); sent = g_bytes_new ("this is a test", 14); web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (sent, received)); /* Received messages should be null terminated (outside of len) */ contents = g_bytes_get_data (received, &len); g_assert (contents[len] == '\0'); g_bytes_unref (sent); g_bytes_unref (received); } static void test_send_big_packets (Test *test, gconstpointer data) { GBytes *sent = NULL; GBytes *received = NULL; g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN); sent = g_bytes_new_take (g_strnfill (400, '!'), 400); web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (sent, received)); g_bytes_unref (sent); g_bytes_unref (received); received = NULL; sent = g_bytes_new_take (g_strnfill (100 * 1000, '?'), 100 * 1000); web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent); WAIT_UNTIL (received != NULL); g_assert (g_bytes_equal (sent, received)); g_bytes_unref (sent); g_bytes_unref (received); } static void test_send_prefixed (Test *test, gconstpointer data) { GBytes *prefix = NULL; GBytes *payload = NULL; GBytes *received = NULL; g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN); prefix = g_bytes_new_static ("funny ", 6); payload = g_bytes_new_static ("thing", 5); web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, prefix, payload); WAIT_UNTIL (received != NULL); g_assert_cmpstr (g_bytes_get_data (received, NULL), ==, "funny thing"); g_assert_cmpint (g_bytes_get_size (received), ==, 11); g_bytes_unref (payload); g_bytes_unref (prefix); g_bytes_unref (received); } static void test_send_bad_data (Test *test, gconstpointer unused) { GError *error = NULL; GIOStream *io; gsize written; const gchar *frame; guint logid; g_signal_handlers_disconnect_by_func (test->server, on_error_not_reached, NULL); g_signal_connect (test->server, "error", G_CALLBACK (on_error_copy), &error); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); io = web_socket_connection_get_io_stream (test->client); logid = g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, null_log_handler, NULL); /* Bad UTF-8 raw frames */ frame = "\x81\x04\xEE\xEE\xEE\xEE"; if (!g_output_stream_write_all (g_io_stream_get_output_stream (io), frame, 6, &written, NULL, NULL)) g_assert_not_reached (); g_assert_cmpuint (written, ==, 6); WAIT_UNTIL (error != NULL); g_assert_error (error, WEB_SOCKET_ERROR, WEB_SOCKET_CLOSE_BAD_DATA); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED); g_assert_cmpuint (web_socket_connection_get_close_code (test->client), ==, WEB_SOCKET_CLOSE_BAD_DATA); g_error_free (error); g_log_remove_handler (G_LOG_DOMAIN, logid); } static void test_protocol_negotiate (Test *test, gconstpointer unused) { const gchar *server_protocols[] = { "aaa", "bbb", "ccc", NULL }; const gchar *client_protocols[] = { "bbb", "ccc", NULL }; g_object_set (test->server, "protocols", server_protocols, NULL); g_object_set (test->client, "protocols", client_protocols, NULL); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpstr (web_socket_connection_get_protocol (test->client), ==, "bbb"); g_assert_cmpstr (web_socket_connection_get_protocol (test->server), ==, "bbb"); } static void test_protocol_mismatch (Test *test, gconstpointer unused) { GError *error = NULL; guint logid; const gchar *server_protocols[] = { "aaa", "bbb", "ccc", NULL }; const gchar *client_protocols[] = { "ddd", NULL }; g_signal_handlers_disconnect_by_func (test->client, on_error_not_reached, NULL); g_signal_handlers_disconnect_by_func (test->server, on_error_not_reached, NULL); g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy), &error); logid = g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, null_log_handler, NULL); g_object_set (test->server, "protocols", server_protocols, NULL); g_object_set (test->client, "protocols", client_protocols, NULL); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); g_assert_error (error, WEB_SOCKET_ERROR, WEB_SOCKET_CLOSE_PROTOCOL); g_error_free (error); g_log_remove_handler (G_LOG_DOMAIN, logid); } static void test_protocol_server_any (Test *test, gconstpointer unused) { GError *error = NULL; /* Server accepts any protocol */ const gchar *client_protocols[] = { "aaa", "bbb", "ccc", NULL }; g_signal_handlers_disconnect_by_func (test->client, on_error_not_reached, NULL); g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy), &error); g_object_set (test->client, "protocols", client_protocols, NULL); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpstr (web_socket_connection_get_protocol (test->client), ==, "aaa"); g_assert_cmpstr (web_socket_connection_get_protocol (test->server), ==, "aaa"); g_clear_error (&error); } static void test_protocol_client_any (Test *test, gconstpointer unused) { GError *error = NULL; /* Client accepts any protocol */ const gchar *server_protocols[] = { "aaa", "bbb", "ccc", NULL }; g_signal_handlers_disconnect_by_func (test->client, on_error_not_reached, NULL); g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy), &error); g_object_set (test->server, "protocols", server_protocols, NULL); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpstr (web_socket_connection_get_protocol (test->client), ==, "aaa"); g_assert_cmpstr (web_socket_connection_get_protocol (test->server), ==, "aaa"); g_clear_error (&error); } static void test_close_clean_client (Test *test, gconstpointer data) { gboolean close_event_client = FALSE; gboolean close_event_server = FALSE; g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client); g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_OPEN); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_OPEN); web_socket_connection_close (test->client, WEB_SOCKET_CLOSE_GOING_AWAY, "give me a reason"); g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_CLOSING); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_CLOSED); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED); g_assert (close_event_client); g_assert (close_event_server); g_assert_cmpint (web_socket_connection_get_close_code (test->client), ==, WEB_SOCKET_CLOSE_GOING_AWAY); g_assert_cmpint (web_socket_connection_get_close_code (test->server), ==, WEB_SOCKET_CLOSE_GOING_AWAY); g_assert_cmpstr (web_socket_connection_get_close_data (test->server), ==, "give me a reason"); } static void test_close_clean_server (Test *test, gconstpointer data) { gboolean close_event_client = FALSE; gboolean close_event_server = FALSE; g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client); g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_OPEN); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_OPEN); web_socket_connection_close (test->server, WEB_SOCKET_CLOSE_GOING_AWAY, "another reason"); g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_CLOSING); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_CLOSED); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED); g_assert (close_event_client); g_assert (close_event_server); g_assert_cmpint (web_socket_connection_get_close_code (test->server), ==, WEB_SOCKET_CLOSE_GOING_AWAY); g_assert_cmpint (web_socket_connection_get_close_code (test->client), ==, WEB_SOCKET_CLOSE_GOING_AWAY); g_assert_cmpstr (web_socket_connection_get_close_data (test->client), ==, "another reason"); } static void test_close_immediately (void) { WebSocketConnection *client; gboolean close_event = FALSE; client = web_socket_client_new ("ws://localhost/unix", NULL, NULL); g_signal_connect (client, "close", G_CALLBACK (on_close_set_flag), &close_event); g_assert_cmpint (web_socket_connection_get_ready_state (client), ==, WEB_SOCKET_STATE_CONNECTING); web_socket_connection_close (client, 0, NULL); g_assert_cmpint (web_socket_connection_get_ready_state (client), ==, WEB_SOCKET_STATE_CLOSED); g_assert (close_event == TRUE); g_object_unref (client); } static gboolean on_idle_real_close (gpointer data) { WebSocketConnection *ws = data; web_socket_connection_close (ws, 0, NULL); return FALSE; } static gboolean on_closing_send_message (WebSocketConnection *ws, gpointer data) { GBytes *message = data; web_socket_connection_send (ws, WEB_SOCKET_DATA_TEXT, NULL, message); g_signal_handlers_disconnect_by_func (ws, on_closing_send_message, data); g_idle_add (on_idle_real_close, ws); return TRUE; } static void test_message_after_closing (Test *test, gconstpointer data) { gboolean close_event_client = FALSE; gboolean close_event_server = FALSE; GBytes *received = NULL; GBytes *message; message = g_bytes_new ("another test because", 20); g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client); g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received); g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server); g_signal_connect (test->server, "closing", G_CALLBACK (on_closing_send_message), message); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_OPEN); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_OPEN); web_socket_connection_close (test->client, WEB_SOCKET_CLOSE_GOING_AWAY, "another reason"); g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_CLOSING); WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_CLOSED); WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED); g_assert (close_event_client); g_assert (close_event_server); g_assert (received != NULL); g_assert (g_bytes_equal (message, received)); g_bytes_unref (received); g_bytes_unref (message); } static void mock_perform_handshake (GIOStream *io) { GHashTable *headers; gchar buffer[1024]; gssize count; gssize ret; const gchar *key; gchar *accept; gsize written; /* Assumes client codes sends headers as a single write() */ count = g_input_stream_read (g_io_stream_get_input_stream (io), buffer, sizeof (buffer), NULL, NULL); g_assert (count > 0); /* Parse the incoming request */ ret = web_socket_util_parse_req_line (buffer, count, NULL, NULL); g_assert_cmpint (ret, >, 0); ret = web_socket_util_parse_headers (buffer + ret, count - ret, &headers); g_assert_cmpint (ret, >, 0); key = g_hash_table_lookup (headers, "Sec-WebSocket-Key"); accept = _web_socket_complete_accept_key_rfc6455 (key); count = g_snprintf (buffer, sizeof (buffer), "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n" "\r\n", accept); g_free (accept); if (!g_output_stream_write_all (g_io_stream_get_output_stream (io), buffer, count, &written, NULL, NULL)) g_assert_not_reached (); g_assert_cmpuint (count, ==, written); g_hash_table_unref (headers); } static gpointer handshake_then_timeout_server_thread (gpointer user_data) { GIOStream *io = user_data; mock_perform_handshake (io); return NULL; } static void test_close_after_timeout (void) { WebSocketConnection *client; gboolean close_event = FALSE; GIOStream *io_a; GIOStream *io_b; GThread *thread; /* Note that no server is around in this test, so no close happens */ create_iostream_pair (&io_a, &io_b); thread = g_thread_new ("timeout-thread", handshake_then_timeout_server_thread, io_a); client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, io_b); g_signal_connect (client, "close", G_CALLBACK (on_close_set_flag), &close_event); g_signal_connect (client, "error", G_CALLBACK (on_error_not_reached), NULL); WAIT_UNTIL (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_OPEN); /* Now try and close things */ web_socket_connection_close (client, 0, NULL); g_assert_cmpint (web_socket_connection_get_ready_state (client), ==, WEB_SOCKET_STATE_CLOSING); #if 0 /* g_test_expect_message is pretty much incompatible with g_debug() */ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "server did not close io when expected"); #endif WAIT_UNTIL (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_CLOSED); g_assert (close_event == TRUE); g_object_unref (client); /* Now actually close the server side stream */ g_thread_join (thread); g_object_unref (io_a); g_object_unref (io_b); } static gpointer send_fragments_server_thread (gpointer user_data) { GIOStream *io = user_data; gsize written; const gchar fragments[] = "\x01\x04""one " /* !fin | opcode */ "\x00\x04""two " /* !fin | no opcode */ "\x80\x05""three"; /* fin | no opcode */ mock_perform_handshake (io); if (!g_output_stream_write_all (g_io_stream_get_output_stream (io), fragments, sizeof (fragments) -1, &written, NULL, NULL)) g_assert_not_reached (); g_assert_cmpuint (written, ==, sizeof (fragments) - 1); return NULL; } static void test_receive_fragmented (void) { WebSocketConnection *client; GIOStream *io_a; GIOStream *io_b; GThread *thread; GBytes *received = NULL; GBytes *expect; /* Note that no server is around in this test, so no close happens */ create_iostream_pair (&io_a, &io_b); thread = g_thread_new ("fragment-thread", send_fragments_server_thread, io_a); client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, io_b); g_signal_connect (client, "error", G_CALLBACK (on_error_not_reached), NULL); g_signal_connect (client, "message", G_CALLBACK (on_text_message), &received); WAIT_UNTIL (received != NULL); expect = g_bytes_new ("one two three", 13); g_assert (g_bytes_equal (expect, received)); g_bytes_unref (expect); g_bytes_unref (received); g_thread_join (thread); g_object_unref (client); g_object_unref (io_a); g_object_unref (io_b); } static gpointer client_thread (gpointer data) { GIOStream *io = data; GMainContext *context; WebSocketConnection *client; context = g_main_context_new (); g_main_context_push_thread_default (context); client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, io); g_signal_connect (client, "error", G_CALLBACK (on_error_not_reached), NULL); while (web_socket_connection_get_ready_state (client) != WEB_SOCKET_STATE_CLOSED) g_main_context_iteration (context, TRUE); g_main_context_pop_thread_default (context); g_main_context_unref (context); g_object_unref (client); return NULL; } static void test_handshake_with_buffer_and_headers (void) { WebSocketConnection *server; GHashTable *headers; GByteArray *input; gchar buffer[1024]; GIOStream *ioc; GIOStream *ios; gssize count; gssize in1, in2; GThread *thread; create_iostream_pair (&ioc, &ios); thread = g_thread_new ("client-thread", client_thread, ioc); count = g_input_stream_read (g_io_stream_get_input_stream (ios), buffer, sizeof (buffer), NULL, NULL); g_assert_cmpint (count, >, 0); /* Parse the incoming request */ in1 = web_socket_util_parse_req_line (buffer, count, NULL, NULL); g_assert_cmpint (in1, >, 0); in2 = web_socket_util_parse_headers (buffer + in1, count - in1, &headers); g_assert_cmpint (in2, >, 0); /* Make a buffer for the rest */ input = g_byte_array_new (); g_byte_array_append (input, (guchar *)buffer + (in1 + in2), count - (in1 + in2)); server = web_socket_server_new_for_stream ("ws://localhost/unix", NULL, NULL, ios, headers, input); g_signal_connect (server, "error", G_CALLBACK (on_error_not_reached), NULL); WAIT_UNTIL (web_socket_connection_get_ready_state (server) != WEB_SOCKET_STATE_CONNECTING); g_assert_cmpint (web_socket_connection_get_ready_state (server), ==, WEB_SOCKET_STATE_OPEN); web_socket_connection_close (server, 0, NULL); WAIT_UNTIL (web_socket_connection_get_ready_state (server) == WEB_SOCKET_STATE_CLOSED); g_byte_array_unref (input); g_hash_table_unref (headers); g_object_unref (server); g_thread_join (thread); g_object_unref (ioc); g_object_unref (ios); } int main (int argc, char *argv[]) { gchar *name; gint j; struct { void (* func) (Test *, gconstpointer); const gchar *name; } tests_with_client_server_pair[] = { { test_handshake, "handshake" }, { test_send_client_to_server, "send-client-to-server" }, { test_send_server_to_client, "send-server-to-client" }, { test_send_big_packets, "send-big-packets" }, { test_send_prefixed, "send-prefixed" }, { test_send_bad_data, "send-bad-data" }, { test_protocol_negotiate, "protocol-negotiate" }, { test_protocol_mismatch, "protocol-mismatch" }, { test_protocol_server_any, "protocol-server-any" }, { test_protocol_client_any, "protocol-client-any" }, { test_close_clean_client, "close-clean-client" }, { test_close_clean_server, "close-clean-server" }, }; signal (SIGPIPE, SIG_IGN); g_setenv ("GSETTINGS_BACKEND", "memory", TRUE); g_setenv ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE); g_setenv ("GIO_USE_VFS", "local", TRUE); #if !GLIB_CHECK_VERSION(2,36,0) g_type_init (); #endif g_set_prgname ("test-websocket"); g_test_init (&argc, &argv, NULL); g_test_add_func ("/web-socket/parse-url", test_parse_url); g_test_add_func ("/web-socket/parse-url-no-out", test_parse_url_no_out); g_test_add_func ("/web-socket/parse-url-bad", test_parse_url_bad); g_test_add_func ("/web-socket/parse-url-no-path", test_parse_url_no_path); g_test_add_func ("/web-socket/parse-url-with-user", test_parse_url_with_user); g_test_add_func ("/web-socket/parse-req", test_parse_req); g_test_add_func ("/web-socket/parse-req-no-out", test_parse_req_no_out); g_test_add_func ("/web-socket/parse-req-not-enough", test_parse_req_not_enough); g_test_add_func ("/web-socket/parse-req-bad", test_parse_req_bad); g_test_add_func ("/web-socket/parse-status", test_parse_status); g_test_add_func ("/web-socket/parse-status-no-out", test_parse_status_no_out); g_test_add_func ("/web-socket/parse-status-not-enough", test_parse_status_not_enough); g_test_add_func ("/web-socket/parse-status-bad", test_parse_status_bad); g_test_add_func ("/web-socket/parse-version-1-0", test_parse_version_1_0); g_test_add_func ("/web-socket/parse-version-1-1", test_parse_version_1_1); g_test_add_func ("/web-socket/parse-headers", test_parse_headers); g_test_add_func ("/web-socket/parse-headers-no-out", test_parse_headers_no_out); g_test_add_func ("/web-socket/parse-headers-bad", test_parse_headers_bad); g_test_add_func ("/web-socket/parse-headers-not-enough", test_parse_headers_not_enough); g_test_add_func ("/web-socket/header-equals", test_header_equals); g_test_add_func ("/web-socket/header-contains", test_header_contains); g_test_add_func ("/web-socket/header-empty", test_header_empty); for (j = 0; j < G_N_ELEMENTS (tests_with_client_server_pair); j++) { name = g_strdup_printf ("/web-socket/%s", tests_with_client_server_pair[j].name); g_test_add (name, Test, NULL, setup_pair, tests_with_client_server_pair[j].func, teardown); g_free (name); } g_test_add_func ("/web-socket/close-immediately", test_close_immediately); if (g_test_slow ()) g_test_add_func ("/web-socket/close-after-timeout", test_close_after_timeout); g_test_add_func ("/web-socket/receive-fragmented", test_receive_fragmented); g_test_add_func ("/web-socket/handshake-with-buffer-headers", test_handshake_with_buffer_and_headers); g_test_add ("/web-socket/message-after-closing", Test, NULL, setup_pair, test_message_after_closing, teardown); return g_test_run (); }