blob: 6b9cf5d4b8519a351f094e34808795852aa7dfce [file] [log] [blame]
/*
* 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 <WebKit/WKNavigationDelegate.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKUIDelegatePrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <wtf/RetainPtr.h>
#import <wtf/URL.h>
#import <wtf/Vector.h>
static bool committedNavigation;
static bool warningShown;
static bool didCloseCalled;
@interface SafeBrowsingNavigationDelegate : NSObject <WKNavigationDelegate, WKUIDelegatePrivate>
@end
@implementation SafeBrowsingNavigationDelegate
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation
{
committedNavigation = true;
}
- (void)_webViewDidShowSafeBrowsingWarning:(WKWebView *)webView
{
warningShown = true;
}
- (void)webViewDidClose:(WKWebView *)webView
{
didCloseCalled = true;
}
@end
@interface TestServiceLookupResult : NSObject {
RetainPtr<NSString> _provider;
BOOL _isPhishing;
BOOL _isMalware;
BOOL _isUnwantedSoftware;
}
@end
@implementation TestServiceLookupResult
+ (instancetype)resultWithProvider:(RetainPtr<NSString>&&)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;
}
@end
@interface TestLookupResult : NSObject {
RetainPtr<NSArray> _results;
}
@end
@implementation TestLookupResult
+ (instancetype)resultWithResults:(RetainPtr<NSArray<TestServiceLookupResult *>>&&)results
{
auto result = adoptNS([[TestLookupResult alloc] init]);
if (!result)
return nil;
result->_results = WTFMove(results);
return result.autorelease();
}
- (NSArray<TestServiceLookupResult *> *)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<WKWebView> 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<typename ViewType> 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<typename ViewType> 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<URL> urls;
@interface SafeBrowsingObserver : NSObject
@end
@implementation SafeBrowsingObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *, id> *)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<NSURL> simpleURL = [[NSBundle mainBundle] URLForResource:@"simple" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
RetainPtr<NSURL> simple2URL = [[NSBundle mainBundle] URLForResource:@"simple2" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
auto observer = adoptNS([SafeBrowsingObserver new]);
auto webViewWithWarning = [&] () -> RetainPtr<WKWebView> {
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<RetainPtr<NSURL>>&& 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<NSString> 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 <WKNavigationDelegate>
@end
@implementation WKWebViewGoBackNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified 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)