[iOS] Deny mach lookup access to content filter service in the WebContent sandbox
[WebKit.git] / Tools / TestWebKitAPI / Tests / WebKitCocoa / ContentFiltering.mm
1 /*
2  * Copyright (C) 2015-2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27
28 #import "ContentFiltering.h"
29 #import "MockContentFilterSettings.h"
30 #import "PlatformUtilities.h"
31 #import "TestProtocol.h"
32 #import "WKWebViewConfigurationExtras.h"
33 #import <WebKit/WKErrorRef.h>
34 #import <WebKit/WKNavigationDelegatePrivate.h>
35 #import <WebKit/WKProcessPoolPrivate.h>
36 #import <WebKit/WKWebView.h>
37 #import <WebKit/WKWebViewPrivate.h>
38 #import <WebKit/_WKDownloadDelegate.h>
39 #import <WebKit/_WKRemoteObjectInterface.h>
40 #import <WebKit/_WKRemoteObjectRegistry.h>
41 #import <pal/spi/cocoa/NEFilterSourceSPI.h>
42 #import <pal/spi/cocoa/WebFilterEvaluatorSPI.h>
43 #import <wtf/RetainPtr.h>
44 #import <wtf/SoftLinking.h>
45
46 SOFT_LINK_FRAMEWORK_OPTIONAL(NetworkExtension);
47 SOFT_LINK_CLASS_OPTIONAL(NetworkExtension, NEFilterSource);
48
49 SOFT_LINK_PRIVATE_FRAMEWORK(WebContentAnalysis);
50 SOFT_LINK_CLASS(WebContentAnalysis, WebFilterEvaluator);
51
52 using Decision = WebCore::MockContentFilterSettings::Decision;
53 using DecisionPoint = WebCore::MockContentFilterSettings::DecisionPoint;
54
55 static bool isDone;
56
57 @interface MockContentFilterEnabler : NSObject <NSCopying, NSSecureCoding>
58 - (instancetype)initWithDecision:(Decision)decision decisionPoint:(DecisionPoint)decisionPoint;
59 @end
60
61 @implementation MockContentFilterEnabler {
62     Decision _decision;
63     DecisionPoint _decisionPoint;
64 }
65
66 + (BOOL)supportsSecureCoding
67 {
68     return YES;
69 }
70
71 - (id)copyWithZone:(NSZone *)zone
72 {
73     return [self retain];
74 }
75
76 - (instancetype)initWithCoder:(NSCoder *)decoder
77 {
78     return [super init];
79 }
80
81 - (instancetype)initWithDecision:(Decision)decision decisionPoint:(DecisionPoint)decisionPoint
82 {
83     if (!(self = [super init]))
84         return nil;
85
86     _decision = decision;
87     _decisionPoint = decisionPoint;
88     return self;
89 }
90
91 - (void)encodeWithCoder:(NSCoder *)coder
92 {
93     [coder encodeInt:static_cast<int>(_decision) forKey:@"Decision"];
94     [coder encodeInt:static_cast<int>(_decisionPoint) forKey:@"DecisionPoint"];
95 }
96
97 @end
98
99 static RetainPtr<WKWebViewConfiguration> configurationWithContentFilterSettings(Decision decision, DecisionPoint decisionPoint)
100 {
101     auto configuration = retainPtr([WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ContentFilteringPlugIn"]);
102     auto contentFilterEnabler = adoptNS([[MockContentFilterEnabler alloc] initWithDecision:decision decisionPoint:decisionPoint]);
103     [[configuration processPool] _setObject:contentFilterEnabler.get() forBundleParameter:NSStringFromClass([MockContentFilterEnabler class])];
104     return configuration;
105 }
106
107 @interface ServerRedirectNavigationDelegate : NSObject <WKNavigationDelegate>
108 @end
109
110 @implementation ServerRedirectNavigationDelegate
111
112 - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation
113 {
114     EXPECT_WK_STREQ(webView.URL.absoluteString, @"http://redirect/?pass");
115 }
116
117 - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation
118 {
119     EXPECT_WK_STREQ(webView.URL.absoluteString, @"http://pass/");
120 }
121
122 - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation
123 {
124     isDone = true;
125 }
126
127 @end
128
129 TEST(ContentFiltering, URLAfterServerRedirect)
130 {
131     @autoreleasepool {
132         [TestProtocol registerWithScheme:@"http"];
133
134         auto configuration = configurationWithContentFilterSettings(Decision::Allow, DecisionPoint::AfterAddData);
135         auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]);
136         auto navigationDelegate = adoptNS([[ServerRedirectNavigationDelegate alloc] init]);
137         [webView setNavigationDelegate:navigationDelegate.get()];
138         [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://redirect?pass"]]];
139         TestWebKitAPI::Util::run(&isDone);
140
141         [TestProtocol unregister];
142     }
143 }
144
145 @interface BecomeDownloadDelegate : NSObject <WKNavigationDelegate>
146 @end
147
148 @implementation BecomeDownloadDelegate
149
150 - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
151 {
152     decisionHandler(_WKNavigationResponsePolicyBecomeDownload);
153 }
154
155 - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
156 {
157     isDone = true;
158 }
159
160 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
161 {
162     isDone = true;
163 }
164
165 @end
166
167 static bool downloadDidStart;
168
169 @interface ContentFilteringDownloadDelegate : NSObject <_WKDownloadDelegate>
170 @end
171
172 @implementation ContentFilteringDownloadDelegate
173
174 - (void)_downloadDidStart:(_WKDownload *)download
175 {
176     downloadDidStart = true;
177 }
178
179 @end
180
181 static void downloadTest(Decision decision, DecisionPoint decisionPoint)
182 {
183     @autoreleasepool {
184         [TestProtocol registerWithScheme:@"http"];
185
186         auto configuration = configurationWithContentFilterSettings(decision, decisionPoint);
187         auto downloadDelegate = adoptNS([[ContentFilteringDownloadDelegate alloc] init]);
188         [[configuration processPool] _setDownloadDelegate:downloadDelegate.get()];
189         auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]);
190         auto navigationDelegate = adoptNS([[BecomeDownloadDelegate alloc] init]);
191         [webView setNavigationDelegate:navigationDelegate.get()];
192         [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://redirect/?download"]]];
193
194         isDone = false;
195         downloadDidStart = false;
196         const bool downloadShouldStart = decision == Decision::Allow || decisionPoint > DecisionPoint::AfterResponse;
197         if (downloadShouldStart)
198             TestWebKitAPI::Util::run(&downloadDidStart);
199         else
200             TestWebKitAPI::Util::run(&isDone);
201
202         EXPECT_EQ(downloadShouldStart, downloadDidStart);
203
204         [TestProtocol unregister];
205     }
206 }
207
208 TEST(ContentFiltering, AllowDownloadAfterWillSendRequest)
209 {
210     downloadTest(Decision::Allow, DecisionPoint::AfterWillSendRequest);
211 }
212
213 TEST(ContentFiltering, BlockDownloadAfterWillSendRequest)
214 {
215     downloadTest(Decision::Block, DecisionPoint::AfterWillSendRequest);
216 }
217
218 TEST(ContentFiltering, AllowDownloadAfterRedirect)
219 {
220     downloadTest(Decision::Allow, DecisionPoint::AfterRedirect);
221 }
222
223 TEST(ContentFiltering, BlockDownloadAfterRedirect)
224 {
225     downloadTest(Decision::Block, DecisionPoint::AfterRedirect);
226 }
227
228 TEST(ContentFiltering, AllowDownloadAfterResponse)
229 {
230     downloadTest(Decision::Allow, DecisionPoint::AfterResponse);
231 }
232
233 TEST(ContentFiltering, BlockDownloadAfterResponse)
234 {
235     downloadTest(Decision::Block, DecisionPoint::AfterResponse);
236 }
237
238 TEST(ContentFiltering, AllowDownloadAfterAddData)
239 {
240     downloadTest(Decision::Allow, DecisionPoint::AfterAddData);
241 }
242
243 TEST(ContentFiltering, BlockDownloadAfterAddData)
244 {
245     downloadTest(Decision::Block, DecisionPoint::AfterAddData);
246 }
247
248 TEST(ContentFiltering, AllowDownloadAfterFinishedAddingData)
249 {
250     downloadTest(Decision::Allow, DecisionPoint::AfterFinishedAddingData);
251 }
252
253 TEST(ContentFiltering, BlockDownloadAfterFinishedAddingData)
254 {
255     downloadTest(Decision::Block, DecisionPoint::AfterFinishedAddingData);
256 }
257
258 TEST(ContentFiltering, AllowDownloadNever)
259 {
260     downloadTest(Decision::Allow, DecisionPoint::Never);
261 }
262
263 TEST(ContentFiltering, BlockDownloadNever)
264 {
265     downloadTest(Decision::Block, DecisionPoint::Never);
266 }
267
268 @interface LoadAlternateNavigationDelegate : NSObject <WKNavigationDelegate>
269 @end
270
271 @implementation LoadAlternateNavigationDelegate
272
273 - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
274 {
275     EXPECT_WK_STREQ(WebKitErrorDomain, error.domain);
276     EXPECT_EQ(kWKErrorCodeFrameLoadBlockedByContentFilter, error.code);
277     [webView _loadAlternateHTMLString:@"FAIL" baseURL:nil forUnreachableURL:[error.userInfo objectForKey:NSURLErrorFailingURLErrorKey]];
278 }
279
280 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
281 {
282     [webView evaluateJavaScript:@"document.body.innerText" completionHandler:^ (id result, NSError *error) {
283         EXPECT_TRUE([result isKindOfClass:[NSString class]]);
284         EXPECT_WK_STREQ(@"blocked", result);
285         isDone = true;
286     }];
287 }
288
289 @end
290
291 static void loadAlternateTest(Decision decision, DecisionPoint decisionPoint)
292 {
293     @autoreleasepool {
294         [TestProtocol registerWithScheme:@"http"];
295
296         auto configuration = configurationWithContentFilterSettings(decision, decisionPoint);
297         auto webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration.get()]);
298         auto navigationDelegate = adoptNS([[LoadAlternateNavigationDelegate alloc] init]);
299         [webView setNavigationDelegate:navigationDelegate.get()];
300         [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://redirect/?result"]]];
301
302         isDone = false;
303         TestWebKitAPI::Util::run(&isDone);
304
305         [TestProtocol unregister];
306     }
307 }
308
309 TEST(ContentFiltering, LoadAlternateAfterWillSendRequestWK2)
310 {
311     loadAlternateTest(Decision::Block, DecisionPoint::AfterWillSendRequest);
312 }
313
314 TEST(ContentFiltering, LoadAlternateAfterRedirectWK2)
315 {
316     loadAlternateTest(Decision::Block, DecisionPoint::AfterRedirect);
317 }
318
319 TEST(ContentFiltering, LoadAlternateAfterResponseWK2)
320 {
321     loadAlternateTest(Decision::Block, DecisionPoint::AfterResponse);
322 }
323
324 TEST(ContentFiltering, LoadAlternateAfterAddDataWK2)
325 {
326     loadAlternateTest(Decision::Block, DecisionPoint::AfterAddData);
327 }
328
329 TEST(ContentFiltering, LoadAlternateAfterFinishedAddingDataWK2)
330 {
331     loadAlternateTest(Decision::Block, DecisionPoint::AfterFinishedAddingData);
332 }
333
334
335 @interface LazilyLoadPlatformFrameworksController : NSObject <WKNavigationDelegate>
336 @property (nonatomic, readonly) WKWebView *webView;
337 - (void)expectParentalControlsLoaded:(BOOL)parentalControlsShouldBeLoaded networkExtensionLoaded:(BOOL)networkExtensionShouldBeLoaded;
338 @end
339
340 @implementation LazilyLoadPlatformFrameworksController {
341     RetainPtr<WKWebView> _webView;
342     RetainPtr<id <ContentFilteringProtocol>> _remoteObjectProxy;
343 }
344
345 - (instancetype)init
346 {
347     if (!(self = [super init]))
348         return nil;
349
350     WKWebViewConfiguration *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ContentFilteringPlugIn"];
351     _webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
352     [_webView setNavigationDelegate:self];
353
354     _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ContentFilteringProtocol)];
355     _remoteObjectProxy = [[_webView _remoteObjectRegistry] remoteObjectProxyWithInterface:interface];
356
357     return self;
358 }
359
360 - (WKWebView *)webView
361 {
362     return _webView.get();
363 }
364
365 - (void)expectParentalControlsLoaded:(BOOL)parentalControlsShouldBeLoaded networkExtensionLoaded:(BOOL)networkExtensionShouldBeLoaded
366 {
367     isDone = false;
368     [_remoteObjectProxy checkIfPlatformFrameworksAreLoaded:^(BOOL parentalControlsLoaded, BOOL networkExtensionLoaded) {
369 #if HAVE(PARENTAL_CONTROLS)
370         EXPECT_EQ(static_cast<bool>(parentalControlsShouldBeLoaded), static_cast<bool>(parentalControlsLoaded));
371 #endif
372 #if HAVE(NETWORK_EXTENSION)
373         EXPECT_EQ(static_cast<bool>(networkExtensionShouldBeLoaded), static_cast<bool>(networkExtensionLoaded));
374 #endif // HAVE(NETWORK_EXTENSION)
375         isDone = true;
376     }];
377     TestWebKitAPI::Util::run(&isDone);
378 }
379
380 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
381 {
382     isDone = true;
383 }
384
385 @end
386
387 static BOOL filterRequired(id self, SEL _cmd)
388 {
389     return YES;
390 }
391
392 static BOOL isManagedSession(id self, SEL _cmd)
393 {
394     return YES;
395 }
396
397 TEST(ContentFiltering, LazilyLoadPlatformFrameworks)
398 {
399     // Swizzle [NEFilterSource filterRequired] to return YES in the UI process since NetworkExtension will not be loaded otherwise.
400     Method method = class_getClassMethod(getNEFilterSourceClass(), @selector(filterRequired));
401     method_setImplementation(method, reinterpret_cast<IMP>(filterRequired));
402
403     // Swizzle [WebFilterEvaluator isManagedSession] to return YES in the UI process since WebContentAnalysis will not be loaded otherwise.
404     method = class_getClassMethod(getWebFilterEvaluatorClass(), @selector(isManagedSession));
405     method_setImplementation(method, reinterpret_cast<IMP>(isManagedSession));
406
407     @autoreleasepool {
408         auto controller = adoptNS([[LazilyLoadPlatformFrameworksController alloc] init]);
409         [controller expectParentalControlsLoaded:NO networkExtensionLoaded:NO];
410
411         isDone = false;
412         [[controller webView] loadHTMLString:@"PASS" baseURL:[NSURL URLWithString:@"about:blank"]];
413         TestWebKitAPI::Util::run(&isDone);
414         [controller expectParentalControlsLoaded:NO networkExtensionLoaded:NO];
415
416         isDone = false;
417         [[controller webView] loadData:[NSData dataWithBytes:"PASS" length:4] MIMEType:@"text/html" characterEncodingName:@"UTF-8" baseURL:[NSURL URLWithString:@"about:blank"]];
418         TestWebKitAPI::Util::run(&isDone);
419         [controller expectParentalControlsLoaded:NO networkExtensionLoaded:NO];
420
421         isDone = false;
422         NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"ContentFiltering" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"];
423         [[controller webView] loadFileURL:fileURL allowingReadAccessToURL:fileURL];
424         TestWebKitAPI::Util::run(&isDone);
425         [controller expectParentalControlsLoaded:NO networkExtensionLoaded:NO];
426
427         isDone = false;
428         [TestProtocol registerWithScheme:@"custom"];
429         [[controller webView] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"custom://test"]]];
430         TestWebKitAPI::Util::run(&isDone);
431         [controller expectParentalControlsLoaded:NO networkExtensionLoaded:NO];
432         [TestProtocol unregister];
433
434         isDone = false;
435         [TestProtocol registerWithScheme:@"http"];
436         [[controller webView] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://test"]]];
437         TestWebKitAPI::Util::run(&isDone);
438 #if PLATFORM(MAC)
439         [controller expectParentalControlsLoaded:NO networkExtensionLoaded:YES];
440 #else
441         [controller expectParentalControlsLoaded:YES networkExtensionLoaded:YES];
442 #endif
443         [TestProtocol unregister];
444
445 #if PLATFORM(MAC)
446         isDone = false;
447         [TestProtocol registerWithScheme:@"https"];
448         [[controller webView] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://test"]]];
449         TestWebKitAPI::Util::run(&isDone);
450         [controller expectParentalControlsLoaded:YES networkExtensionLoaded:YES];
451         [TestProtocol unregister];
452 #endif
453     }
454 }