Add SPI to restrict loading to main resources or non-network loads
https://bugs.webkit.org/show_bug.cgi?id=209893

Patch by Alex Christensen <achristensen@webkit.org> on 2020-04-02
Reviewed by Tim Horton.

Source/WebCore:

This will allow two projects that currently use the injected bundle SPI to use these instead.
Covered by API tests.

* Modules/websockets/ThreadableWebSocketChannel.cpp:
(WebCore::ThreadableWebSocketChannel::validateURL):
* loader/ResourceLoadNotifier.cpp:
(WebCore::ResourceLoadNotifier::assignIdentifierToInitialRequest):
(WebCore::ResourceLoadNotifier::dispatchWillSendRequest):
* loader/ResourceLoadNotifier.h:
* page/Page.cpp:
(WebCore::m_loadsFromNetwork):
(WebCore::m_deviceOrientationUpdateProvider): Deleted.
* page/Page.h:
(WebCore::Page::loadsSubresources const):
(WebCore::Page::loadsFromNetwork const):
* page/PageConfiguration.h:

Source/WebKit:

* Shared/WebPageCreationParameters.cpp:
(WebKit::WebPageCreationParameters::encode const):
(WebKit::WebPageCreationParameters::decode):
* Shared/WebPageCreationParameters.h:
* UIProcess/API/APIPageConfiguration.cpp:
(API::PageConfiguration::copy const):
* UIProcess/API/APIPageConfiguration.h:
(API::PageConfiguration::loadsSubresources const):
(API::PageConfiguration::setLoadsSubresources):
(API::PageConfiguration::loadsFromNetwork const):
(API::PageConfiguration::setLoadsFromNetwork):
* UIProcess/API/Cocoa/WKWebViewConfiguration.mm:
(-[WKWebViewConfiguration _setLoadsFromNetwork:]):
(-[WKWebViewConfiguration _loadsFromNetwork]):
(-[WKWebViewConfiguration _setLoadsSubresources:]):
(-[WKWebViewConfiguration _loadsSubresources]):
* UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h:
* UIProcess/WebPageProxy.cpp:
* WebProcess/WebPage/WebPage.cpp:
(WebKit::m_processDisplayName):

Tools:

* TestWebKitAPI/Tests/WebKitCocoa/WKURLSchemeHandler-1.mm:
* TestWebKitAPI/cocoa/HTTPServer.h:
(TestWebKitAPI::HTTPServer::totalRequests const):
* TestWebKitAPI/cocoa/HTTPServer.mm:
(TestWebKitAPI::HTTPServer::respondToRequests):

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@259392 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 2e860dc..e24cff7 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,27 @@
+2020-04-02  Alex Christensen  <achristensen@webkit.org>
+
+        Add SPI to restrict loading to main resources or non-network loads
+        https://bugs.webkit.org/show_bug.cgi?id=209893
+
+        Reviewed by Tim Horton.
+
+        This will allow two projects that currently use the injected bundle SPI to use these instead.
+        Covered by API tests.
+
+        * Modules/websockets/ThreadableWebSocketChannel.cpp:
+        (WebCore::ThreadableWebSocketChannel::validateURL):
+        * loader/ResourceLoadNotifier.cpp:
+        (WebCore::ResourceLoadNotifier::assignIdentifierToInitialRequest):
+        (WebCore::ResourceLoadNotifier::dispatchWillSendRequest):
+        * loader/ResourceLoadNotifier.h:
+        * page/Page.cpp:
+        (WebCore::m_loadsFromNetwork):
+        (WebCore::m_deviceOrientationUpdateProvider): Deleted.
+        * page/Page.h:
+        (WebCore::Page::loadsSubresources const):
+        (WebCore::Page::loadsFromNetwork const):
+        * page/PageConfiguration.h:
+
 2020-04-02  Eric Carlson  <eric.carlson@apple.com>
 
         [iOS] Allow WebKit to use camera in multi-tasking mode
diff --git a/Source/WebCore/Modules/websockets/ThreadableWebSocketChannel.cpp b/Source/WebCore/Modules/websockets/ThreadableWebSocketChannel.cpp
index fb6d885..62bfe17 100644
--- a/Source/WebCore/Modules/websockets/ThreadableWebSocketChannel.cpp
+++ b/Source/WebCore/Modules/websockets/ThreadableWebSocketChannel.cpp
@@ -82,8 +82,10 @@
 Optional<ThreadableWebSocketChannel::ValidatedURL> ThreadableWebSocketChannel::validateURL(Document& document, const URL& requestedURL)
 {
     ValidatedURL validatedURL { requestedURL, true };
-#if ENABLE(CONTENT_EXTENSIONS)
     if (auto* page = document.page()) {
+        if (!page->loadsFromNetwork())
+            return { };
+#if ENABLE(CONTENT_EXTENSIONS)
         if (auto* documentLoader = document.loader()) {
             auto results = page->userContentProvider().processContentRuleListsForLoad(validatedURL.url, ContentExtensions::ResourceType::Raw, *documentLoader);
             if (results.summary.blockedLoad)
@@ -94,10 +96,10 @@
             }
             validatedURL.areCookiesAllowed = !results.summary.blockedCookies;
         }
-    }
 #else
-    UNUSED_PARAM(document);
+        UNUSED_PARAM(document);
 #endif
+    }
     return validatedURL;
 }
 
diff --git a/Source/WebCore/loader/ResourceLoadNotifier.cpp b/Source/WebCore/loader/ResourceLoadNotifier.cpp
index 812e2744a..0b6f788 100644
--- a/Source/WebCore/loader/ResourceLoadNotifier.cpp
+++ b/Source/WebCore/loader/ResourceLoadNotifier.cpp
@@ -109,6 +109,13 @@
 
 void ResourceLoadNotifier::assignIdentifierToInitialRequest(unsigned long identifier, DocumentLoader* loader, const ResourceRequest& request)
 {
+    bool pageIsProvisionallyLoading = false;
+    if (auto* frameLoader = loader ? loader->frameLoader() : nullptr)
+        pageIsProvisionallyLoading = frameLoader->provisionalDocumentLoader() == loader;
+
+    if (pageIsProvisionallyLoading)
+        m_initialRequestIdentifier = identifier;
+
     m_frame.loader().client().assignIdentifierToInitialRequest(identifier, loader, request);
 }
 
@@ -126,6 +133,14 @@
     if (m_frame.loader().documentLoader())
         m_frame.loader().documentLoader()->didTellClientAboutLoad(request.url());
 
+    if (auto* page = m_frame.page()) {
+        if (!page->loadsSubresources()) {
+            if (!m_frame.isMainFrame() || (m_initialRequestIdentifier && *m_initialRequestIdentifier != identifier))
+                request = { };
+        } else if (!page->loadsFromNetwork() && request.url().protocolIsInHTTPFamily())
+            request = { };
+    }
+    
     // Notifying the FrameLoaderClient may cause the frame to be destroyed.
     Ref<Frame> protect(m_frame);
     m_frame.loader().client().dispatchWillSendRequest(loader, identifier, request, redirectResponse);
diff --git a/Source/WebCore/loader/ResourceLoadNotifier.h b/Source/WebCore/loader/ResourceLoadNotifier.h
index e0e6a48..6b17d25 100644
--- a/Source/WebCore/loader/ResourceLoadNotifier.h
+++ b/Source/WebCore/loader/ResourceLoadNotifier.h
@@ -67,6 +67,7 @@
 
 private:
     Frame& m_frame;
+    Optional<unsigned long> m_initialRequestIdentifier;
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/page/Page.cpp b/Source/WebCore/page/Page.cpp
index 9f0a406..90e75d5 100644
--- a/Source/WebCore/page/Page.cpp
+++ b/Source/WebCore/page/Page.cpp
@@ -290,6 +290,8 @@
 #if ENABLE(DEVICE_ORIENTATION) && PLATFORM(IOS_FAMILY)
     , m_deviceOrientationUpdateProvider(WTFMove(pageConfiguration.deviceOrientationUpdateProvider))
 #endif
+    , m_loadsSubresources(pageConfiguration.loadsSubresources)
+    , m_loadsFromNetwork(pageConfiguration.loadsFromNetwork)
 {
     updateTimerThrottlingState();
 
diff --git a/Source/WebCore/page/Page.h b/Source/WebCore/page/Page.h
index e2484f6..b34455d 100644
--- a/Source/WebCore/page/Page.h
+++ b/Source/WebCore/page/Page.h
@@ -702,6 +702,9 @@
     bool isOnlyNonUtilityPage() const;
     bool isUtilityPage() const { return m_isUtilityPage; }
 
+    bool loadsSubresources() const { return m_loadsSubresources; }
+    bool loadsFromNetwork() const { return m_loadsFromNetwork; }
+
     bool isLowPowerModeEnabled() const;
     WEBCORE_EXPORT void setLowPowerModeEnabledOverrideForTesting(Optional<bool>);
 
@@ -1010,6 +1013,8 @@
     Vector<UserContentURLPattern> m_corsDisablingPatterns;
     Vector<UserStyleSheet> m_userStyleSheetsPendingInjection;
     bool m_shouldFireResizeEvents { true };
+    bool m_loadsSubresources { true };
+    bool m_loadsFromNetwork { true };
 };
 
 inline PageGroup& Page::group()
diff --git a/Source/WebCore/page/PageConfiguration.h b/Source/WebCore/page/PageConfiguration.h
index 369f063..ee7457c 100644
--- a/Source/WebCore/page/PageConfiguration.h
+++ b/Source/WebCore/page/PageConfiguration.h
@@ -127,6 +127,8 @@
 #endif
     Vector<String> corsDisablingPatterns;
     UniqueRef<MediaRecorderProvider> mediaRecorderProvider;
+    bool loadsSubresources { true };
+    bool loadsFromNetwork { true };
 };
 
 }
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index 012895a..80d21b6 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,31 @@
+2020-04-02  Alex Christensen  <achristensen@webkit.org>
+
+        Add SPI to restrict loading to main resources or non-network loads
+        https://bugs.webkit.org/show_bug.cgi?id=209893
+
+        Reviewed by Tim Horton.
+
+        * Shared/WebPageCreationParameters.cpp:
+        (WebKit::WebPageCreationParameters::encode const):
+        (WebKit::WebPageCreationParameters::decode):
+        * Shared/WebPageCreationParameters.h:
+        * UIProcess/API/APIPageConfiguration.cpp:
+        (API::PageConfiguration::copy const):
+        * UIProcess/API/APIPageConfiguration.h:
+        (API::PageConfiguration::loadsSubresources const):
+        (API::PageConfiguration::setLoadsSubresources):
+        (API::PageConfiguration::loadsFromNetwork const):
+        (API::PageConfiguration::setLoadsFromNetwork):
+        * UIProcess/API/Cocoa/WKWebViewConfiguration.mm:
+        (-[WKWebViewConfiguration _setLoadsFromNetwork:]):
+        (-[WKWebViewConfiguration _loadsFromNetwork]):
+        (-[WKWebViewConfiguration _setLoadsSubresources:]):
+        (-[WKWebViewConfiguration _loadsSubresources]):
+        * UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h:
+        * UIProcess/WebPageProxy.cpp:
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::m_processDisplayName):
+
 2020-04-02  youenn fablet  <youenn@apple.com>
 
         Debug crash: ASSERTION FAILED: m_ongoingFetches.contains(task.fetchIdentifier())
diff --git a/Source/WebKit/Shared/WebPageCreationParameters.cpp b/Source/WebKit/Shared/WebPageCreationParameters.cpp
index 8937b26..87725f1 100644
--- a/Source/WebKit/Shared/WebPageCreationParameters.cpp
+++ b/Source/WebKit/Shared/WebPageCreationParameters.cpp
@@ -133,6 +133,8 @@
     encoder << oldPageID;
     encoder << overriddenMediaType;
     encoder << corsDisablingPatterns;
+    encoder << loadsSubresources;
+    encoder << loadsFromNetwork;
     encoder << crossOriginAccessControlCheckEnabled;
     encoder << processDisplayName;
 
@@ -416,6 +418,18 @@
         return WTF::nullopt;
     parameters.corsDisablingPatterns = WTFMove(*corsDisablingPatterns);
 
+    Optional<bool> loadsSubresources;
+    decoder >> loadsSubresources;
+    if (!loadsSubresources)
+        return WTF::nullopt;
+    parameters.loadsSubresources = *loadsSubresources;
+
+    Optional<bool> loadsFromNetwork;
+    decoder >> loadsFromNetwork;
+    if (!loadsFromNetwork)
+        return WTF::nullopt;
+    parameters.loadsFromNetwork = *loadsFromNetwork;
+
     Optional<bool> crossOriginAccessControlCheckEnabled;
     decoder >> crossOriginAccessControlCheckEnabled;
     if (!crossOriginAccessControlCheckEnabled)
diff --git a/Source/WebKit/Shared/WebPageCreationParameters.h b/Source/WebKit/Shared/WebPageCreationParameters.h
index b47560e..736cba3 100644
--- a/Source/WebKit/Shared/WebPageCreationParameters.h
+++ b/Source/WebKit/Shared/WebPageCreationParameters.h
@@ -202,6 +202,9 @@
 
     String overriddenMediaType;
     Vector<String> corsDisablingPatterns;
+    bool loadsSubresources { true };
+    bool loadsFromNetwork { true };
+
     bool crossOriginAccessControlCheckEnabled { true };
     String processDisplayName;
 
diff --git a/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp b/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp
index d091765..c8d10b7 100644
--- a/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp
+++ b/Source/WebKit/UIProcess/API/APIPageConfiguration.cpp
@@ -95,6 +95,8 @@
 
     copy->m_processDisplayName = this->m_processDisplayName;
     copy->m_ignoresAppBoundDomains = this->m_ignoresAppBoundDomains;
+    copy->m_loadsSubresources = this->m_loadsSubresources;
+    copy->m_loadsFromNetwork = this->m_loadsFromNetwork;
 
     return copy;
 }
diff --git a/Source/WebKit/UIProcess/API/APIPageConfiguration.h b/Source/WebKit/UIProcess/API/APIPageConfiguration.h
index eec7f19..ff58c91 100644
--- a/Source/WebKit/UIProcess/API/APIPageConfiguration.h
+++ b/Source/WebKit/UIProcess/API/APIPageConfiguration.h
@@ -149,7 +149,13 @@
 
     bool ignoresAppBoundDomains() const { return m_ignoresAppBoundDomains; }
     void setIgnoresAppBoundDomains(bool shouldIgnore) { m_ignoresAppBoundDomains = shouldIgnore; }
-    
+
+    bool loadsSubresources() const { return m_loadsSubresources; }
+    void setLoadsSubresources(bool loads) { m_loadsSubresources = loads; }
+
+    bool loadsFromNetwork() const { return m_loadsFromNetwork; }
+    void setLoadsFromNetwork(bool loads) { m_loadsFromNetwork = loads; }
+
 private:
 
     RefPtr<WebKit::WebProcessPool> m_processPool;
@@ -191,6 +197,8 @@
     WTF::String m_processDisplayName;
     WebKit::WebViewCategory m_webViewCategory { WebKit::WebViewCategory::AppBoundDomain };
     bool m_ignoresAppBoundDomains { false };
+    bool m_loadsSubresources { true };
+    bool m_loadsFromNetwork { true };
 };
 
 } // namespace API
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfiguration.mm b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfiguration.mm
index 8539a2a..d44ed2b 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfiguration.mm
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfiguration.mm
@@ -913,6 +913,26 @@
     _pageConfiguration->setCORSDisablingPatterns(WTFMove(vector));
 }
 
+- (void)_setLoadsFromNetwork:(BOOL)loads
+{
+    _pageConfiguration->setLoadsFromNetwork(loads);
+}
+
+- (BOOL)_loadsFromNetwork
+{
+    return _pageConfiguration->loadsFromNetwork();
+}
+
+- (void)_setLoadsSubresources:(BOOL)loads
+{
+    _pageConfiguration->setLoadsSubresources(loads);
+}
+
+- (BOOL)_loadsSubresources
+{
+    return _pageConfiguration->loadsSubresources();
+}
+
 - (void)_setCrossOriginAccessControlCheckEnabled:(BOOL)enabled
 {
     _pageConfiguration->setCrossOriginAccessControlCheckEnabled(enabled);
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h
index 68b6eb6..12a7c2d 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfigurationPrivate.h
@@ -86,6 +86,9 @@
 @property (nonatomic, copy, setter=_setCORSDisablingPatterns:) NSArray<NSString *> *_corsDisablingPatterns WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 @property (nonatomic, setter=_setCrossOriginAccessControlCheckEnabled:) BOOL _crossOriginAccessControlCheckEnabled WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 
+@property (nonatomic, setter=_setLoadsFromNetwork:) BOOL _loadsFromNetwork WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+@property (nonatomic, setter=_setLoadsSubresources:) BOOL _loadsSubresources WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+
 #if TARGET_OS_IPHONE
 @property (nonatomic, setter=_setClientNavigationsRunAtForegroundPriority:) BOOL _clientNavigationsRunAtForegroundPriority WK_API_AVAILABLE(ios(WK_IOS_TBA));
 @property (nonatomic, setter=_setAlwaysRunsAtForegroundPriority:) BOOL _alwaysRunsAtForegroundPriority WK_API_AVAILABLE(ios(9_0));
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 0208a87..efa2ba4 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -7781,6 +7781,8 @@
 
     parameters.overriddenMediaType = m_overriddenMediaType;
     parameters.corsDisablingPatterns = m_configuration->corsDisablingPatterns();
+    parameters.loadsFromNetwork = m_configuration->loadsFromNetwork();
+    parameters.loadsSubresources = m_configuration->loadsSubresources();
     parameters.crossOriginAccessControlCheckEnabled = m_configuration->crossOriginAccessControlCheckEnabled();
     parameters.hasResourceLoadClient = !!m_resourceLoadClient;
 
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
index 224384a..b7889d8 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
@@ -529,6 +529,9 @@
 #endif
 
     pageConfiguration.corsDisablingPatterns = WTFMove(parameters.corsDisablingPatterns);
+    pageConfiguration.loadsSubresources = parameters.loadsSubresources;
+    pageConfiguration.loadsFromNetwork = parameters.loadsFromNetwork;
+
     if (!parameters.crossOriginAccessControlCheckEnabled)
         CrossOriginAccessControlCheckDisabler::singleton().setCrossOriginAccessControlCheckEnabled(false);
 
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index a2ac238..a5386d6 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,16 @@
+2020-04-02  Alex Christensen  <achristensen@webkit.org>
+
+        Add SPI to restrict loading to main resources or non-network loads
+        https://bugs.webkit.org/show_bug.cgi?id=209893
+
+        Reviewed by Tim Horton.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/WKURLSchemeHandler-1.mm:
+        * TestWebKitAPI/cocoa/HTTPServer.h:
+        (TestWebKitAPI::HTTPServer::totalRequests const):
+        * TestWebKitAPI/cocoa/HTTPServer.mm:
+        (TestWebKitAPI::HTTPServer::respondToRequests):
+
 2020-04-02  Kate Cheney  <katherine_cheney@apple.com>
 
         Add additional WKAppBoundDomains to TestWebKitAPI's expectations after initializing eTLD+1 by default
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKURLSchemeHandler-1.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKURLSchemeHandler-1.mm
index da9217e..e4e807a 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKURLSchemeHandler-1.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/WKURLSchemeHandler-1.mm
@@ -1004,6 +1004,118 @@
     EXPECT_FALSE(loadFail);
 }
 
+TEST(URLSchemeHandler, LoadsFromNetwork)
+{
+    TestWebKitAPI::HTTPServer server({
+        { "/", { {{ "Access-Control-Allow-Origin", "*" }}, "test content" } }
+    });
+
+    bool loadSuccess = false;
+    bool loadFail = false;
+    bool done = false;
+
+    auto handler = adoptNS([TestURLSchemeHandler new]);
+
+    WKWebViewConfiguration *configuration = [[[WKWebViewConfiguration alloc] init] autorelease];
+    [configuration setURLSchemeHandler:handler.get() forURLScheme:@"test"];
+
+    [handler setStartURLSchemeTaskHandler:[&](WKWebView *, id<WKURLSchemeTask> task) {
+        if ([task.request.URL.path isEqualToString:@"/main.html"]) {
+            NSData *data = [[NSString stringWithFormat:@"<script>"
+                "fetch('http://127.0.0.1:%d/').then(()=>{"
+                    "fetch('/loadSuccess')"
+                "}).catch(()=>{"
+                    "var ws = new WebSocket('ws://127.0.0.1:%d');"
+                    "ws.onerror = function() { fetch('/loadFail') };"
+                "})"
+                "</script>", server.port(), server.port()] dataUsingEncoding:NSUTF8StringEncoding];
+            [task didReceiveResponse:[[[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:data.length textEncodingName:nil] autorelease]];
+            [task didReceiveData:data];
+            [task didFinish];
+        } else if ([task.request.URL.path isEqualToString:@"/loadSuccess"]) {
+            loadSuccess = true;
+            done = true;
+        } else if ([task.request.URL.path isEqualToString:@"/loadFail"]) {
+            loadFail = true;
+            done = true;
+        } else
+            ASSERT_NOT_REACHED();
+    }];
+    
+    {
+        auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration]);
+        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"test://host1/main.html"]]];
+        TestWebKitAPI::Util::run(&done);
+    }
+    EXPECT_TRUE(loadSuccess);
+    EXPECT_FALSE(loadFail);
+    EXPECT_EQ(server.totalRequests(), 1u);
+    
+    loadSuccess = false;
+    loadFail = false;
+    done = false;
+
+    configuration._loadsFromNetwork = NO;
+    {
+        auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration]);
+        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"test://host1/main.html"]]];
+        TestWebKitAPI::Util::run(&done);
+    }
+    EXPECT_FALSE(loadSuccess);
+    EXPECT_TRUE(loadFail);
+    EXPECT_EQ(server.totalRequests(), 1u);
+}
+
+TEST(URLSchemeHandler, LoadsSubresources)
+{
+    bool loadedImage = false;
+    bool loadedIFrame = false;
+
+    auto handler = adoptNS([TestURLSchemeHandler new]);
+
+    WKWebViewConfiguration *configuration = [[[WKWebViewConfiguration alloc] init] autorelease];
+    [configuration setURLSchemeHandler:handler.get() forURLScheme:@"test"];
+
+    [handler setStartURLSchemeTaskHandler:[&](WKWebView *, id<WKURLSchemeTask> task) {
+        NSString *response = nil;
+        if ([task.request.URL.path isEqualToString:@"/main.html"])
+            response = @"<img src='/imgsrc'></img><iframe src='/iframesrc'></iframe>";
+        else if ([task.request.URL.path isEqualToString:@"/imgsrc"]) {
+            response = @"image content";
+            loadedImage = true;
+        } else if ([task.request.URL.path isEqualToString:@"/iframesrc"]) {
+            response = @"iframe content";
+            loadedIFrame = true;
+        } else
+            ASSERT_NOT_REACHED();
+        [task didReceiveResponse:[[[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:response.length textEncodingName:nil] autorelease]];
+        [task didReceiveData:[response dataUsingEncoding:NSUTF8StringEncoding]];
+        [task didFinish];
+    }];
+    
+    {
+        auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration]);
+        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"test://host1/main.html"]]];
+        TestWebKitAPI::Util::run(&loadedImage);
+        TestWebKitAPI::Util::run(&loadedIFrame);
+    }
+    
+    loadedImage = false;
+    loadedIFrame = false;
+
+    configuration._loadsSubresources = NO;
+    {
+        auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration]);
+        auto delegate = adoptNS([TestNavigationDelegate new]);
+        webView.get().navigationDelegate = delegate.get();
+        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"test://host1/main.html"]]];
+        [delegate waitForDidFinishNavigation];
+        TestWebKitAPI::Util::spinRunLoop(100);
+        EXPECT_FALSE(loadedIFrame);
+        EXPECT_FALSE(loadedImage);
+    }
+}
+
 #endif // HAVE(NETWORK_FRAMEWORK)
 
 @interface FrameSchemeHandler : NSObject <WKURLSchemeHandler>
diff --git a/Tools/TestWebKitAPI/cocoa/HTTPServer.h b/Tools/TestWebKitAPI/cocoa/HTTPServer.h
index 1a1c074..89f173d 100644
--- a/Tools/TestWebKitAPI/cocoa/HTTPServer.h
+++ b/Tools/TestWebKitAPI/cocoa/HTTPServer.h
@@ -43,6 +43,7 @@
     HTTPServer(std::initializer_list<std::pair<String, HTTPResponse>>, Protocol = Protocol::Http);
     uint16_t port() const;
     NSURLRequest *request() const;
+    size_t totalRequests() const { return m_totalRequests; }
     
 private:
     void respondToRequests(nw_connection_t);
@@ -50,6 +51,7 @@
     RetainPtr<nw_listener_t> m_listener;
     const Protocol m_protocol;
     const HashMap<String, HTTPResponse> m_requestResponseMap;
+    size_t m_totalRequests { 0 };
 };
 
 struct HTTPServer::HTTPResponse {
diff --git a/Tools/TestWebKitAPI/cocoa/HTTPServer.mm b/Tools/TestWebKitAPI/cocoa/HTTPServer.mm
index 0cc950b..c62cfda 100644
--- a/Tools/TestWebKitAPI/cocoa/HTTPServer.mm
+++ b/Tools/TestWebKitAPI/cocoa/HTTPServer.mm
@@ -100,6 +100,8 @@
         });
         request.append('\0');
 
+        m_totalRequests++;
+
         const char* getPathPrefix = "GET ";
         const char* postPathPrefix = "POST ";
         const char* pathSuffix = " HTTP/1.1\r\n";