/* * Copyright (C) 2019 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "connection.h" #include "testing.h" #include "server.h" #include "utils.h" #include "testlib/cockpittest.h" #define SOCKET_ACTIVATION_HELPER BUILDDIR "/socket-activation-helper" #define COCKPIT_WS BUILDDIR "/cockpit-ws" /* this has a corresponding mock-server.key */ #define CERTFILE SRCDIR "/test/data/mock-server.crt" #define KEYFILE SRCDIR "/test/data/mock-server.key" #define ECC_CERTFILE SRCDIR "/test/data/mock-ecc.crt" #define ECC_KEYFILE SRCDIR "/test/data/mock-ecc.key" #define CLIENT_CERTFILE SRCDIR "/src/tls/ca/alice.pem" #define CLIENT_KEYFILE SRCDIR "/src/tls/ca/alice.key" #define ALTERNATE_CERTFILE SRCDIR "/src/tls/ca/bob.pem" #define ALTERNATE_KEYFILE SRCDIR "/src/tls/ca/bob.key" #define CLIENT_EXPIRED_CERTFILE SRCDIR "/src/tls/ca/alice-expired.pem" typedef struct { gchar *ws_socket_dir; gchar *runtime_dir; gchar *clients_dir; gchar *cgroup_line; GPid ws_spawner; struct sockaddr_in6 server_addr; } TestCase; typedef struct { const char *certfile; const char *keyfile; const char *certfile2; const char *keyfile2; int cert_request_mode; int idle_timeout; const char *client_crt; const char *client_key; const char *client_fingerprint; const char *priority; int expected_pk_algo; unsigned int max_connection_count; } TestFixture; static const TestFixture fixture_max_connection_count_10 = { .max_connection_count = 10, }; static const TestFixture fixture_separate_crt_key = { .certfile = CERTFILE, .keyfile = KEYFILE, }; static const TestFixture fixture_separate_crt_key_client_cert = { .certfile = CERTFILE, .keyfile = KEYFILE, .cert_request_mode = GNUTLS_CERT_REQUEST, .client_crt = CLIENT_CERTFILE, .client_key = CLIENT_KEYFILE, .client_fingerprint = CLIENT_CERT_FINGERPRINT, }; static const TestFixture fixture_expired_client_cert = { .certfile = CERTFILE, .keyfile = KEYFILE, .cert_request_mode = GNUTLS_CERT_REQUEST, .client_crt = CLIENT_EXPIRED_CERTFILE, .client_key = CLIENT_KEYFILE, .client_fingerprint = CLIENT_EXPIRED_FINGERPRINT, }; static const TestFixture fixture_alternate_client_cert = { .certfile = CERTFILE, .keyfile = KEYFILE, .cert_request_mode = GNUTLS_CERT_REQUEST, .client_crt = ALTERNATE_CERTFILE, .client_key = ALTERNATE_KEYFILE, .client_fingerprint = ALTERNATE_FINGERPRINT, }; static const TestFixture fixture_run_idle = { .idle_timeout = 1, }; static const TestFixture fixture_multiple_certs_ecdsa = { .certfile = CERTFILE, .keyfile = KEYFILE, .certfile2 = ECC_CERTFILE, .keyfile2 = ECC_KEYFILE, .priority = "NORMAL:-SIGN-ALL:+SIGN-ECDSA-SECP256R1-SHA256", .expected_pk_algo = GNUTLS_PK_ECDSA, }; static const TestFixture fixture_multiple_certs_rsa = { .certfile = CERTFILE, .keyfile = KEYFILE, .certfile2 = ECC_CERTFILE, .keyfile2 = ECC_KEYFILE, .priority = "NORMAL:-SIGN-ALL:+SIGN-RSA-SHA256:+SIGN-RSA-PSS-RSAE-SHA256", .expected_pk_algo = GNUTLS_PK_RSA, }; /* for forking test cases, where server's SIGCHLD handling gets in the way */ static void block_sigchld (void) { const struct sigaction child_action = { .sa_handler = SIG_DFL }; g_assert_cmpint (sigaction (SIGCHLD, &child_action, NULL), ==, 0); } /* check if we have a client certificate for a given cgroup */ static bool check_for_certfile (TestCase *tc, char **out_contents) { g_autoptr(GDir) dir = g_dir_open (tc->clients_dir, 0, NULL); g_assert (dir != NULL); const char *name; while ((name = g_dir_read_name (dir))) { g_autofree char *filename = g_build_filename (tc->clients_dir, name, NULL); g_autofree char *contents = NULL; g_autoptr(GError) error = NULL; if (!g_file_get_contents (filename, &contents, NULL, &error)) { /* files are flying around all the time: this might reasonably fail */ if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) continue; g_assert_no_error (error); g_assert_not_reached (); } if (g_str_has_prefix (contents, tc->cgroup_line)) { if (out_contents) *out_contents = g_steal_pointer (&contents); return true; } } return false; } static int do_connect (TestCase *tc) { int fd = socket (AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0); g_assert_cmpint (fd, >, 0); if (connect (fd, (struct sockaddr *) &tc->server_addr, sizeof (tc->server_addr)) < 0) { close (fd); return -errno; } else { return fd; } } /* Explicitly connect using IPv4 to the dual-stack server */ static int do_connect_ipv4 (TestCase *tc) { struct sockaddr_in sa; int fd = socket (AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); g_assert_cmpint (fd, >, 0); memset (&sa, 0, sizeof (sa)); sa.sin_family = AF_INET; sa.sin_addr.s_addr = htonl (INADDR_LOOPBACK); sa.sin_port = tc->server_addr.sin6_port; if (connect (fd, (struct sockaddr *) &sa, sizeof (sa)) < 0) { close (fd); return -errno; } else { return fd; } } /* Connect using IPv4-mapped IPv6 address (::ffff:x.x.x.x) */ static int do_connect_ipv4_mapped (TestCase *tc, const char *ipv4_mapped_addr) { struct sockaddr_in6 sa; int fd = socket (AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0); g_assert_cmpint (fd, >, 0); /* Use server_addr but change address to specified IPv4-mapped address */ memcpy (&sa, &tc->server_addr, sizeof (sa)); g_assert_cmpint (inet_pton (AF_INET6, ipv4_mapped_addr, &sa.sin6_addr), ==, 1); if (connect (fd, (struct sockaddr *) &sa, sizeof (sa)) < 0) { close (fd); return -errno; } else { return fd; } } static void send_request (int fd, const char *request) { g_assert_cmpint (fd, >, 0); g_assert_cmpint (write (fd, request, strlen (request)), ==, strlen (request)); } static const char* recv_reply (int fd, char *buf, size_t buflen) { ssize_t len; len = recv (fd, buf, buflen - 1, MSG_DONTWAIT); close (fd); if (len < 0) g_error ("recv_reply: unexpected error: %m"); g_assert_cmpint (len, >=, 50); buf[len] = '\0'; /* so that we can use string functions on it */ return buf; } static const char* do_request_fd (int fd, const char *request) { static char buf[4096]; int res; send_request (fd, request); /* wait until data is available */ for (int timeout = 0; timeout < 100; ++timeout) { res = recv (fd, buf, 100, MSG_PEEK | MSG_DONTWAIT); if (res >= 50) return recv_reply (fd, buf, sizeof (buf)); server_poll_event (100); } g_error ("timed out waiting for enough data to become available: res=%d, error: %m", res); } static const char* do_request (TestCase *tc, const char *request) { int fd = do_connect (tc); return do_request_fd (fd, request); } static void assert_http (TestCase *tc) { const char *res = do_request (tc, "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"); /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */ if (strstr (res, "200 OK")) { cockpit_assert_strmatch (res, "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Content-Security-Policy: connect-src 'self' http://localhost ws://localhost;*"); } else { cockpit_assert_strmatch (res, "HTTP/1.1 404 Not Found\r\nContent-Type: text/html*"); } } static void assert_https_outcome (TestCase *tc, const TestFixture *fixture, unsigned expected_server_certs, bool expect_tls_failure) { pid_t pid; int status = -1; block_sigchld (); /* do the connection in a subprocess, as gnutls_handshake is synchronous */ pid = fork (); if (pid < 0) g_error ("failed to fork: %m"); if (pid == 0) { const char request[] = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; char buf[4096]; gnutls_session_t session; gnutls_certificate_credentials_t xcred; const gnutls_datum_t *server_certs; unsigned server_certs_len; ssize_t len; int ret; int fd = do_connect (tc); g_assert_cmpint (fd, >, 0); g_assert_cmpint (gnutls_init (&session, GNUTLS_CLIENT), ==, GNUTLS_E_SUCCESS); gnutls_transport_set_int (session, fd); if (fixture && fixture->priority) g_assert_cmpint (gnutls_priority_set_direct (session, fixture->priority, NULL), ==, GNUTLS_E_SUCCESS); else g_assert_cmpint (gnutls_set_default_priority (session), ==, GNUTLS_E_SUCCESS); gnutls_handshake_set_timeout(session, 5000); g_assert_cmpint (gnutls_certificate_allocate_credentials (&xcred), ==, GNUTLS_E_SUCCESS); g_assert_cmpint (gnutls_certificate_set_x509_system_trust (xcred), >=, 0); if (fixture && fixture->client_crt) { g_assert (fixture->client_key); g_assert_cmpint (gnutls_certificate_set_x509_key_file (xcred, fixture->client_crt, fixture->client_key, GNUTLS_X509_FMT_PEM), ==, GNUTLS_E_SUCCESS);; } g_assert_cmpint (gnutls_credentials_set (session, GNUTLS_CRD_CERTIFICATE, xcred), ==, GNUTLS_E_SUCCESS); ret = gnutls_handshake (session); if (ret != GNUTLS_E_SUCCESS) { if (expect_tls_failure) exit (0); else g_error ("Handshake failed: %s", gnutls_strerror (ret)); } /* check server certificate */ server_certs = gnutls_certificate_get_peers (session, &server_certs_len); g_assert (server_certs); g_assert_cmpuint (server_certs_len, ==, expected_server_certs); /* check server certificate algorithm if requested */ if (fixture && fixture->expected_pk_algo) { gnutls_x509_crt_t cert; g_assert_cmpint (gnutls_x509_crt_init (&cert), ==, GNUTLS_E_SUCCESS); g_assert_cmpint (gnutls_x509_crt_import (cert, &server_certs[0], GNUTLS_X509_FMT_DER), ==, GNUTLS_E_SUCCESS); g_assert_cmpint (gnutls_x509_crt_get_pk_algorithm (cert, NULL), ==, fixture->expected_pk_algo); gnutls_x509_crt_deinit (cert); } /* send request, read response */ len = gnutls_record_send (session, request, sizeof (request)); if (len < 0 && expect_tls_failure) exit (0); g_assert_cmpint (len, ==, sizeof (request)); len = gnutls_record_recv (session, buf, sizeof (buf) - 1); if (len < 0 && expect_tls_failure) exit (0); g_assert_cmpint (len, >=, 100); g_assert_cmpint (len, <=, sizeof (buf) - 1); buf[len] = '\0'; /* so that we can use string functions on it */ /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */ if (strstr (buf, "200 OK")) { cockpit_assert_strmatch (buf, "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Content-Security-Policy: connect-src 'self' https://localhost wss://localhost;*"); } else { cockpit_assert_strmatch (buf, "HTTP/1.1 404 Not Found\r\nContent-Type: text/html*"); } /* check client certificate in state dir */ if (fixture && fixture->client_crt && tc->cgroup_line) { if (fixture->cert_request_mode != GNUTLS_CERT_IGNORE) { g_autofree char *cert_file = NULL; g_autofree char *expected_pem = NULL; g_assert (check_for_certfile (tc, &cert_file)); g_assert (g_file_get_contents (fixture->client_crt, &expected_pem, NULL, NULL)); g_assert (g_str_has_suffix (cert_file, expected_pem)); } else { g_assert (!check_for_certfile (tc, NULL)); } } g_assert_cmpint (gnutls_bye (session, GNUTLS_SHUT_RDWR), ==, GNUTLS_E_SUCCESS); g_assert_false (expect_tls_failure); close (fd); exit (0); } for (int retry = 0; retry < 100 && waitpid (pid, &status, WNOHANG) <= 0; ++retry) server_poll_event (200); g_assert_cmpint (status, ==, 0); /* cleans up client certificate after closing connection */ g_assert (!check_for_certfile (tc, NULL)); } static void assert_https (TestCase *tc, const TestFixture *fixture, unsigned expected_server_certs) { assert_https_outcome (tc, fixture, expected_server_certs, false); } static void setup (TestCase *tc, gconstpointer data) { const TestFixture *fixture = data; g_autoptr(GError) error = NULL; alarm (120); tc->ws_socket_dir = g_dir_make_tmp ("server.wssock.XXXXXX", NULL); g_assert (tc->ws_socket_dir); /* This absolutely must be on a real filesystem: overlayfs (as often * seen for /tmp in containers) doesn't work. /dev/shm is always * tmpfs, which works nicely (and matches what we expect to be at /run * when we use this code in production). */ char runtime_dir_template[] = "/dev/shm/server.runtime.XXXXXX"; tc->runtime_dir = g_mkdtemp (runtime_dir_template); g_assert (tc->runtime_dir); tc->runtime_dir = g_strdup (tc->runtime_dir); tc->clients_dir = g_build_filename (tc->runtime_dir, "clients", NULL); if (fixture && fixture->client_fingerprint) tc->cgroup_line = g_strdup_printf ("0::/system.slice/system-cockpithttps.slice/cockpit-wsinstance-https@%s.service\n", fixture->client_fingerprint); gchar* sah_argv[] = { SOCKET_ACTIVATION_HELPER, COCKPIT_WS, tc->ws_socket_dir, NULL }; if (!g_spawn_async (NULL, sah_argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &tc->ws_spawner, &error)) g_error ("Failed to spawn " SOCKET_ACTIVATION_HELPER ": %s", error->message); /* wait until socket activation helper is ready */ int socket_dir_fd = open (tc->ws_socket_dir, O_RDONLY | O_DIRECTORY); g_assert_cmpint (socket_dir_fd, >=, 0); for (int retry = 0; retry < 200; ++retry) { if (faccessat (socket_dir_fd, "ready", F_OK, 0) == 0) break; g_usleep (10000); } close (socket_dir_fd); unsigned max_connection_count = (fixture && fixture->max_connection_count != 0) ? fixture->max_connection_count : 500; /* Let the kernel assign a port */ server_init (tc->ws_socket_dir, tc->runtime_dir, fixture ? fixture->idle_timeout : 0, 0, max_connection_count); if (fixture && fixture->certfile) { /* Set up certs directory with 0.crt and 0.key */ g_autofree gchar *certs_dir = g_build_filename (tc->runtime_dir, "tls/server", NULL); g_assert_cmpint (g_mkdir_with_parents (certs_dir, 0700), ==, 0); const char *sources[] = { fixture->certfile, fixture->keyfile, fixture->certfile2, fixture->keyfile2 }; const char *dests[] = { "0.crt", "0.key", "1.crt", "1.key" }; for (int i = 0; i < G_N_ELEMENTS (sources) && sources[i]; i++) { g_autofree gchar *contents = NULL; g_autofree gchar *dest = g_build_filename (certs_dir, dests[i], NULL); g_assert (g_file_get_contents (sources[i], &contents, NULL, NULL)); g_assert (g_file_set_contents (dest, contents, -1, NULL)); } int cert_dirfd = open (certs_dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC); g_assert_cmpint (cert_dirfd, >=, 0); connection_crypto_init (cert_dirfd, false, fixture->cert_request_mode); close (cert_dirfd); } /* Figure out the socket address we ought to connect to */ socklen_t addrlen = sizeof tc->server_addr; int r = getsockname (server_get_listener (), (struct sockaddr *) &tc->server_addr, &addrlen); g_assert_no_errno (r); /* Sanity check */ g_assert_cmpint (addrlen, ==, sizeof tc->server_addr); g_assert_cmpint (tc->server_addr.sin6_family, ==, AF_INET6); } static void teardown (TestCase *tc, gconstpointer data) { for (int i = 0; i < 100 && server_num_connections (); i++) /* 10s */ g_usleep (100000); /* 0.1s */ server_cleanup (); g_assert_cmpint (kill (tc->ws_spawner, SIGTERM), ==, 0); g_assert_cmpint (waitpid (tc->ws_spawner, NULL, 0), ==, tc->ws_spawner); /* all children got cleaned up */ g_assert_cmpint (wait (NULL), ==, -1); g_assert_cmpint (errno, ==, ECHILD); /* connection should fail */ /* coverity[leaked_handle : FALSE] */ g_assert_cmpint (do_connect (tc), ==, -ECONNREFUSED); g_unsetenv ("COCKPIT_WS_PROCESS_IDLE"); int socket_dir_fd = open (tc->ws_socket_dir, O_RDONLY | O_DIRECTORY); g_assert_cmpint (socket_dir_fd, >=, 0); g_assert_cmpint (unlinkat (socket_dir_fd, "http.sock", 0), ==, 0); g_assert_cmpint (unlinkat (socket_dir_fd, "https-factory.sock", 0), ==, 0); g_assert_cmpint (unlinkat (socket_dir_fd, "https@" SHA256_NIL ".sock", 0), ==, 0); g_assert_cmpint (unlinkat (socket_dir_fd, "https@" CLIENT_CERT_FINGERPRINT ".sock", 0), ==, 0); g_assert_cmpint (unlinkat (socket_dir_fd, "https@" CLIENT_EXPIRED_FINGERPRINT ".sock", 0), ==, 0); g_assert_cmpint (unlinkat (socket_dir_fd, "https@" ALTERNATE_FINGERPRINT ".sock", 0), ==, 0); g_assert_cmpint (unlinkat (socket_dir_fd, "ready", 0), ==, 0); close (socket_dir_fd); g_assert_cmpint (g_rmdir (tc->ws_socket_dir), ==, 0); g_free (tc->ws_socket_dir); g_free (tc->cgroup_line); g_assert_cmpint (g_rmdir (tc->clients_dir), ==, 0); g_free (tc->clients_dir); /* We use only rmdir() to verity that certificate_load() definitely * deleted all of the key material. We want to make sure we don't see * `ENOTEMPTY`. */ g_autofree gchar *tls_dir = g_build_filename (tc->runtime_dir, "tls", NULL); g_autofree gchar *certs_dir = g_build_filename (tls_dir, "server", NULL); if (g_rmdir (certs_dir) != 0 && errno != ENOENT) g_error ("rmdir %s: %s", certs_dir, g_strerror (errno)); if (g_rmdir (tls_dir) != 0 && errno != ENOENT) g_error ("rmdir %s: %s", tls_dir, g_strerror (errno)); g_assert_cmpint (g_rmdir (tc->runtime_dir), ==, 0); g_free (tc->runtime_dir); alarm (0); } static void test_no_tls_single (TestCase *tc, gconstpointer data) { g_assert_cmpuint (server_num_connections (), ==, 0); assert_http (tc); /* let the server process "peer has closed connection" */ for (int retries = 0; retries < 10 && server_num_connections () == 1; ++retries) server_poll_event (100); g_assert_cmpuint (server_num_connections (), ==, 0); } static void test_no_tls_many_serial (TestCase *tc, gconstpointer data) { for (int i = 0; i < 20; ++i) assert_http (tc); } static void test_tls_blocked_handshake (TestCase *tc, gconstpointer data) { block_sigchld (); pid_t pid = fork (); if (pid == -1) g_error ("fork failed: %m"); if (pid == 0) { /* child */ gint first_fd = do_connect (tc); send_request (first_fd, "\x16"); /* start the TLS handshake */ /* Make sure the byte gets there before the next connection request */ sleep (1); /* make sure we can do a second connection while the first one is * blocked in the handshake */ gint second_fd = do_connect (tc); send_request (second_fd, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"); /* wait 10 seconds for the reply */ struct pollfd pfd = { .fd = second_fd, .events = POLLIN }; g_assert_cmpint (poll (&pfd, 1, 10000), ==, 1); close (second_fd); close (first_fd); exit (0); } else { /* parent */ int status; while (waitpid (pid, &status, WNOHANG) <= 0) server_poll_event (50); g_assert_cmpint (status, ==, 0); } } static void test_no_tls_many_parallel (TestCase *tc, gconstpointer data) { int i; block_sigchld (); for (i = 0; i < 20; ++i) { pid_t pid = fork (); if (pid < 0) g_error ("failed to fork: %m"); if (pid > 0) continue; /* child */ char buf[4096]; int fd = do_connect (tc); server_cleanup (); send_request (fd, "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"); /* wait until data is available */ for (int timeout = 0; timeout < 10 && recv (fd, buf, 100, MSG_PEEK | MSG_DONTWAIT) < 100; ++timeout) sleep (1); recv_reply (fd, buf, sizeof (buf)); /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */ if (strstr (buf, "200 OK")) cockpit_assert_strmatch (buf, "HTTP/1.1 200 OK*"); else cockpit_assert_strmatch (buf, "HTTP/1.1 404 Not Found*"); exit (0); } /* wait until all i child processes have finished */ while (i > 0) { int status; int r = waitpid (-1, &status, WNOHANG); g_assert_cmpint (r, >=, 0); if (r == 0) { server_poll_event (50); } else { g_assert_cmpint (status, ==, 0); --i; } } } static void test_no_tls_too_many_parallel (TestCase *tc, gconstpointer data) { int i; int fds[20]; const char *request = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; for (i = 0; i < 20; ++i) { fds[i] = do_connect (tc); g_assert_cmpint (fds[i], >, 0); server_poll_event (50); } for (i = 0; i < 20; ++i) { if (i < 10) { /* the first 10 are allowed to connect and will receive a reply */ do_request_fd (fds[i], request); } else { /* the rest gets disconnected immediately */ int r = write (fds[i], request, strlen (request)); g_assert (r == -1 && errno == ECONNRESET); } close(fds[i]); } } static void test_no_tls_redirect (TestCase *tc, gconstpointer data) { struct sockaddr *sa = (struct sockaddr *) &tc->server_addr; /* Server uses IPv6 dual-stack socket */ g_assert_cmpint (sa->sa_family, ==, AF_INET6); /* Connect to non-localhost address (127.0.0.2 as IPv4-mapped IPv6) to test redirect logic */ int fd = do_connect_ipv4_mapped (tc, "::ffff:127.0.0.2"); g_assert_cmpint (fd, >, 0); /* without TLS support it should not redirect */ const char *res = do_request_fd (fd, "GET / HTTP/1.0\r\nHost: some.remote:1234\r\n\r\n"); /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */ if (strstr (res, "200 OK")) cockpit_assert_strmatch (res, "HTTP/1.1 200 OK*"); else cockpit_assert_strmatch (res, "HTTP/1.1 404 Not Found*"); } static void test_tls_no_client_cert (TestCase *tc, gconstpointer data) { assert_https (tc, data, 1); } static void test_tls_no_server_cert (TestCase *tc, gconstpointer data) { assert_http (tc); assert_https_outcome (tc, data, 0, true); assert_http (tc); } static void test_tls_redirect (TestCase *tc, gconstpointer data) { struct sockaddr *sa = (struct sockaddr *) &tc->server_addr; /* Server uses IPv6 dual-stack socket */ g_assert_cmpint (sa->sa_family, ==, AF_INET6); /* Connect to non-localhost address (127.0.0.2 as IPv4-mapped IPv6) to test redirect logic */ int fd = do_connect_ipv4_mapped (tc, "::ffff:127.0.0.2"); g_assert_cmpint (fd, >, 0); /* with TLS support it should redirect */ const char *res = do_request_fd (fd, "GET / HTTP/1.0\r\nHost: some.remote:1234\r\n\r\n"); cockpit_assert_strmatch (res, "HTTP/1.1 301 Moved Permanently*"); } static void test_tls_redirect_header_safety (TestCase *tc, gconstpointer data) { /* verify that the redirect response doesn't contain any headers from the request, * possibly through crafted host/path values. */ int fd = do_connect_ipv4_mapped (tc, "::ffff:127.0.0.2"); g_assert_cmpint (fd, >, 0); const char *res = do_request_fd (fd, "GET /test HTTP/1.0\r\nHost: some.remote:1234\r\nX-Injected: evil\r\n\r\n"); cockpit_assert_strmatch (res, "HTTP/1.1 301 Moved Permanently*"); cockpit_assert_strmatch (res, "*Location: https://some.remote:1234/test*"); g_assert_null (strstr (res, "X-Injected")); g_assert_null (strstr (res, "evil")); /* Request headers should not leak into response */ fd = do_connect_ipv4_mapped (tc, "::ffff:127.0.0.2"); g_assert_cmpint (fd, >, 0); res = do_request_fd (fd, "GET /path?query HTTP/1.0\r\nHost: example.org\r\nCookie: secret=data\r\n\r\n"); cockpit_assert_strmatch (res, "HTTP/1.1 301 Moved Permanently*"); cockpit_assert_strmatch (res, "*Location: https://example.org/path?query*"); g_assert_null (strstr (res, "Cookie")); g_assert_null (strstr (res, "secret")); /* Verify URL-encoded CRLF in path is safely handled (preserved as literal %0D%0A) */ fd = do_connect_ipv4_mapped (tc, "::ffff:127.0.0.2"); g_assert_cmpint (fd, >, 0); res = do_request_fd (fd, "GET /path%0D%0A HTTP/1.0\r\nHost: example.org\r\n\r\n"); cockpit_assert_strmatch (res, "HTTP/1.1 301 Moved Permanently*"); cockpit_assert_strmatch (res, "*Location: https://example.org/path%0D%0A*"); /* The URL encoding should not be decoded into actual CRLF */ g_assert_null (strstr (res, "path\r\n")); g_assert_null (strstr (res, "path\n")); } static void test_tls_client_cert (TestCase *tc, gconstpointer data) { assert_https (tc, data, 1); /* no-cert case is handled by separate ws; pass NULL fixture to not use a client cert */ assert_https (tc, NULL, 1); assert_https (tc, data, 1); } static void test_tls_client_cert_disabled (TestCase *tc, gconstpointer data) { assert_https (tc, data, 1); /* no-cert case is handled by same ws, as client certs are disabled server-side; pass NULL fixture to not use a client cert */ assert_https (tc, NULL, 1); } static void test_tls_client_cert_expired (TestCase *tc, gconstpointer data) { /* expect_tls_failure==true only does a coarse-grained check that the request * fails anywhere during handshake or the first send/recv. GnuTLS 3.6.4 * introduces TLS 1.3 by default, which has only a two-step handshake: that * does not pick up the server's late failing handshake from the verify * function, only the next read/write attempt does */ assert_https_outcome (tc, data, 1, true); } static void test_tls_client_cert_parallel (TestCase *tc, gconstpointer data) { const TestFixture *fixture = data; gboolean alternate = strcmp (fixture->client_fingerprint, ALTERNATE_FINGERPRINT) == 0; pid_t pid; int status; /* HACK: This testcase runs slowly under valgrind and sometimes fails * inexplicably. It's not likely that we're going to find leaks in * the code with this test, so running it under valgrind is just * costing us pain. Disable it for now. */ if (cockpit_test_skip_slow ()) return; block_sigchld (); /* do the connection in a subprocess, as gnutls_handshake is synchronous */ pid = fork (); if (pid < 0) g_error ("failed to fork: %m"); if (pid == 0) { gnutls_certificate_credentials_t xcred; const unsigned n_connections = 20; int fds[n_connections]; gnutls_session_t sessions[n_connections]; g_assert_cmpint (gnutls_certificate_allocate_credentials (&xcred), ==, GNUTLS_E_SUCCESS); g_assert_cmpint (gnutls_certificate_set_x509_system_trust (xcred), >=, 0); g_assert_cmpint (gnutls_certificate_set_x509_key_file (xcred, fixture->client_crt, fixture->client_key, GNUTLS_X509_FMT_PEM), ==, GNUTLS_E_SUCCESS); g_assert (!check_for_certfile (tc, NULL)); /* start parallel connections; we don't actually need to send/receive anything (i. e. talk to cockpit-ws) -- * certificate export and refcounting is entirely done on the client → cockpit-tls side */ for (unsigned i = 0; i < n_connections; ++i) { fds[i] = do_connect (tc); g_assert_cmpint (fds[i], >, 0); g_assert_cmpint (gnutls_init (&sessions[i], GNUTLS_CLIENT), ==, GNUTLS_E_SUCCESS); gnutls_transport_set_int (sessions[i], fds[i]); g_assert_cmpint (gnutls_set_default_priority (sessions[i]), ==, GNUTLS_E_SUCCESS); g_assert_cmpint (gnutls_credentials_set (sessions[i], GNUTLS_CRD_CERTIFICATE, xcred), ==, GNUTLS_E_SUCCESS); gnutls_handshake_set_timeout(sessions[i], 5000); g_assert_cmpint (gnutls_handshake (sessions[i]), ==, GNUTLS_E_SUCCESS); /* the file should be written on first connection, and just exist for the next ones. * * In the "alternate" mode we will receive a "hello" message to tell us that the * server is active (by which time the file will have been created). For the * other case, we have to wait for it to appear. * */ if (alternate) { char buffer[6]; ssize_t s; do s = gnutls_record_recv (sessions[i], buffer, sizeof buffer); while (s == GNUTLS_E_INTERRUPTED); g_assert_cmpint (s, ==, 5); g_assert (memcmp (buffer, "hello", 5) == 0); } else { if (i == 0) { for (int retry = 0; retry < 100; ++retry) { if (check_for_certfile (tc, NULL)) break; g_usleep (10000); } } } g_assert (check_for_certfile (tc, NULL)); } /* close the connections again, all but the last one */ for (unsigned i = 0; i < n_connections - 1; ++i) { g_assert_cmpint (gnutls_bye (sessions[i], GNUTLS_SHUT_RDWR), ==, GNUTLS_E_SUCCESS); close (fds[i]); } if (!alternate) { /* The certificate file should still exist for the last connection, but it might * not *yet* exist (if the last connection failed to initialise before all the * other connections exited, which is a race that we've seen in practice). Wait * for it, as above. */ for (int retry = 0;; ++retry) { g_assert_cmpint (retry, <, 100); if (check_for_certfile (tc, NULL)) break; g_usleep (10000); } } else { /* In the "alternate" case there should be no such strange * races. */ g_assert (check_for_certfile (tc, NULL)); } /* closing last connection removes it */ g_assert_cmpint (gnutls_bye (sessions[n_connections - 1], GNUTLS_SHUT_RDWR), ==, GNUTLS_E_SUCCESS); close (fds[n_connections - 1]); for (int retry = 0; retry < 100; ++retry) { if (!check_for_certfile (tc, NULL)) break; g_usleep (10000); } g_assert (!check_for_certfile (tc, NULL)); exit (0); } for (int retry = 0; retry < 200 && waitpid (pid, &status, WNOHANG) <= 0; ++retry) server_poll_event (100); g_assert_cmpint (status, ==, 0); } static void test_mixed_protocols (TestCase *tc, gconstpointer data) { assert_https (tc, data, 1); assert_http (tc); assert_https (tc, data, 1); assert_http (tc); } static void test_run_idle (TestCase *tc, gconstpointer data) { /* exits after idle without any connections */ server_run (); /* exits after idle after processing an event */ assert_http (tc); server_run (); } static void test_ipv4_connection (TestCase *tc, gconstpointer data) { /* Explicitly connect using IPv4 to the dual-stack server */ int fd = do_connect_ipv4 (tc); g_assert_cmpint (fd, >, 0); const char *reply = do_request_fd (fd, "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"); /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */ if (strstr (reply, "200 OK")) cockpit_assert_strmatch (reply, "HTTP/1.1 200 OK*"); else cockpit_assert_strmatch (reply, "HTTP/1.1 404 Not Found*"); } int main (int argc, char *argv[]) { cockpit_test_init (&argc, &argv); g_test_add ("/server/no-tls/single-request", TestCase, NULL, setup, test_no_tls_single, teardown); g_test_add ("/server/no-tls/many-serial", TestCase, NULL, setup, test_no_tls_many_serial, teardown); g_test_add ("/server/no-tls/many-parallel", TestCase, NULL, setup, test_no_tls_many_parallel, teardown); g_test_add ("/server/no-tls/too-many-parallel", TestCase, &fixture_max_connection_count_10, setup, test_no_tls_too_many_parallel, teardown); g_test_add ("/server/no-tls/redirect", TestCase, NULL, setup, test_no_tls_redirect, teardown); g_test_add ("/server/tls/no-client-cert", TestCase, &fixture_separate_crt_key, setup, test_tls_no_client_cert, teardown); g_test_add ("/server/tls/client-cert", TestCase, &fixture_separate_crt_key_client_cert, setup, test_tls_client_cert, teardown); g_test_add ("/server/tls/client-cert-disabled", TestCase, &fixture_separate_crt_key, setup, test_tls_client_cert_disabled, teardown); g_test_add ("/server/tls/client-cert-expired", TestCase, &fixture_expired_client_cert, setup, test_tls_client_cert_expired, teardown); g_test_add ("/server/tls/client-cert-parallel", TestCase, &fixture_separate_crt_key_client_cert, setup, test_tls_client_cert_parallel, teardown); g_test_add ("/server/tls/client-cert-parallel/alternate", TestCase, &fixture_alternate_client_cert, setup, test_tls_client_cert_parallel, teardown); g_test_add ("/server/tls/no-server-cert", TestCase, NULL, setup, test_tls_no_server_cert, teardown); g_test_add ("/server/tls/redirect", TestCase, &fixture_separate_crt_key, setup, test_tls_redirect, teardown); g_test_add ("/server/tls/redirect-header-safety", TestCase, &fixture_separate_crt_key, setup, test_tls_redirect_header_safety, teardown); g_test_add ("/server/tls/blocked-handshake", TestCase, &fixture_separate_crt_key, setup, test_tls_blocked_handshake, teardown); g_test_add ("/server/mixed-protocols", TestCase, &fixture_separate_crt_key, setup, test_mixed_protocols, teardown); g_test_add ("/server/tls/multiple-certs/ecdsa", TestCase, &fixture_multiple_certs_ecdsa, setup, test_tls_no_client_cert, teardown); g_test_add ("/server/tls/multiple-certs/rsa", TestCase, &fixture_multiple_certs_rsa, setup, test_tls_no_client_cert, teardown); g_test_add ("/server/run-idle", TestCase, &fixture_run_idle, setup, test_run_idle, teardown); g_test_add ("/server/ipv4/connection", TestCase, NULL, setup, test_ipv4_connection, teardown); return g_test_run (); }