/* * Copyright (C) 2018 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 HAVE(SAFE_BROWSING) #import "ClassMethodSwizzler.h" #import "PlatformUtilities.h" #import "TestNavigationDelegate.h" #import "TestWKWebView.h" #import #import #import #import #import #import #import static bool committedNavigation; static bool warningShown; static bool didCloseCalled; @interface SafeBrowsingNavigationDelegate : NSObject @end @implementation SafeBrowsingNavigationDelegate - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation { committedNavigation = true; } - (void)_webViewDidShowSafeBrowsingWarning:(WKWebView *)webView { warningShown = true; } - (void)webViewDidClose:(WKWebView *)webView { didCloseCalled = true; } @end @interface TestServiceLookupResult : NSObject { RetainPtr _provider; BOOL _isPhishing; BOOL _isMalware; BOOL _isUnwantedSoftware; } @end @implementation TestServiceLookupResult + (instancetype)resultWithProvider:(RetainPtr&&)provider phishing:(BOOL)phishing malware:(BOOL)malware unwantedSoftware:(BOOL)unwantedSoftware { auto result = adoptNS([[TestServiceLookupResult alloc] init]); if (!result) return nil; result->_provider = WTFMove(provider); result->_isPhishing = phishing; result->_isMalware = malware; result->_isUnwantedSoftware = unwantedSoftware; return result.autorelease(); } - (NSString *)provider { return _provider.get(); } - (BOOL)isPhishing { return _isPhishing; } - (BOOL)isMalware { return _isMalware; } - (BOOL)isUnwantedSoftware { return _isUnwantedSoftware; } #if HAVE(SAFE_BROWSING_RESULT_DETAILS) - (NSString *)malwareDetailsBaseURLString { return @"test://"; } - (NSURL *)learnMoreURL { return [NSURL URLWithString:@"test://"]; } - (NSString *)reportAnErrorBaseURLString { return @"test://"; } - (NSString *)localizedProviderDisplayName { return @"test display name"; } #endif @end @interface TestLookupResult : NSObject { RetainPtr _results; } @end @implementation TestLookupResult + (instancetype)resultWithResults:(RetainPtr>&&)results { auto result = adoptNS([[TestLookupResult alloc] init]); if (!result) return nil; result->_results = WTFMove(results); return result.autorelease(); } - (NSArray *)serviceLookupResults { return _results.get(); } @end @interface TestLookupContext : NSObject @end @implementation TestLookupContext + (TestLookupContext *)sharedLookupContext { static TestLookupContext *context = [[TestLookupContext alloc] init]; return context; } - (void)lookUpURL:(NSURL *)URL completionHandler:(void (^)(TestLookupResult *, NSError *))completionHandler { completionHandler([TestLookupResult resultWithResults:@[[TestServiceLookupResult resultWithProvider:@"TestProvider" phishing:YES malware:NO unwantedSoftware:NO]]], nil); } @end static NSURL *resourceURL(NSString *resource) { return [[NSBundle mainBundle] URLForResource:resource withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]; } TEST(SafeBrowsing, Preference) { ClassMethodSwizzler swizzler(objc_getClass("SSBLookupContext"), @selector(sharedLookupContext), [TestLookupContext methodForSelector:@selector(sharedLookupContext)]); __block bool done = false; auto delegate = adoptNS([TestNavigationDelegate new]); delegate.get().didStartProvisionalNavigation = ^(WKWebView *, WKNavigation *) { done = true; }; auto webView = adoptNS([WKWebView new]); EXPECT_TRUE([webView configuration].preferences.fraudulentWebsiteWarningEnabled); [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES; [webView setNavigationDelegate:delegate.get()]; [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES; [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple")]]; while (![webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); [webView configuration].preferences.fraudulentWebsiteWarningEnabled = NO; [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple2")]]; TestWebKitAPI::Util::run(&done); EXPECT_FALSE([webView configuration].preferences.fraudulentWebsiteWarningEnabled); EXPECT_FALSE([webView _safeBrowsingWarning]); } static RetainPtr safeBrowsingView() { ClassMethodSwizzler swizzler(objc_getClass("SSBLookupContext"), @selector(sharedLookupContext), [TestLookupContext methodForSelector:@selector(sharedLookupContext)]); static auto delegate = adoptNS([SafeBrowsingNavigationDelegate new]); auto webView = adoptNS([WKWebView new]); [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES; [webView setNavigationDelegate:delegate.get()]; [webView setUIDelegate:delegate.get()]; [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple")]]; EXPECT_FALSE(warningShown); while (![webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); EXPECT_TRUE(warningShown); #if !PLATFORM(MAC) [[webView _safeBrowsingWarning] didMoveToWindow]; #endif return webView; } #if PLATFORM(MAC) static void checkTitleAndClick(NSButton *button, const char* expectedTitle) { EXPECT_STREQ(button.title.UTF8String, expectedTitle); [button performClick:nil]; } #else static void checkTitleAndClick(UIButton *button, const char* expectedTitle) { EXPECT_STREQ([button attributedTitleForState:UIControlStateNormal].string.UTF8String, expectedTitle); UIView *target = button.superview.superview; SEL selector = NSSelectorFromString(strcmp(expectedTitle, "Go Back") ? @"showDetailsClicked" : @"goBackClicked"); [target performSelector:selector]; } #endif template void goBack(ViewType *view, bool mainFrame = true) { WKWebView *webView = (WKWebView *)view.superview; auto box = view.subviews.firstObject; checkTitleAndClick(box.subviews[3], "Go Back"); if (mainFrame) EXPECT_EQ([webView _safeBrowsingWarning], nil); else EXPECT_NE([webView _safeBrowsingWarning], nil); } TEST(SafeBrowsing, GoBack) { auto webView = safeBrowsingView(); EXPECT_FALSE(didCloseCalled); goBack([webView _safeBrowsingWarning]); EXPECT_TRUE(didCloseCalled); } TEST(SafeBrowsing, GoBackAfterRestoreFromSessionState) { auto webView1 = adoptNS([WKWebView new]); [webView1 loadRequest:[NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"simple" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]]; [webView1 _test_waitForDidFinishNavigation]; _WKSessionState *state = [webView1 _sessionState]; ClassMethodSwizzler swizzler(objc_getClass("SSBLookupContext"), @selector(sharedLookupContext), [TestLookupContext methodForSelector:@selector(sharedLookupContext)]); auto delegate = adoptNS([SafeBrowsingNavigationDelegate new]); auto webView2 = adoptNS([WKWebView new]); [webView2 configuration].preferences.fraudulentWebsiteWarningEnabled = YES; [webView2 setNavigationDelegate:delegate.get()]; [webView2 setUIDelegate:delegate.get()]; [webView2 _restoreSessionState:state andNavigate:YES]; EXPECT_FALSE(warningShown); while (![webView2 _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); EXPECT_TRUE(warningShown); #if !PLATFORM(MAC) [[webView2 _safeBrowsingWarning] didMoveToWindow]; #endif EXPECT_FALSE(didCloseCalled); goBack([webView2 _safeBrowsingWarning]); EXPECT_TRUE(didCloseCalled); WKBackForwardList *list = [webView2 backForwardList]; EXPECT_FALSE(!!list.backItem); EXPECT_FALSE(!!list.forwardItem); EXPECT_TRUE([list.currentItem.URL.path hasSuffix:@"/simple.html"]); } template void visitUnsafeSite(ViewType *view) { [view performSelector:NSSelectorFromString(@"clickedOnLink:") withObject:[NSURL URLWithString:@"WKVisitUnsafeWebsiteSentinel"]]; } TEST(SafeBrowsing, VisitUnsafeWebsite) { auto webView = safeBrowsingView(); auto warning = [webView _safeBrowsingWarning]; EXPECT_EQ(warning.subviews.count, 1ull); #if PLATFORM(MAC) EXPECT_GT(warning.subviews.firstObject.subviews[2].frame.size.height, 0); #endif EXPECT_WK_STREQ([webView title], "Deceptive Website Warning"); checkTitleAndClick(warning.subviews.firstObject.subviews[4], "Show Details"); EXPECT_EQ(warning.subviews.count, 2ull); EXPECT_FALSE(committedNavigation); visitUnsafeSite(warning); EXPECT_WK_STREQ([webView title], ""); TestWebKitAPI::Util::run(&committedNavigation); } TEST(SafeBrowsing, NavigationClearsWarning) { auto webView = safeBrowsingView(); EXPECT_NE([webView _safeBrowsingWarning], nil); [webView loadRequest:[NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"simple2" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]]; while ([webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); } TEST(SafeBrowsing, ShowWarningSPI) { __block bool completionHandlerCalled = false; __block BOOL shouldContinueValue = NO; auto webView = adoptNS([WKWebView new]); auto showWarning = ^{ completionHandlerCalled = false; [webView _showSafeBrowsingWarningWithURL:nil title:@"test title" warning:@"test warning" details:adoptNS([[NSAttributedString alloc] initWithString:@"test details"]).get() completionHandler:^(BOOL shouldContinue) { shouldContinueValue = shouldContinue; completionHandlerCalled = true; }]; #if !PLATFORM(MAC) [[webView _safeBrowsingWarning] didMoveToWindow]; #endif }; showWarning(); checkTitleAndClick([webView _safeBrowsingWarning].subviews.firstObject.subviews[3], "Go Back"); TestWebKitAPI::Util::run(&completionHandlerCalled); EXPECT_FALSE(shouldContinueValue); showWarning(); [[webView _safeBrowsingWarning] performSelector:NSSelectorFromString(@"clickedOnLink:") withObject:[WKWebView _visitUnsafeWebsiteSentinel]]; TestWebKitAPI::Util::run(&completionHandlerCalled); EXPECT_TRUE(shouldContinueValue); } static Vector urls; @interface SafeBrowsingObserver : NSObject @end @implementation SafeBrowsingObserver - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { urls.append((NSURL *)[change objectForKey:NSKeyValueChangeNewKey]); } @end TEST(SafeBrowsing, URLObservation) { ClassMethodSwizzler swizzler(objc_getClass("SSBLookupContext"), @selector(sharedLookupContext), [TestLookupContext methodForSelector:@selector(sharedLookupContext)]); RetainPtr simpleURL = [[NSBundle mainBundle] URLForResource:@"simple" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]; RetainPtr simple2URL = [[NSBundle mainBundle] URLForResource:@"simple2" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]; auto observer = adoptNS([SafeBrowsingObserver new]); auto webViewWithWarning = [&] () -> RetainPtr { auto webView = adoptNS([WKWebView new]); [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES; [webView addObserver:observer.get() forKeyPath:@"URL" options:NSKeyValueObservingOptionNew context:nil]; [webView loadHTMLString:@"meaningful content to be drawn" baseURL:simpleURL.get()]; while (![webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); #if !PLATFORM(MAC) [[webView _safeBrowsingWarning] didMoveToWindow]; #endif visitUnsafeSite([webView _safeBrowsingWarning]); EXPECT_TRUE(!![webView _safeBrowsingWarning]); while ([webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); EXPECT_FALSE(!![webView _safeBrowsingWarning]); [webView evaluateJavaScript:[NSString stringWithFormat:@"window.location='%@'", simple2URL.get()] completionHandler:nil]; while (![webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); #if !PLATFORM(MAC) [[webView _safeBrowsingWarning] didMoveToWindow]; #endif return webView; }; auto checkURLs = [&] (Vector>&& expected) { EXPECT_EQ(urls.size(), expected.size()); if (urls.size() != expected.size()) return; for (size_t i = 0; i < expected.size(); ++i) EXPECT_STREQ(urls[i].string().utf8().data(), [expected[i] absoluteString].UTF8String); }; { auto webView = webViewWithWarning(); checkURLs({ simpleURL, simple2URL }); goBack([webView _safeBrowsingWarning]); checkURLs({ simpleURL, simple2URL, simpleURL }); [webView removeObserver:observer.get() forKeyPath:@"URL"]; } urls.clear(); { auto webView = webViewWithWarning(); checkURLs({ simpleURL, simple2URL }); visitUnsafeSite([webView _safeBrowsingWarning]); TestWebKitAPI::Util::spinRunLoop(5); checkURLs({ simpleURL, simple2URL }); [webView removeObserver:observer.get() forKeyPath:@"URL"]; } } static RetainPtr phishingResourceName; @interface SimpleLookupContext : NSObject @end @implementation SimpleLookupContext + (SimpleLookupContext *)sharedLookupContext { static SimpleLookupContext *context = [[SimpleLookupContext alloc] init]; return context; } - (void)lookUpURL:(NSURL *)URL completionHandler:(void (^)(TestLookupResult *, NSError *))completionHandler { BOOL phishing = NO; if ([URL isEqual:resourceURL(phishingResourceName.get())]) phishing = YES; completionHandler([TestLookupResult resultWithResults:@[[TestServiceLookupResult resultWithProvider:@"TestProvider" phishing:phishing malware:NO unwantedSoftware:NO]]], nil); } @end static bool navigationFinished; @interface WKWebViewGoBackNavigationDelegate : NSObject @end @implementation WKWebViewGoBackNavigationDelegate - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { navigationFinished = true; } @end TEST(SafeBrowsing, WKWebViewGoBack) { phishingResourceName = @"simple3"; ClassMethodSwizzler swizzler(objc_getClass("SSBLookupContext"), @selector(sharedLookupContext), [SimpleLookupContext methodForSelector:@selector(sharedLookupContext)]); auto delegate = adoptNS([WKWebViewGoBackNavigationDelegate new]); auto webView = adoptNS([WKWebView new]); [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES; [webView setNavigationDelegate:delegate.get()]; [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple")]]; TestWebKitAPI::Util::run(&navigationFinished); navigationFinished = false; [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple2")]]; TestWebKitAPI::Util::run(&navigationFinished); navigationFinished = false; [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple3")]]; while (![webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); [webView goBack]; TestWebKitAPI::Util::run(&navigationFinished); EXPECT_TRUE([[webView URL] isEqual:resourceURL(@"simple2")]); } TEST(SafeBrowsing, WKWebViewGoBackIFrame) { phishingResourceName = @"simple"; ClassMethodSwizzler swizzler(objc_getClass("SSBLookupContext"), @selector(sharedLookupContext), [SimpleLookupContext methodForSelector:@selector(sharedLookupContext)]); auto delegate = adoptNS([WKWebViewGoBackNavigationDelegate new]); auto webView = adoptNS([WKWebView new]); [webView configuration].preferences._safeBrowsingEnabled = YES; [webView setNavigationDelegate:delegate.get()]; [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple2")]]; TestWebKitAPI::Util::run(&navigationFinished); [webView loadRequest:[NSURLRequest requestWithURL:resourceURL(@"simple-iframe")]]; while (![webView _safeBrowsingWarning]) TestWebKitAPI::Util::spinRunLoop(); #if !PLATFORM(MAC) [[webView _safeBrowsingWarning] didMoveToWindow]; #endif navigationFinished = false; goBack([webView _safeBrowsingWarning], false); TestWebKitAPI::Util::run(&navigationFinished); EXPECT_TRUE([[webView URL] isEqual:resourceURL(@"simple2")]); } @interface NullLookupContext : NSObject @end @implementation NullLookupContext + (NullLookupContext *)sharedLookupContext { return nil; } @end TEST(SafeBrowsing, MissingFramework) { ClassMethodSwizzler swizzler(objc_getClass("SSBLookupContext"), @selector(sharedLookupContext), [NullLookupContext methodForSelector:@selector(sharedLookupContext)]); auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600)]); [webView synchronouslyLoadTestPageNamed:@"simple"]; } #endif // HAVE(SAFE_BROWSING)