/* * Copyright (C) 2015-2016 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" #if ENABLE(CONTENT_FILTERING) #import "DeprecatedGlobalValues.h" #import "ContentFiltering.h" #import "PlatformUtilities.h" #import "TestProtocol.h" #import "WKWebViewConfigurationExtras.h" #import #import #import #import #import #import #import #import #import #import #import #import #import SOFT_LINK_FRAMEWORK_OPTIONAL(NetworkExtension); SOFT_LINK_CLASS_OPTIONAL(NetworkExtension, NEFilterSource); SOFT_LINK_PRIVATE_FRAMEWORK(WebContentAnalysis); SOFT_LINK_CLASS(WebContentAnalysis, WebFilterEvaluator); using Decision = WebCore::MockContentFilterSettings::Decision; using DecisionPoint = WebCore::MockContentFilterSettings::DecisionPoint; @interface MockContentFilterEnabler : NSObject - (instancetype)initWithDecision:(Decision)decision decisionPoint:(DecisionPoint)decisionPoint; @end @implementation MockContentFilterEnabler { Decision _decision; DecisionPoint _decisionPoint; } + (BOOL)supportsSecureCoding { return YES; } - (id)copyWithZone:(NSZone *)zone { return [self retain]; } - (instancetype)initWithCoder:(NSCoder *)decoder { return [super init]; } - (instancetype)initWithDecision:(Decision)decision decisionPoint:(DecisionPoint)decisionPoint { if (!(self = [super init])) return nil; _decision = decision; _decisionPoint = decisionPoint; return self; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeInt:static_cast(_decision) forKey:@"Decision"]; [coder encodeInt:static_cast(_decisionPoint) forKey:@"DecisionPoint"]; } @end static RetainPtr configurationWithContentFilterSettings(Decision decision, DecisionPoint decisionPoint) { auto configuration = retainPtr([WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ContentFilteringPlugIn"]); auto contentFilterEnabler = adoptNS([[MockContentFilterEnabler alloc] initWithDecision:decision decisionPoint:decisionPoint]); [[configuration processPool] _setObject:contentFilterEnabler.get() forBundleParameter:NSStringFromClass([MockContentFilterEnabler class])]; return configuration; } @interface ServerRedirectNavigationDelegate : NSObject @end @implementation ServerRedirectNavigationDelegate - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { EXPECT_WK_STREQ(webView.URL.absoluteString, @"https://redirect/?pass"); } - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation { EXPECT_WK_STREQ(webView.URL.absoluteString, @"https://pass/"); } - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation { isDone = true; } @end TEST(ContentFiltering, URLAfterServerRedirect) { @autoreleasepool { [TestProtocol registerWithScheme:@"https"]; auto configuration = configurationWithContentFilterSettings(Decision::Allow, DecisionPoint::AfterAddData); auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]); auto navigationDelegate = adoptNS([[ServerRedirectNavigationDelegate alloc] init]); [webView setNavigationDelegate:navigationDelegate.get()]; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://redirect?pass"]]]; TestWebKitAPI::Util::run(&isDone); [TestProtocol unregister]; } } @interface BecomeDownloadDelegate : NSObject @end @implementation BecomeDownloadDelegate - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { decisionHandler(WKNavigationResponsePolicyDownload); } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { isDone = true; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { isDone = true; } @end static bool downloadDidStart; @interface ContentFilteringDownloadDelegate : NSObject <_WKDownloadDelegate> @end @implementation ContentFilteringDownloadDelegate - (void)_downloadDidStart:(_WKDownload *)download { downloadDidStart = true; } @end static void downloadTest(Decision decision, DecisionPoint decisionPoint) { @autoreleasepool { [TestProtocol registerWithScheme:@"https"]; auto configuration = configurationWithContentFilterSettings(decision, decisionPoint); auto downloadDelegate = adoptNS([[ContentFilteringDownloadDelegate alloc] init]); [[configuration processPool] _setDownloadDelegate:downloadDelegate.get()]; auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]); auto navigationDelegate = adoptNS([[BecomeDownloadDelegate alloc] init]); [webView setNavigationDelegate:navigationDelegate.get()]; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://redirect/?download"]]]; isDone = false; downloadDidStart = false; const bool downloadShouldStart = decision == Decision::Allow || decisionPoint > DecisionPoint::AfterResponse; if (downloadShouldStart) TestWebKitAPI::Util::run(&downloadDidStart); else TestWebKitAPI::Util::run(&isDone); EXPECT_EQ(downloadShouldStart, downloadDidStart); [TestProtocol unregister]; } } TEST(ContentFiltering, AllowDownloadAfterWillSendRequest) { downloadTest(Decision::Allow, DecisionPoint::AfterWillSendRequest); } TEST(ContentFiltering, BlockDownloadAfterWillSendRequest) { downloadTest(Decision::Block, DecisionPoint::AfterWillSendRequest); } TEST(ContentFiltering, AllowDownloadAfterRedirect) { downloadTest(Decision::Allow, DecisionPoint::AfterRedirect); } TEST(ContentFiltering, BlockDownloadAfterRedirect) { downloadTest(Decision::Block, DecisionPoint::AfterRedirect); } TEST(ContentFiltering, AllowDownloadAfterResponse) { downloadTest(Decision::Allow, DecisionPoint::AfterResponse); } TEST(ContentFiltering, BlockDownloadAfterResponse) { downloadTest(Decision::Block, DecisionPoint::AfterResponse); } TEST(ContentFiltering, AllowDownloadAfterAddData) { downloadTest(Decision::Allow, DecisionPoint::AfterAddData); } TEST(ContentFiltering, BlockDownloadAfterAddData) { downloadTest(Decision::Block, DecisionPoint::AfterAddData); } TEST(ContentFiltering, AllowDownloadAfterFinishedAddingData) { downloadTest(Decision::Allow, DecisionPoint::AfterFinishedAddingData); } TEST(ContentFiltering, BlockDownloadAfterFinishedAddingData) { downloadTest(Decision::Block, DecisionPoint::AfterFinishedAddingData); } TEST(ContentFiltering, AllowDownloadNever) { downloadTest(Decision::Allow, DecisionPoint::Never); } TEST(ContentFiltering, BlockDownloadNever) { downloadTest(Decision::Block, DecisionPoint::Never); } @interface LoadAlternateNavigationDelegate : NSObject @end @implementation LoadAlternateNavigationDelegate - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { EXPECT_WK_STREQ(WebKitErrorDomain, error.domain); EXPECT_EQ(kWKErrorCodeFrameLoadBlockedByContentFilter, error.code); [webView _loadAlternateHTMLString:@"FAIL" baseURL:nil forUnreachableURL:[error.userInfo objectForKey:NSURLErrorFailingURLErrorKey]]; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { [webView evaluateJavaScript:@"document.body.innerText" completionHandler:^ (id result, NSError *error) { EXPECT_TRUE([result isKindOfClass:[NSString class]]); EXPECT_WK_STREQ(@"blocked", result); isDone = true; }]; } @end static void loadAlternateTest(Decision decision, DecisionPoint decisionPoint) { @autoreleasepool { [TestProtocol registerWithScheme:@"https"]; auto configuration = configurationWithContentFilterSettings(decision, decisionPoint); auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]); auto navigationDelegate = adoptNS([[LoadAlternateNavigationDelegate alloc] init]); [webView setNavigationDelegate:navigationDelegate.get()]; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://redirect/?result"]]]; isDone = false; TestWebKitAPI::Util::run(&isDone); [TestProtocol unregister]; } } TEST(ContentFiltering, LoadAlternateAfterWillSendRequestWK2) { loadAlternateTest(Decision::Block, DecisionPoint::AfterWillSendRequest); } TEST(ContentFiltering, LoadAlternateAfterRedirectWK2) { loadAlternateTest(Decision::Block, DecisionPoint::AfterRedirect); } TEST(ContentFiltering, LoadAlternateAfterResponseWK2) { loadAlternateTest(Decision::Block, DecisionPoint::AfterResponse); } TEST(ContentFiltering, LoadAlternateAfterAddDataWK2) { loadAlternateTest(Decision::Block, DecisionPoint::AfterAddData); } TEST(ContentFiltering, LoadAlternateAfterFinishedAddingDataWK2) { loadAlternateTest(Decision::Block, DecisionPoint::AfterFinishedAddingData); } @interface LazilyLoadPlatformFrameworksController : NSObject @property (nonatomic, readonly) WKWebView *webView; - (void)expectParentalControlsLoaded:(BOOL)parentalControlsShouldBeLoaded; @end @implementation LazilyLoadPlatformFrameworksController { RetainPtr _webView; RetainPtr> _remoteObjectProxy; } - (instancetype)init { if (!(self = [super init])) return nil; WKWebViewConfiguration *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ContentFilteringPlugIn"]; _webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration]); [_webView setNavigationDelegate:self]; _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ContentFilteringProtocol)]; _remoteObjectProxy = [[_webView _remoteObjectRegistry] remoteObjectProxyWithInterface:interface]; return self; } - (WKWebView *)webView { return _webView.get(); } - (void)expectParentalControlsLoaded:(BOOL)parentalControlsShouldBeLoaded { isDone = false; [_remoteObjectProxy checkIfPlatformFrameworksAreLoaded:^(BOOL parentalControlsLoaded) { #if HAVE(PARENTAL_CONTROLS) EXPECT_EQ(static_cast(parentalControlsShouldBeLoaded), static_cast(parentalControlsLoaded)); #endif isDone = true; }]; TestWebKitAPI::Util::run(&isDone); } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { isDone = true; } @end static BOOL filterRequired(id self, SEL _cmd) { return YES; } static BOOL isManagedSession(id self, SEL _cmd) { return YES; } TEST(ContentFiltering, LazilyLoadPlatformFrameworks) { // Swizzle [NEFilterSource filterRequired] to return YES in the UI process since NetworkExtension will not be loaded otherwise. Method method = class_getClassMethod(getNEFilterSourceClass(), @selector(filterRequired)); method_setImplementation(method, reinterpret_cast(filterRequired)); // Swizzle [WebFilterEvaluator isManagedSession] to return YES in the UI process since WebContentAnalysis will not be loaded otherwise. method = class_getClassMethod(getWebFilterEvaluatorClass(), @selector(isManagedSession)); method_setImplementation(method, reinterpret_cast(isManagedSession)); @autoreleasepool { auto controller = adoptNS([[LazilyLoadPlatformFrameworksController alloc] init]); [controller expectParentalControlsLoaded:NO]; isDone = false; [[controller webView] loadHTMLString:@"PASS" baseURL:[NSURL URLWithString:@"about:blank"]]; TestWebKitAPI::Util::run(&isDone); [controller expectParentalControlsLoaded:NO]; isDone = false; [[controller webView] loadData:[NSData dataWithBytes:"PASS" length:4] MIMEType:@"text/html" characterEncodingName:@"UTF-8" baseURL:[NSURL URLWithString:@"about:blank"]]; TestWebKitAPI::Util::run(&isDone); [controller expectParentalControlsLoaded:NO]; isDone = false; NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"ContentFiltering" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]; [[controller webView] loadFileURL:fileURL allowingReadAccessToURL:fileURL]; TestWebKitAPI::Util::run(&isDone); [controller expectParentalControlsLoaded:NO]; isDone = false; [TestProtocol registerWithScheme:@"custom"]; [[controller webView] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"custom://test"]]]; TestWebKitAPI::Util::run(&isDone); [controller expectParentalControlsLoaded:NO]; [TestProtocol unregister]; isDone = false; [TestProtocol registerWithScheme:@"http"]; [[controller webView] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://test"]]]; TestWebKitAPI::Util::run(&isDone); #if PLATFORM(MAC) || ENABLE(CONTENT_FILTERING_IN_NETWORKING_PROCESS) [controller expectParentalControlsLoaded:NO]; #else [controller expectParentalControlsLoaded:YES]; #endif [TestProtocol unregister]; #if PLATFORM(MAC) isDone = false; [TestProtocol registerWithScheme:@"https"]; [[controller webView] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://test"]]]; TestWebKitAPI::Util::run(&isDone); #if ENABLE(CONTENT_FILTERING_IN_NETWORKING_PROCESS) [controller expectParentalControlsLoaded:NO]; #else [controller expectParentalControlsLoaded:YES]; #endif [TestProtocol unregister]; #endif } } #endif // ENABLE(CONTENT_FILTERING)