/* * This file is part of Cockpit. * * Copyright (C) 2015 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 "cockpitwebsocketstream.h" #include "cockpitchannel.h" #include "common/cockpittest.h" #include "common/cockpitjson.h" #include "common/cockpitwebresponse.h" #include "common/cockpitwebserver.h" #include "websocket/websocketclient.h" #include "mock-transport.h" #include static void on_closed_get_problem (CockpitChannel *channel, const gchar *problem, gpointer user_data) { gchar **result = user_data; g_assert (problem != NULL); g_assert (*result == NULL); *result = g_strdup (problem); } typedef struct { MockTransport *transport; CockpitWebServer *server; GIOStream *client; guint port; gchar *origin; gchar *url; gboolean ws_closed; } TestCase; static void on_socket_message (WebSocketConnection *self, WebSocketDataType type, GBytes *message, gpointer user_data) { GByteArray *array = g_bytes_unref_to_array (g_bytes_ref (message)); GBytes *payload; guint i; /* Capitalize and relay back */ for (i = 0; i < array->len; i++) array->data[i] = g_ascii_toupper (array->data[i]); payload = g_byte_array_free_to_bytes (array); web_socket_connection_send (self, type, NULL, payload); g_bytes_unref (payload); } static void on_socket_close (WebSocketConnection *ws, gpointer user_data) { TestCase *test = user_data; g_object_unref (ws); test->ws_closed = TRUE; } static gboolean handle_socket (CockpitWebServer *server, const gchar *original_path, const gchar *path, const gchar *method, GIOStream *io_stream, GHashTable *headers, GByteArray *input, gpointer data) { const gchar *origins[] = { NULL, NULL }; const gchar *protocols[] = { "one", "two", "three", NULL }; WebSocketConnection *ws = NULL; TestCase *test = data; if (!g_str_equal (path, "/socket")) return FALSE; /* HEAD on a socket makes little sense, we don't test it */ g_assert (g_strcmp0 (method, "GET") == 0); origins[0] = test->origin; ws = web_socket_server_new_for_stream (test->url, (const gchar **)origins, protocols, io_stream, headers, input); g_signal_connect (ws, "message", G_CALLBACK (on_socket_message), NULL); g_signal_connect (ws, "close", G_CALLBACK (on_socket_close), test); return TRUE; } static void setup (TestCase *test, gconstpointer data) { test->server = cockpit_web_server_new (NULL, 0, NULL, NULL, NULL); test->port = cockpit_web_server_get_port (test->server); test->transport = mock_transport_new (); test->ws_closed = FALSE; test->origin = g_strdup_printf ("http://localhost:%u", test->port); test->url = g_strdup_printf ("ws://localhost:%u/socket", test->port); g_signal_connect (test->server, "handle-stream", G_CALLBACK (handle_socket), test); } static void teardown (TestCase *test, gconstpointer data) { g_object_unref (test->server); g_object_unref (test->transport); g_free (test->origin); g_free (test->url); cockpit_assert_expected (); } static void test_basic (TestCase *test, gconstpointer data) { JsonObject *options; CockpitChannel *channel; GBytes *bytes = NULL; GBytes *recv = NULL; options = json_object_new (); json_object_set_int_member (options, "port", test->port); json_object_set_string_member (options, "payload", "websocket-stream1"); json_object_set_string_member (options, "path", "/socket"); channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM, "transport", test->transport, "id", "444", "options", options, NULL); json_object_unref (options); bytes = g_bytes_new ("Message", 7); cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), "444", bytes); g_bytes_unref (bytes); while (mock_transport_count_sent (test->transport) < 3) g_main_context_iteration (NULL, TRUE); recv = mock_transport_pop_channel (test->transport, "444"); cockpit_assert_bytes_eq (recv, "MESSAGE", 7); g_bytes_unref (recv); cockpit_channel_close (channel, "ending"); while (!test->ws_closed) g_main_context_iteration (NULL, TRUE); g_object_unref (channel); } static void test_bad_origin (TestCase *test, gconstpointer data) { JsonObject *options; CockpitChannel *channel; gchar *problem = NULL; options = json_object_new (); json_object_set_int_member (options, "port", test->port); json_object_set_string_member (options, "payload", "websocket-stream1"); json_object_set_string_member (options, "path", "/socket"); g_free (test->origin); test->origin = g_strdup ("bad-origin"); channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM, "transport", test->transport, "id", "444", "options", options, NULL); g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem); json_object_unref (options); while (problem == NULL) g_main_context_iteration (NULL, TRUE); g_assert_cmpstr (problem, ==, "protocol-error"); while (!test->ws_closed) g_main_context_iteration (NULL, TRUE); g_object_unref (channel); g_free (problem); } typedef struct { GTlsCertificate *certificate; MockTransport *transport; CockpitWebServer *server; guint port; gchar *origin; gchar *url; gboolean ws_closed; } TestTls; static void setup_tls (TestTls *test, gconstpointer data) { GError *error = NULL; test->certificate = g_tls_certificate_new_from_files (SRCDIR "/src/bridge/mock-server.crt", SRCDIR "/src/bridge/mock-server.key", &error); g_assert_no_error (error); test->server = cockpit_web_server_new (NULL, 0, test->certificate, NULL, &error); g_assert_no_error (error); test->port = cockpit_web_server_get_port (test->server); test->transport = mock_transport_new (); test->ws_closed = FALSE; test->origin = g_strdup_printf ("https://localhost:%u", test->port); test->url = g_strdup_printf ("wss://localhost:%u/socket", test->port); g_signal_connect (test->server, "handle-stream", G_CALLBACK (handle_socket), test); } static void teardown_tls (TestTls *test, gconstpointer data) { g_object_unref (test->certificate); g_object_unref (test->server); g_object_unref (test->transport); g_free (test->origin); g_free (test->url); } static const gchar fixture_tls_authority_good[] = "{ \"authority\": { \"file\": \"" SRCDIR "/src/bridge/mock-server.crt\" } }"; static void test_tls_authority_good (TestTls *test, gconstpointer json) { JsonObject *tls; JsonObject *options; CockpitChannel *channel; GBytes *bytes = NULL; GBytes *recv = NULL; GError *error = NULL; tls = cockpit_json_parse_object (json, -1, &error); g_assert_no_error (error); options = json_object_new (); json_object_set_int_member (options, "port", test->port); json_object_set_string_member (options, "payload", "websocket-stream1"); json_object_set_string_member (options, "path", "/socket"); json_object_set_object_member (options, "tls", tls); channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM, "transport", test->transport, "id", "444", "options", options, NULL); json_object_unref (options); bytes = g_bytes_new ("Message", 7); cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), "444", bytes); g_bytes_unref (bytes); while (mock_transport_count_sent (test->transport) < 3) g_main_context_iteration (NULL, TRUE); recv = mock_transport_pop_channel (test->transport, "444"); cockpit_assert_bytes_eq (recv, "MESSAGE", 7); g_bytes_unref (recv); cockpit_channel_close (channel, "ending"); while (!test->ws_closed) g_main_context_iteration (NULL, TRUE); g_object_unref (channel); } static const gchar fixture_tls_authority_bad[] = "{ \"authority\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.crt\" } }"; static void test_tls_authority_bad (TestTls *test, gconstpointer json) { CockpitChannel *channel; JsonObject *options; JsonObject *tls; GError *error = NULL; GBytes *bytes; JsonObject *resp; gchar *expected_pem = NULL; gchar *expected_json = NULL; g_object_get (test->certificate, "certificate-pem", &expected_pem, NULL); g_assert_true (expected_pem != NULL); tls = cockpit_json_parse_object (json, -1, &error); g_assert_no_error (error); options = json_object_new (); json_object_set_int_member (options, "port", test->port); json_object_set_string_member (options, "payload", "websocket-stream1"); json_object_set_string_member (options, "path", "/socket"); json_object_set_object_member (options, "tls", tls); channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM, "transport", test->transport, "id", "444", "options", options, NULL); json_object_unref (options); bytes = g_bytes_new ("Message", 7); cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), "444", bytes); g_bytes_unref (bytes); while (mock_transport_count_sent (test->transport) < 1) g_main_context_iteration (NULL, TRUE); resp = mock_transport_pop_control (test->transport); expected_json = g_strdup_printf ("{\"command\":\"close\",\"channel\":\"444\",\"problem\":\"unknown-hostkey\", " " \"rejected-certificate\":\"%s\"}", expected_pem); cockpit_assert_json_eq (resp, expected_json); g_object_unref (channel); g_free (expected_pem); g_free (expected_json); } int main (int argc, char *argv[]) { cockpit_test_init (&argc, &argv); g_test_add ("/websocket-stream/test_basic", TestCase, NULL, setup, test_basic, teardown); g_test_add ("/websocket-stream/test_bad_origin", TestCase, NULL, setup, test_bad_origin, teardown); g_test_add ("/websocket/tls/authority-good", TestTls, fixture_tls_authority_good, setup_tls, test_tls_authority_good, teardown_tls); g_test_add ("/websocket/tls/authority-bad", TestTls, fixture_tls_authority_bad, setup_tls, test_tls_authority_bad, teardown_tls); return g_test_run (); }