blob: 547b357ee9ea0aa19daedc8924cab1baac4262db [file] [log] [blame]
/*
* Copyright (C) 2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "HTTPServer.h"
#import "PlatformUtilities.h"
#import "TestNavigationDelegate.h"
#import "TestUIDelegate.h"
#import "Utilities.h"
#import "WKWebViewConfigurationExtras.h"
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <pal/spi/cf/CFNetworkSPI.h>
#import <wtf/RetainPtr.h>
#if HAVE(PRECONNECT_PING)
@interface SessionDelegate : NSObject <NSURLSessionDataDelegate>
@end
@implementation SessionDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}
@end
#endif
namespace TestWebKitAPI {
TEST(Preconnect, HTTP)
{
size_t connectionCount = 0;
bool connected = false;
bool requested = false;
HTTPServer server([&] (Connection connection) {
++connectionCount;
connected = true;
connection.receiveHTTPRequest([&](Vector<char>&&) {
requested = true;
});
});
auto webView = adoptNS([WKWebView new]);
[webView _preconnectToServer:server.request().URL];
Util::run(&connected);
Util::spinRunLoop(10);
EXPECT_FALSE(requested);
[webView loadRequest:server.request()];
Util::run(&requested);
EXPECT_EQ(connectionCount, 1u);
}
TEST(Preconnect, ConnectionCount)
{
size_t connectionCount = 0;
bool anyConnections = false;
bool requested = false;
HTTPServer server([&] (Connection connection) {
++connectionCount;
anyConnections = true;
connection.receiveHTTPRequest([&](Vector<char>&&) {
requested = true;
});
});
auto webView = adoptNS([WKWebView new]);
// The preconnect to the server will use the default setting of "use the credential store",
// and therefore use the credential-store-blessed NSURLSession.
[webView _preconnectToServer:server.request().URL];
Util::run(&anyConnections);
Util::spinRunLoop(10);
EXPECT_FALSE(requested);
// Then this request will *not* use the credential store, therefore using a different NSURLSession
// that doesn't know about the above preconnect, triggering a second connection to the server.
webView.get()._canUseCredentialStorage = NO;
[webView loadRequest:server.request()];
Util::run(&requested);
EXPECT_EQ(connectionCount, 2u);
}
TEST(Preconnect, HTTPS)
{
bool connected = false;
bool requested = false;
__block bool receivedChallenge = false;
HTTPServer server([&] (Connection connection) {
connected = true;
connection.receiveHTTPRequest([&](Vector<char>&&) {
requested = true;
});
}, HTTPServer::Protocol::Https);
auto webView = adoptNS([WKWebView new]);
auto delegate = adoptNS([TestNavigationDelegate new]);
[webView setNavigationDelegate:delegate.get()];
[delegate setDidReceiveAuthenticationChallenge:^(WKWebView *, NSURLAuthenticationChallenge *challenge, void (^callback)(NSURLSessionAuthChallengeDisposition, NSURLCredential *)) {
receivedChallenge = true;
EXPECT_WK_STREQ(challenge.protectionSpace.authenticationMethod, NSURLAuthenticationMethodServerTrust);
callback(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}];
[webView _preconnectToServer:server.request().URL];
Util::run(&connected);
Util::spinRunLoop(10);
EXPECT_FALSE(receivedChallenge);
EXPECT_FALSE(requested);
[webView loadRequest:server.request()];
Util::run(&requested);
EXPECT_TRUE(receivedChallenge);
}
#if HAVE(PRECONNECT_PING)
static void pingPong(Ref<H2::Connection>&& connection, size_t* headersCount)
{
connection->receive([connection, headersCount] (H2::Frame&& frame) mutable {
switch (frame.type()) {
case H2::Frame::Type::Headers:
++*headersCount;
break;
case H2::Frame::Type::Settings:
case H2::Frame::Type::WindowUpdate:
// These frame types are ok for a preconnect task.
break;
case H2::Frame::Type::Ping:
{
// https://http2.github.io/http2-spec/#rfc.section.6.7
constexpr uint8_t ack = 0x1;
connection->send(H2::Frame(H2::Frame::Type::Ping, ack, frame.streamID(), frame.payload()));
}
break;
default:
// If anything else is sent by the client, preconnect is doing too much.
ASSERT_NOT_REACHED();
break;
}
pingPong(WTFMove(connection), headersCount);
});
}
TEST(Preconnect, H2Ping)
{
size_t headersCount = 0;
HTTPServer server([headersCount = &headersCount] (Connection tlsConnection) {
pingPong(H2::Connection::create(tlsConnection), headersCount);
}, HTTPServer::Protocol::Http2);
auto delegate = adoptNS([SessionDelegate new]);
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:delegate.get() delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:server.request()];
task._preconnect = YES;
__block bool done = false;
[task getUnderlyingHTTPConnectionInfoWithCompletionHandler:^(_NSHTTPConnectionInfo *connectionInfo) {
EXPECT_TRUE(connectionInfo.isValid);
[connectionInfo sendPingWithReceiveHandler:^(NSError *error, NSTimeInterval interval) {
EXPECT_FALSE(error);
EXPECT_GT(interval, 0.0);
done = true;
}];
}];
[task resume];
Util::run(&done);
// Make sure the client doesn't send anything except Settings, WindowUpdate, and Ping.
// If Headers or Data were sent, then the preconnect wouldn't be preconnect.
usleep(100000);
Util::spinRunLoop(100);
EXPECT_EQ(headersCount, 0u);
NSURLSessionDataTask *task2 = [session dataTaskWithRequest:server.request()];
[task2 resume];
while (!headersCount)
Util::spinRunLoop();
EXPECT_EQ(headersCount, 1u);
usleep(100000);
Util::spinRunLoop(100);
EXPECT_EQ(headersCount, 1u);
}
TEST(Preconnect, H2PingFromWebCoreNSURLSession)
{
size_t headersCount = 0;
HTTPServer server([headersCount = &headersCount] (Connection tlsConnection) {
pingPong(H2::Connection::create(tlsConnection), headersCount);
}, HTTPServer::Protocol::Http2);
WKWebViewConfiguration *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
auto delegate = adoptNS([TestNavigationDelegate new]);
__block bool receivedChallenge = false;
[delegate setDidReceiveAuthenticationChallenge:^(WKWebView *, NSURLAuthenticationChallenge *challenge, void (^callback)(NSURLSessionAuthChallengeDisposition, NSURLCredential *)) {
EXPECT_WK_STREQ(challenge.protectionSpace.authenticationMethod, NSURLAuthenticationMethodServerTrust);
receivedChallenge = true;
callback(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}];
[webView setNavigationDelegate:delegate.get()];
[webView loadHTMLString:[NSString stringWithFormat:@"<script>internals.sendH2Ping('https://127.0.0.1:%d/').then(function(t){if(t>0){alert('pass')}else{alert('fail')}})</script>", server.port()] baseURL:nil];
EXPECT_WK_STREQ([webView _test_waitForAlert], "pass");
EXPECT_FALSE(headersCount);
EXPECT_TRUE(receivedChallenge);
}
#endif // HAVE(PRECONNECT_PING)
static void verifyPreconnectDisabled(void(*disabler)(WKWebViewConfiguration *))
{
size_t connectionCount { 0 };
HTTPServer server([&](Connection) {
connectionCount++;
});
NSString *html = [NSString stringWithFormat:@"<link rel='preconnect' href='http://127.0.0.1:%d'>", server.port()];
{
auto configuration = adoptNS([WKWebViewConfiguration new]);
disabler(configuration.get());
auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]);
[webView loadHTMLString:html baseURL:nil];
[webView _test_waitForDidFinishNavigation];
Util::spinRunLoop(10);
usleep(10000);
Util::spinRunLoop(10);
EXPECT_EQ(connectionCount, 0u);
}
{
auto webView = adoptNS([WKWebView new]);
[webView loadHTMLString:html baseURL:nil];
[webView _test_waitForDidFinishNavigation];
while (connectionCount != 1)
Util::spinRunLoop();
}
}
TEST(Preconnect, DisablePreconnect)
{
verifyPreconnectDisabled([] (WKWebViewConfiguration *configuration) {
configuration._allowedNetworkHosts = [NSSet set];
});
verifyPreconnectDisabled([] (WKWebViewConfiguration *configuration) {
configuration._loadsSubresources = NO;
});
}
}