blob: 2644680e173e1f3d75fdd963eb3dd8ae5bf18e0d [file] [log] [blame]
/*
* Copyright (C) 2012 Igalia S.L.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "LoadTrackingTest.h"
#include "WebKitTestServer.h"
static WebKitTestServer* kHttpsServer;
static WebKitTestServer* kHttpServer;
static const char* indexHTML = "<html><body>Testing WebKit2GTK+ SSL</body></htmll>";
static const char* insecureContentHTML = "<html><script src=\"%s\"></script><body><p>Text + image <img src=\"%s\" align=\"right\"/></p></body></html>";
static const char TLSExpectedSuccessTitle[] = "WebKit2Gtk+ TLS permission test";
static const char TLSSuccessHTMLString[] = "<html><head><title>WebKit2Gtk+ TLS permission test</title></head><body></body></html>";
class SSLTest: public LoadTrackingTest {
public:
MAKE_GLIB_TEST_FIXTURE(SSLTest);
SSLTest()
: m_tlsErrors(static_cast<GTlsCertificateFlags>(0))
{
}
virtual void provisionalLoadFailed(const gchar* failingURI, GError* error)
{
g_assert_error(error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED);
LoadTrackingTest::provisionalLoadFailed(failingURI, error);
}
virtual void loadCommitted()
{
GTlsCertificate* certificate = 0;
webkit_web_view_get_tls_info(m_webView, &certificate, &m_tlsErrors);
m_certificate = certificate;
LoadTrackingTest::loadCommitted();
}
void waitUntilLoadFinished()
{
m_certificate = 0;
m_tlsErrors = static_cast<GTlsCertificateFlags>(0);
LoadTrackingTest::waitUntilLoadFinished();
}
GRefPtr<GTlsCertificate> m_certificate;
GTlsCertificateFlags m_tlsErrors;
};
static void testSSL(SSLTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
WebKitTLSErrorsPolicy originalPolicy = webkit_web_context_get_tls_errors_policy(context);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
test->loadURI(kHttpsServer->getURIForPath("/").data());
test->waitUntilLoadFinished();
g_assert_nonnull(test->m_certificate);
// Self-signed certificate has a nullptr issuer.
g_assert_null(g_tls_certificate_get_issuer(test->m_certificate.get()));
// We always expect errors because we are using a self-signed certificate,
// but only G_TLS_CERTIFICATE_UNKNOWN_CA flags should be present.
g_assert_cmpuint(test->m_tlsErrors, ==, G_TLS_CERTIFICATE_UNKNOWN_CA);
// Non HTTPS loads shouldn't have a certificate nor errors.
test->loadHtml(indexHTML, 0);
test->waitUntilLoadFinished();
g_assert_null(test->m_certificate);
g_assert_cmpuint(test->m_tlsErrors, ==, 0);
webkit_web_context_set_tls_errors_policy(context, originalPolicy);
}
class InsecureContentTest: public WebViewTest {
public:
MAKE_GLIB_TEST_FIXTURE(InsecureContentTest);
InsecureContentTest()
: m_insecureContentRun(false)
, m_insecureContentDisplayed(false)
{
g_signal_connect(m_webView, "insecure-content-detected", G_CALLBACK(insecureContentDetectedCallback), this);
}
static void insecureContentDetectedCallback(WebKitWebView* webView, WebKitInsecureContentEvent event, InsecureContentTest* test)
{
g_assert_true(webView == test->m_webView);
if (event == WEBKIT_INSECURE_CONTENT_RUN)
test->m_insecureContentRun = true;
if (event == WEBKIT_INSECURE_CONTENT_DISPLAYED)
test->m_insecureContentDisplayed = true;
}
bool m_insecureContentRun;
bool m_insecureContentDisplayed;
};
static void testInsecureContent(InsecureContentTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
WebKitTLSErrorsPolicy originalPolicy = webkit_web_context_get_tls_errors_policy(context);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
test->loadURI(kHttpsServer->getURIForPath("/insecure-content/").data());
test->waitUntilLoadFinished();
g_assert_false(test->m_insecureContentRun);
// Images are currently always displayed, even bypassing mixed content settings. Check
// https://bugs.webkit.org/show_bug.cgi?id=142469
g_assert_true(test->m_insecureContentDisplayed);
webkit_web_context_set_tls_errors_policy(context, originalPolicy);
}
static bool assertIfSSLRequestProcessed = false;
static void testTLSErrorsPolicy(SSLTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
// TLS errors are treated as transport failures by default.
g_assert_cmpint(webkit_web_context_get_tls_errors_policy(context), ==, WEBKIT_TLS_ERRORS_POLICY_FAIL);
assertIfSSLRequestProcessed = true;
test->loadURI(kHttpsServer->getURIForPath("/").data());
test->waitUntilLoadFinished();
g_assert_true(test->m_loadFailed);
g_assert_true(test->m_loadEvents.contains(LoadTrackingTest::ProvisionalLoadFailed));
g_assert_false(test->m_loadEvents.contains(LoadTrackingTest::LoadCommitted));
assertIfSSLRequestProcessed = false;
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
g_assert_cmpint(webkit_web_context_get_tls_errors_policy(context), ==, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
test->m_loadFailed = false;
test->loadURI(kHttpsServer->getURIForPath("/").data());
test->waitUntilLoadFinished();
g_assert_false(test->m_loadFailed);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_FAIL);
g_assert_cmpint(webkit_web_context_get_tls_errors_policy(context), ==, WEBKIT_TLS_ERRORS_POLICY_FAIL);
}
static void testTLSErrorsRedirect(SSLTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
WebKitTLSErrorsPolicy originalPolicy = webkit_web_context_get_tls_errors_policy(context);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_FAIL);
assertIfSSLRequestProcessed = true;
test->loadURI(kHttpsServer->getURIForPath("/redirect").data());
test->waitUntilLoadFinished();
g_assert_true(test->m_loadFailed);
g_assert_true(test->m_loadEvents.contains(LoadTrackingTest::ProvisionalLoadFailed));
g_assert_false(test->m_loadEvents.contains(LoadTrackingTest::LoadCommitted));
assertIfSSLRequestProcessed = false;
webkit_web_context_set_tls_errors_policy(context, originalPolicy);
}
static gboolean webViewAuthenticationCallback(WebKitWebView*, WebKitAuthenticationRequest* request)
{
g_assert_not_reached();
return TRUE;
}
static void testTLSErrorsHTTPAuth(SSLTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
WebKitTLSErrorsPolicy originalPolicy = webkit_web_context_get_tls_errors_policy(context);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_FAIL);
assertIfSSLRequestProcessed = true;
g_signal_connect(test->m_webView, "authenticate", G_CALLBACK(webViewAuthenticationCallback), NULL);
test->loadURI(kHttpsServer->getURIForPath("/auth").data());
test->waitUntilLoadFinished();
g_assert_true(test->m_loadFailed);
g_assert_true(test->m_loadEvents.contains(LoadTrackingTest::ProvisionalLoadFailed));
g_assert_false(test->m_loadEvents.contains(LoadTrackingTest::LoadCommitted));
assertIfSSLRequestProcessed = false;
webkit_web_context_set_tls_errors_policy(context, originalPolicy);
}
class TLSErrorsTest: public SSLTest {
public:
MAKE_GLIB_TEST_FIXTURE(TLSErrorsTest);
TLSErrorsTest()
: m_tlsErrors(static_cast<GTlsCertificateFlags>(0))
, m_failingURI(nullptr)
{
}
~TLSErrorsTest()
{
if (m_failingURI)
soup_uri_free(m_failingURI);
}
bool loadFailedWithTLSErrors(const char* failingURI, GTlsCertificate* certificate, GTlsCertificateFlags tlsErrors) override
{
LoadTrackingTest::loadFailedWithTLSErrors(failingURI, certificate, tlsErrors);
assertObjectIsDeletedWhenTestFinishes(G_OBJECT(certificate));
m_certificate = certificate;
m_tlsErrors = tlsErrors;
if (m_failingURI)
soup_uri_free(m_failingURI);
m_failingURI = soup_uri_new(failingURI);
return true;
}
GTlsCertificate* certificate() const { return m_certificate.get(); }
GTlsCertificateFlags tlsErrors() const { return m_tlsErrors; }
const char* host() const { return m_failingURI->host; }
private:
GRefPtr<GTlsCertificate> m_certificate;
GTlsCertificateFlags m_tlsErrors;
SoupURI* m_failingURI;
};
static void testLoadFailedWithTLSErrors(TLSErrorsTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
WebKitTLSErrorsPolicy originalPolicy = webkit_web_context_get_tls_errors_policy(context);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_FAIL);
assertIfSSLRequestProcessed = true;
// The load-failed-with-tls-errors signal should be emitted when there is a TLS failure.
test->loadURI(kHttpsServer->getURIForPath("/test-tls/").data());
test->waitUntilLoadFinished();
g_assert_true(G_IS_TLS_CERTIFICATE(test->certificate()));
g_assert_cmpuint(test->tlsErrors(), ==, G_TLS_CERTIFICATE_UNKNOWN_CA);
g_assert_cmpstr(test->host(), ==, soup_uri_get_host(kHttpsServer->baseURI()));
g_assert_cmpint(test->m_loadEvents[0], ==, LoadTrackingTest::ProvisionalLoadStarted);
g_assert_cmpint(test->m_loadEvents[1], ==, LoadTrackingTest::LoadFailedWithTLSErrors);
g_assert_cmpint(test->m_loadEvents[2], ==, LoadTrackingTest::LoadFinished);
assertIfSSLRequestProcessed = false;
// Test allowing an exception for this certificate on this host.
webkit_web_context_allow_tls_certificate_for_host(context, test->certificate(), test->host());
// The page should now load without errors.
test->loadURI(kHttpsServer->getURIForPath("/test-tls/").data());
test->waitUntilLoadFinished();
g_assert_cmpint(test->m_loadEvents[0], ==, LoadTrackingTest::ProvisionalLoadStarted);
g_assert_cmpint(test->m_loadEvents[1], ==, LoadTrackingTest::LoadCommitted);
g_assert_cmpint(test->m_loadEvents[2], ==, LoadTrackingTest::LoadFinished);
g_assert_cmpstr(webkit_web_view_get_title(test->m_webView), ==, TLSExpectedSuccessTitle);
webkit_web_context_set_tls_errors_policy(context, originalPolicy);
}
class TLSSubresourceTest : public WebViewTest {
public:
MAKE_GLIB_TEST_FIXTURE(TLSSubresourceTest);
static void resourceLoadStartedCallback(WebKitWebView* webView, WebKitWebResource* resource, WebKitURIRequest* request, TLSSubresourceTest* test)
{
if (webkit_web_view_get_main_resource(test->m_webView) == resource)
return;
// Ignore favicons.
if (g_str_has_suffix(webkit_uri_request_get_uri(request), "favicon.ico"))
return;
test->subresourceLoadStarted(resource);
}
TLSSubresourceTest()
: m_tlsErrors(static_cast<GTlsCertificateFlags>(0))
{
g_signal_connect(m_webView, "resource-load-started", G_CALLBACK(resourceLoadStartedCallback), this);
}
static void subresourceFailedCallback(WebKitWebResource*, GError*)
{
g_assert_not_reached();
}
static void subresourceFailedWithTLSErrorsCallback(WebKitWebResource* resource, GTlsCertificate* certificate, GTlsCertificateFlags tlsErrors, TLSSubresourceTest* test)
{
test->subresourceFailedWithTLSErrors(resource, certificate, tlsErrors);
}
void subresourceLoadStarted(WebKitWebResource* resource)
{
g_signal_connect(resource, "failed", G_CALLBACK(subresourceFailedCallback), nullptr);
g_signal_connect(resource, "failed-with-tls-errors", G_CALLBACK(subresourceFailedWithTLSErrorsCallback), this);
}
void subresourceFailedWithTLSErrors(WebKitWebResource* resource, GTlsCertificate* certificate, GTlsCertificateFlags tlsErrors)
{
m_certificate = certificate;
m_tlsErrors = tlsErrors;
g_main_loop_quit(m_mainLoop);
}
void waitUntilSubresourceLoadFail()
{
g_main_loop_run(m_mainLoop);
}
GRefPtr<GTlsCertificate> m_certificate;
GTlsCertificateFlags m_tlsErrors;
};
static void testSubresourceLoadFailedWithTLSErrors(TLSSubresourceTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_FAIL);
assertIfSSLRequestProcessed = true;
test->loadURI(kHttpServer->getURIForPath("/").data());
test->waitUntilSubresourceLoadFail();
g_assert_true(G_IS_TLS_CERTIFICATE(test->m_certificate.get()));
g_assert_cmpuint(test->m_tlsErrors, ==, G_TLS_CERTIFICATE_UNKNOWN_CA);
assertIfSSLRequestProcessed = false;
}
#if SOUP_CHECK_VERSION(2, 50, 0)
class WebSocketTest : public WebViewTest {
public:
MAKE_GLIB_TEST_FIXTURE(WebSocketTest);
enum EventFlags {
None = 0,
DidServerCompleteHandshake = 1 << 0,
DidOpen = 1 << 1,
DidClose = 1 << 2
};
WebSocketTest()
{
webkit_user_content_manager_register_script_message_handler(m_userContentManager.get(), "event");
g_signal_connect(m_userContentManager.get(), "script-message-received::event", G_CALLBACK(webSocketTestResultCallback), this);
}
virtual ~WebSocketTest()
{
webkit_user_content_manager_unregister_script_message_handler(m_userContentManager.get(), "event");
g_signal_handlers_disconnect_by_data(m_userContentManager.get(), this);
}
static constexpr const char* webSocketTestJSFormat =
"var socket = new WebSocket('%s');"
"socket.addEventListener('open', onOpen);"
"socket.addEventListener('close', onClose);"
"function onOpen() {"
" window.webkit.messageHandlers.event.postMessage('open');"
" socket.removeEventListener('close', onClose);"
"}"
"function onClose() {"
" window.webkit.messageHandlers.event.postMessage('close');"
" socket.removeEventListener('open', onOpen);"
"}";
static void serverWebSocketCallback(SoupServer*, SoupWebsocketConnection*, const char*, SoupClientContext*, gpointer userData)
{
static_cast<WebSocketTest*>(userData)->m_events |= WebSocketTest::EventFlags::DidServerCompleteHandshake;
}
static void webSocketTestResultCallback(WebKitUserContentManager*, WebKitJavascriptResult* javascriptResult, WebSocketTest* test)
{
GUniquePtr<char> event(WebViewTest::javascriptResultToCString(javascriptResult));
if (!g_strcmp0(event.get(), "open"))
test->m_events |= WebSocketTest::EventFlags::DidOpen;
else if (!g_strcmp0(event.get(), "close"))
test->m_events |= WebSocketTest::EventFlags::DidClose;
else
g_assert_not_reached();
test->quitMainLoop();
}
unsigned connectToServerAndWaitForEvents(WebKitTestServer* server)
{
m_events = 0;
server->addWebSocketHandler(serverWebSocketCallback, this);
GUniquePtr<char> createWebSocketJS(g_strdup_printf(webSocketTestJSFormat, server->getWebSocketURIForPath("/foo").data()));
webkit_web_view_run_javascript(m_webView, createWebSocketJS.get(), nullptr, nullptr, nullptr);
g_main_loop_run(m_mainLoop);
server->removeWebSocketHandler();
return m_events;
}
unsigned m_events { 0 };
};
static void testWebSocketTLSErrors(WebSocketTest* test, gconstpointer)
{
WebKitWebContext* context = webkit_web_view_get_context(test->m_webView);
WebKitTLSErrorsPolicy originalPolicy = webkit_web_context_get_tls_errors_policy(context);
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_FAIL);
// First, check that insecure ws:// web sockets work fine.
unsigned events = test->connectToServerAndWaitForEvents(kHttpServer);
g_assert_true(events);
g_assert_true(events & WebSocketTest::EventFlags::DidServerCompleteHandshake);
g_assert_true(events & WebSocketTest::EventFlags::DidOpen);
g_assert_false(events & WebSocketTest::EventFlags::DidClose);
// Try again using wss:// this time. It should be blocked because the
// server certificate is self-signed.
events = test->connectToServerAndWaitForEvents(kHttpsServer);
g_assert_true(events);
g_assert_false(events & WebSocketTest::EventFlags::DidServerCompleteHandshake);
g_assert_false(events & WebSocketTest::EventFlags::DidOpen);
g_assert_true(events & WebSocketTest::EventFlags::DidClose);
// Now try wss:// again, this time ignoring TLS errors.
webkit_web_context_set_tls_errors_policy(context, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
events = test->connectToServerAndWaitForEvents(kHttpsServer);
g_assert_true(events & WebSocketTest::EventFlags::DidServerCompleteHandshake);
g_assert_true(events & WebSocketTest::EventFlags::DidOpen);
g_assert_false(events & WebSocketTest::EventFlags::DidClose);
webkit_web_context_set_tls_errors_policy(context, originalPolicy);
}
#endif
static void httpsServerCallback(SoupServer* server, SoupMessage* message, const char* path, GHashTable*, SoupClientContext*, gpointer)
{
if (message->method != SOUP_METHOD_GET) {
soup_message_set_status(message, SOUP_STATUS_NOT_IMPLEMENTED);
return;
}
g_assert_false(assertIfSSLRequestProcessed);
if (g_str_equal(path, "/")) {
soup_message_set_status(message, SOUP_STATUS_OK);
soup_message_body_append(message->response_body, SOUP_MEMORY_STATIC, indexHTML, strlen(indexHTML));
soup_message_body_complete(message->response_body);
} else if (g_str_equal(path, "/insecure-content/")) {
GUniquePtr<char> responseHTML(g_strdup_printf(insecureContentHTML, kHttpServer->getURIForPath("/test-script").data(), kHttpServer->getURIForPath("/test-image").data()));
soup_message_body_append(message->response_body, SOUP_MEMORY_COPY, responseHTML.get(), strlen(responseHTML.get()));
soup_message_set_status(message, SOUP_STATUS_OK);
soup_message_body_complete(message->response_body);
} else if (g_str_equal(path, "/test-tls/")) {
soup_message_set_status(message, SOUP_STATUS_OK);
soup_message_body_append(message->response_body, SOUP_MEMORY_STATIC, TLSSuccessHTMLString, strlen(TLSSuccessHTMLString));
soup_message_body_complete(message->response_body);
} else if (g_str_equal(path, "/redirect")) {
soup_message_set_status(message, SOUP_STATUS_MOVED_PERMANENTLY);
soup_message_headers_append(message->response_headers, "Location", kHttpServer->getURIForPath("/test-image").data());
} else if (g_str_equal(path, "/auth")) {
soup_message_set_status(message, SOUP_STATUS_UNAUTHORIZED);
soup_message_headers_append(message->response_headers, "WWW-Authenticate", "Basic realm=\"HTTPS auth\"");
} else if (g_str_equal(path, "/style.css")) {
soup_message_set_status(message, SOUP_STATUS_OK);
static const char* styleCSS = "body { color: black; }";
soup_message_body_append(message->response_body, SOUP_MEMORY_STATIC, styleCSS, strlen(styleCSS));
} else
soup_message_set_status(message, SOUP_STATUS_NOT_FOUND);
}
static void httpServerCallback(SoupServer* server, SoupMessage* message, const char* path, GHashTable*, SoupClientContext*, gpointer)
{
if (message->method != SOUP_METHOD_GET) {
soup_message_set_status(message, SOUP_STATUS_NOT_IMPLEMENTED);
return;
}
if (g_str_equal(path, "/test-script")) {
GUniquePtr<char> pathToFile(g_build_filename(Test::getResourcesDir().data(), "link-title.js", nullptr));
char* contents;
gsize length;
g_file_get_contents(pathToFile.get(), &contents, &length, 0);
soup_message_body_append(message->response_body, SOUP_MEMORY_TAKE, contents, length);
soup_message_set_status(message, SOUP_STATUS_OK);
soup_message_body_complete(message->response_body);
} else if (g_str_equal(path, "/test-image")) {
GUniquePtr<char> pathToFile(g_build_filename(Test::getResourcesDir().data(), "blank.ico", nullptr));
char* contents;
gsize length;
g_file_get_contents(pathToFile.get(), &contents, &length, 0);
soup_message_body_append(message->response_body, SOUP_MEMORY_TAKE, contents, length);
soup_message_set_status(message, SOUP_STATUS_OK);
soup_message_body_complete(message->response_body);
} else if (g_str_equal(path, "/")) {
soup_message_set_status(message, SOUP_STATUS_OK);
char* responseHTML = g_strdup_printf("<html><head><link rel='stylesheet' href='%s' type='text/css'></head><body>SSL subresource test</body></html>",
kHttpsServer->getURIForPath("/style.css").data());
soup_message_body_append(message->response_body, SOUP_MEMORY_TAKE, responseHTML, strlen(responseHTML));
soup_message_body_complete(message->response_body);
} else
soup_message_set_status(message, SOUP_STATUS_NOT_FOUND);
}
void beforeAll()
{
kHttpsServer = new WebKitTestServer(WebKitTestServer::ServerHTTPS);
kHttpsServer->run(httpsServerCallback);
kHttpServer = new WebKitTestServer(WebKitTestServer::ServerHTTP);
kHttpServer->run(httpServerCallback);
SSLTest::add("WebKitWebView", "ssl", testSSL);
InsecureContentTest::add("WebKitWebView", "insecure-content", testInsecureContent);
SSLTest::add("WebKitWebView", "tls-errors-policy", testTLSErrorsPolicy);
SSLTest::add("WebKitWebView", "tls-errors-redirect-to-http", testTLSErrorsRedirect);
SSLTest::add("WebKitWebView", "tls-http-auth", testTLSErrorsHTTPAuth);
TLSSubresourceTest::add("WebKitWebView", "tls-subresource", testSubresourceLoadFailedWithTLSErrors);
TLSErrorsTest::add("WebKitWebView", "load-failed-with-tls-errors", testLoadFailedWithTLSErrors);
#if SOUP_CHECK_VERSION(2, 50, 0)
WebSocketTest::add("WebKitWebView", "web-socket-tls-errors", testWebSocketTLSErrors);
#endif
}
void afterAll()
{
delete kHttpsServer;
delete kHttpServer;
}