diff options
Diffstat (limited to 'tests/hsts-test.c')
-rw-r--r-- | tests/hsts-test.c | 408 |
1 files changed, 408 insertions, 0 deletions
diff --git a/tests/hsts-test.c b/tests/hsts-test.c new file mode 100644 index 00000000..aa77d6e2 --- /dev/null +++ b/tests/hsts-test.c @@ -0,0 +1,408 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2018 Igalia S.L. + * Copyright (C) 2018 Metrological Group B.V. + */ + +#include <stdio.h> +#include "test-utils.h" + +SoupURI *http_uri; +SoupURI *https_uri; + +/* This server pseudo-implements the HSTS spec in order to allow us to + test the Soup HSTS feature. + */ +static void +server_callback (SoupServer *server, SoupMessage *msg, + const char *path, GHashTable *query, + SoupClientContext *context, gpointer data) +{ + const char *server_protocol = data; + + if (strcmp (server_protocol, "http") == 0) { + if (strcmp (path, "/insecure") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=31536000"); + soup_message_set_status (msg, SOUP_STATUS_OK); + } else { + char *uri_string; + SoupURI *uri = soup_uri_new ("https://localhost"); + soup_uri_set_path (uri, path); + uri_string = soup_uri_to_string (uri, FALSE); + soup_message_set_redirect (msg, SOUP_STATUS_MOVED_PERMANENTLY, uri_string); + soup_uri_free (uri); + g_free (uri_string); + } + } else if (strcmp (server_protocol, "https") == 0) { + soup_message_set_status (msg, SOUP_STATUS_OK); + if (strcmp (path, "/long-lasting") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=31536000"); + } else if (strcmp (path, "/two-seconds") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=2"); + } else if (strcmp (path, "/three-seconds") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=3"); + } else if (strcmp (path, "/delete") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=0"); + } else if (strcmp (path, "/subdomains") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains"); + } else if (strcmp (path, "/multiple-headers") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains"); + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=0; includeSubDomains"); + } else if (strcmp (path, "/missing-values") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + ""); + } else if (strcmp (path, "/invalid-values") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=foo"); + } else if (strcmp (path, "/extra-values-0") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=3600; foo"); + } else if (strcmp (path, "/extra-values-1") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + " max-age=3600; includeDomains; foo"); + } else if (strcmp (path, "/extra-values-2") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "max-age=3600; includeDomains; includeDomains"); + } else if (strcmp (path, "/case-insensitive") == 0) { + soup_message_headers_append (msg->response_headers, + "Strict-Transport-Security", + "MAX-AGE=3600; includesubdomains"); + } + } +} + +static void +session_get_uri (SoupSession *session, const char *uri, SoupStatus expected_status) +{ + SoupMessage *msg; + + msg = soup_message_new ("GET", uri); + soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT); + soup_session_send_message (session, msg); + soup_test_assert_message_status (msg, expected_status); + g_object_unref (msg); +} + +/* The HSTS specification does not handle custom ports, so we need to + * rewrite the URI in the request and add the port where the server is + * listening before it is sent, to be able to connect to the localhost + * port where the server is actually running. + */ +static void +rewrite_message_uri (SoupMessage *msg) +{ + if (soup_uri_get_scheme (soup_message_get_uri (msg)) == SOUP_URI_SCHEME_HTTP) + soup_uri_set_port (soup_message_get_uri (msg), soup_uri_get_port (http_uri)); + else if (soup_uri_get_scheme (soup_message_get_uri (msg)) == SOUP_URI_SCHEME_HTTPS) + soup_uri_set_port (soup_message_get_uri (msg), soup_uri_get_port (https_uri)); + else + g_assert_not_reached(); +} + +static void +on_message_restarted (SoupMessage *msg, + gpointer data) +{ + rewrite_message_uri (msg); +} + +static void +on_request_queued (SoupSession *session, + SoupMessage *msg, + gpointer data) +{ + g_signal_connect (msg, "restarted", G_CALLBACK (on_message_restarted), NULL); + + rewrite_message_uri (msg); +} + +static SoupSession * +hsts_session_new (SoupHSTSEnforcer *enforcer) +{ + SoupSession *session; + if (!enforcer) + enforcer = soup_hsts_enforcer_new (); + + session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, + SOUP_SESSION_USE_THREAD_CONTEXT, TRUE, + SOUP_SESSION_ADD_FEATURE, enforcer, + NULL); + g_signal_connect (session, "request-queued", G_CALLBACK (on_request_queued), NULL); + + return session; +} + + +static void +do_hsts_basic_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + session_get_uri (session, "https://localhost/long-lasting", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + + /* The HSTS headers in the url above doesn't incldue + subdomains, so the request should ask for the unchanged + HTTP address below, to which the server will respond with a + moved permanently status. */ + session_get_uri (session, "http://subdomain.localhost", SOUP_STATUS_MOVED_PERMANENTLY); + + soup_test_session_abort_unref (session); +} + +static void +do_hsts_expire_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + + session_get_uri (session, "https://localhost/two-seconds", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + /* Wait for the policy to expire. */ + sleep (3); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + + soup_test_session_abort_unref (session); +} + +static void +do_hsts_delete_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + session_get_uri (session, "https://localhost/delete", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + + soup_test_session_abort_unref (session); +} + +static void +do_hsts_replace_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/long-lasting", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + session_get_uri (session, "https://localhost/two-seconds", SOUP_STATUS_OK); + /* Wait for the policy to expire. */ + sleep (3); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + + soup_test_session_abort_unref (session); +} + +static void +do_hsts_update_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/three-seconds", SOUP_STATUS_OK); + sleep (2); + session_get_uri (session, "https://localhost/three-seconds", SOUP_STATUS_OK); + sleep (2); + + /* At this point, 4 seconds have elapsed since setting the 3 seconds HSTS + rule for the first time, and it should have expired by now, but since it + was updated, it should still be valid. */ + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_set_and_delete_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/long-lasting", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + session_get_uri (session, "https://localhost/delete", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + + soup_test_session_abort_unref (session); +} + +static void +do_hsts_persistency_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/long-lasting", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + soup_test_session_abort_unref (session); + + session = hsts_session_new (NULL); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_subdomains_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/subdomains", SOUP_STATUS_OK); + /* The enforcer should cause the request to ask for an HTTPS + uri, which will fail with an SSL error as there's no server + in subdomain.localhost. */ + session_get_uri (session, "http://subdomain.localhost", SOUP_STATUS_SSL_FAILED); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_multiple_headers_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/multiple-headers", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost/multiple-headers", SOUP_STATUS_OK); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_insecure_sts_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "http://localhost/insecure", SOUP_STATUS_OK); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_missing_values_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/missing-values", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_invalid_values_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/invalid-values", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_extra_values_test (void) +{ + for (int i = 0; i < 3; i++) { + SoupSession *session = hsts_session_new (NULL); + char *uri = g_strdup_printf ("https://localhost/extra-values-%i", i); + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + soup_test_session_abort_unref (session); + g_free (uri); + } +} + +static void +do_hsts_case_insensitive_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/case-insensitive", SOUP_STATUS_OK); + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_ip_address_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://127.0.0.1/basic", SOUP_STATUS_OK); + session_get_uri (session, "http://127.0.0.1/", SOUP_STATUS_MOVED_PERMANENTLY); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_utf8_address_test (void) +{ + SoupSession *session = hsts_session_new (NULL); + session_get_uri (session, "https://localhost/subdomains", SOUP_STATUS_OK); + /* The enforcer should cause the request to ask for an HTTPS + uri, which will fail with an SSL error as there's no server + in subdomain.localhost. */ + session_get_uri (session, "http://食狮.中国.localhost", SOUP_STATUS_SSL_FAILED); + soup_test_session_abort_unref (session); +} + +static void +do_hsts_session_policy_test (void) +{ + SoupHSTSEnforcer *enforcer = soup_hsts_enforcer_new (); + SoupSession *session = hsts_session_new (enforcer); + + session_get_uri (session, "http://localhost", SOUP_STATUS_MOVED_PERMANENTLY); + soup_hsts_enforcer_set_session_policy (enforcer, "localhost", FALSE); + session_get_uri (session, "http://localhost", SOUP_STATUS_OK); + + soup_test_session_abort_unref (session); + g_object_unref (enforcer); +} + +int +main (int argc, char **argv) +{ + int ret; + SoupServer *server; + SoupServer *https_server; + + test_init (argc, argv, NULL); + + server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD); + soup_server_add_handler (server, NULL, server_callback, "http", NULL); + http_uri = soup_test_server_get_uri (server, "http", NULL); + + if (tls_available) { + https_server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD); + soup_server_add_handler (https_server, NULL, server_callback, "https", NULL); + https_uri = soup_test_server_get_uri (https_server, "https", NULL); + } + + g_test_add_func ("/hsts/basic", do_hsts_basic_test); + g_test_add_func ("/hsts/expire", do_hsts_expire_test); + g_test_add_func ("/hsts/delete", do_hsts_delete_test); + g_test_add_func ("/hsts/replace", do_hsts_replace_test); + g_test_add_func ("/hsts/update", do_hsts_update_test); + g_test_add_func ("/hsts/set_and_delete", do_hsts_set_and_delete_test); + g_test_add_func ("/hsts/persistency", do_hsts_persistency_test); + g_test_add_func ("/hsts/subdomains", do_hsts_subdomains_test); + g_test_add_func ("/hsts/multiple-headers", do_hsts_multiple_headers_test); + g_test_add_func ("/hsts/insecure-sts", do_hsts_insecure_sts_test); + g_test_add_func ("/hsts/missing-values", do_hsts_missing_values_test); + g_test_add_func ("/hsts/invalid-values", do_hsts_invalid_values_test); + g_test_add_func ("/hsts/extra-values", do_hsts_extra_values_test); + g_test_add_func ("/hsts/case-insensitive", do_hsts_case_insensitive_test); + g_test_add_func ("/hsts/ip-address", do_hsts_ip_address_test); + g_test_add_func ("/hsts/utf8-address", do_hsts_utf8_address_test); + g_test_add_func ("/hsts/session-policy", do_hsts_session_policy_test); + + ret = g_test_run (); + + soup_uri_free (http_uri); + soup_test_server_quit_unref (server); + + if (tls_available) { + soup_uri_free (https_uri); + soup_test_server_quit_unref (https_server); + } + + test_cleanup (); + return ret; +} |