/* * Copyright (C) 2021 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" #import "DaemonTestUtilities.h" #import "HTTPServer.h" #import "PlatformUtilities.h" #import "Test.h" #import "TestNotificationProvider.h" #import "TestURLSchemeHandler.h" #import "TestWKWebView.h" #import "Utilities.h" #import #import #import #import #import #import #import #import #import #import #import #import #if ENABLE(NOTIFICATIONS) && ENABLE(NOTIFICATION_EVENT) && (PLATFORM(MAC) || PLATFORM(IOS)) using WebKit::WebPushD::MessageType; static bool alertReceived = false; @interface NotificationPermissionDelegate : NSObject @end @implementation NotificationPermissionDelegate - (void)_webView:(WKWebView *)webView requestNotificationPermissionForSecurityOrigin:(WKSecurityOrigin *)securityOrigin decisionHandler:(void (^)(BOOL))decisionHandler { decisionHandler(true); } - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { alertReceived = true; completionHandler(); } @end @interface NotificationScriptMessageHandler : NSObject { BlockPtr _messageHandler; } @end @implementation NotificationScriptMessageHandler - (void)setMessageHandler:(void (^)(id))handler { _messageHandler = handler; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if (_messageHandler) _messageHandler(message.body); } @end namespace TestWebKitAPI { static RetainPtr testWebPushDaemonLocation() { return [currentExecutableDirectory() URLByAppendingPathComponent:@"webpushd" isDirectory:NO]; } enum LaunchOnlyOnce : BOOL { No, Yes }; static NSDictionary *testWebPushDaemonPList(NSURL *storageLocation, LaunchOnlyOnce launchOnlyOnce) { return @{ @"Label" : @"org.webkit.webpushtestdaemon", @"LaunchOnlyOnce" : @(static_cast(launchOnlyOnce)), @"ThrottleInterval" : @(1), @"StandardErrorPath" : [storageLocation URLByAppendingPathComponent:@"daemon_stderr"].path, @"EnvironmentVariables" : @{ @"DYLD_FRAMEWORK_PATH" : currentExecutableDirectory().get().path }, @"MachServices" : @{ @"org.webkit.webpushtestdaemon.service" : @YES }, @"ProgramArguments" : @[ testWebPushDaemonLocation().get().path, @"--machServiceName", @"org.webkit.webpushtestdaemon.service", @"--useMockPushService" ] }; } static bool shouldSetupWebPushD() { static bool shouldSetup = true; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSArray *arguments = [[NSProcessInfo processInfo] arguments]; if ([arguments containsObject:@"--no-webpushd"]) shouldSetup = false; }); return shouldSetup; } static NSURL *setUpTestWebPushD(LaunchOnlyOnce launchOnlyOnce = LaunchOnlyOnce::Yes) { if (!shouldSetupWebPushD()) return nil; NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *tempDir = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"WebPushDaemonTest"] isDirectory:YES]; NSError *error = nil; if ([fileManager fileExistsAtPath:tempDir.path]) [fileManager removeItemAtURL:tempDir error:&error]; EXPECT_NULL(error); killFirstInstanceOfDaemon(@"webpushd"); registerPlistWithLaunchD(testWebPushDaemonPList(tempDir, launchOnlyOnce), tempDir); return tempDir; } // Only works if the test daemon was registered with LaunchOnlyOnce::No. static BOOL restartTestWebPushD() { return restartService(@"org.webkit.webpushtestdaemon", @"webpushd"); } static void cleanUpTestWebPushD(NSURL *tempDir) { if (!shouldSetupWebPushD()) return; killFirstInstanceOfDaemon(@"webpushd"); if (![[NSFileManager defaultManager] fileExistsAtPath:tempDir.path]) return; NSError *error = nil; [[NSFileManager defaultManager] removeItemAtURL:tempDir error:&error]; if (error) NSLog(@"Error removing tempDir URL: %@", error); EXPECT_NULL(error); } static RetainPtr createMessageDictionary(MessageType messageType, const Vector& message) { auto dictionary = adoptNS(xpc_dictionary_create(nullptr, nullptr, 0)); xpc_dictionary_set_uint64(dictionary.get(), "protocol version", 1); xpc_dictionary_set_uint64(dictionary.get(), "message type", static_cast(messageType)); xpc_dictionary_set_data(dictionary.get(), "encoded message", message.data(), message.size()); return WTFMove(dictionary); } // Uses an existing connection to the daemon for a one-off message void sendMessageToDaemon(xpc_connection_t connection, MessageType messageType, const Vector& message) { auto dictionary = createMessageDictionary(messageType, message); xpc_connection_send_message(connection, dictionary.get()); } // Uses an existing connection to the daemon for a one-off message, waiting for the reply void sendMessageToDaemonWaitingForReply(xpc_connection_t connection, MessageType messageType, const Vector& message) { auto dictionary = createMessageDictionary(messageType, message); __block bool done = false; xpc_connection_send_message_with_reply(connection, dictionary.get(), dispatch_get_main_queue(), ^(xpc_object_t request) { done = true; }); TestWebKitAPI::Util::run(&done); } static void sendConfigurationWithAuditToken(xpc_connection_t connection) { audit_token_t token = { 0, 0, 0, 0, 0, 0, 0, 0 }; mach_msg_type_number_t auditTokenCount = TASK_AUDIT_TOKEN_COUNT; kern_return_t result = task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)(&token), &auditTokenCount); if (result != KERN_SUCCESS) { EXPECT_TRUE(false); return; } // Send configuration with audit token { Vector encodedMessage(42); encodedMessage.fill(0); encodedMessage[1] = 1; encodedMessage[2] = 32; memcpy(&encodedMessage[10], &token, sizeof(token)); sendMessageToDaemon(connection, MessageType::UpdateConnectionConfiguration, encodedMessage); } } RetainPtr createAndConfigureConnectionToService(const char* serviceName) { auto connection = adoptNS(xpc_connection_create_mach_service(serviceName, dispatch_get_main_queue(), 0)); xpc_connection_set_event_handler(connection.get(), ^(xpc_object_t) { }); xpc_connection_activate(connection.get()); sendConfigurationWithAuditToken(connection.get()); return WTFMove(connection); } static Vector encodeString(const String& message) { ASSERT(message.is8Bit()); auto utf8 = message.utf8(); Vector result(utf8.length() + 5); result[0] = static_cast(utf8.length()); result[1] = static_cast(utf8.length() >> 8); result[2] = static_cast(utf8.length() >> 16); result[3] = static_cast(utf8.length() >> 24); result[4] = 0x01; auto data = utf8.data(); for (size_t i = 0; i < utf8.length(); ++i) result[5 + i] = data[i]; return result; } TEST(WebPushD, BasicCommunication) { NSURL *tempDir = setUpTestWebPushD(); auto connection = adoptNS(xpc_connection_create_mach_service("org.webkit.webpushtestdaemon.service", dispatch_get_main_queue(), 0)); __block bool done = false; xpc_connection_set_event_handler(connection.get(), ^(xpc_object_t request) { if (xpc_get_type(request) != XPC_TYPE_DICTIONARY) return; const char* debugMessage = xpc_dictionary_get_string(request, "debug message"); if (!debugMessage) return; NSString *nsMessage = [NSString stringWithUTF8String:debugMessage]; // Ignore possible connections/messages from webpushtool if ([nsMessage hasPrefix:@"[webpushtool "]) return; bool stringMatches = [nsMessage hasPrefix:@"[com.apple.WebKit.TestWebKitAPI"] || [nsMessage hasPrefix:@"[TestWebKitAPI"]; stringMatches = stringMatches && [nsMessage hasSuffix:@" Turned Debug Mode on"]; EXPECT_TRUE(stringMatches); if (!stringMatches) WTFLogAlways("String does not match, actual string was %@", nsMessage); done = true; }); xpc_connection_activate(connection.get()); sendConfigurationWithAuditToken(connection.get()); // Enable debug messages, and wait for the resulting debug message { auto dictionary = adoptNS(xpc_dictionary_create(nullptr, nullptr, 0)); Vector encodedMessage(1); encodedMessage[0] = 1; sendMessageToDaemon(connection.get(), MessageType::SetDebugModeIsEnabled, encodedMessage); TestWebKitAPI::Util::run(&done); } // Echo and wait for a reply auto dictionary = adoptNS(xpc_dictionary_create(nullptr, nullptr, 0)); auto encodedString = encodeString("hello"_s); xpc_dictionary_set_uint64(dictionary.get(), "protocol version", 1); xpc_dictionary_set_uint64(dictionary.get(), "message type", 1); xpc_dictionary_set_data(dictionary.get(), "encoded message", encodedString.data(), encodedString.size()); done = false; xpc_connection_send_message_with_reply(connection.get(), dictionary.get(), dispatch_get_main_queue(), ^(xpc_object_t reply) { if (xpc_get_type(reply) != XPC_TYPE_DICTIONARY) { NSLog(@"Unexpected non-dictionary: %@", reply); done = true; EXPECT_TRUE(FALSE); return; } size_t dataSize = 0; const void* data = xpc_dictionary_get_data(reply, "encoded message", &dataSize); EXPECT_EQ(dataSize, 15u); std::array expectedReply { 10, 0, 0, 0, 1, 'h', 'e', 'l', 'l', 'o' , 'h', 'e', 'l', 'l', 'o' }; EXPECT_FALSE(memcmp(data, expectedReply.data(), expectedReply.size())); done = true; }); TestWebKitAPI::Util::run(&done); cleanUpTestWebPushD(tempDir); } static void clearWebsiteDataStore(WKWebsiteDataStore *store) { __block bool clearedStore = false; [store removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { clearedStore = true; }]; TestWebKitAPI::Util::run(&clearedStore); } class WebPushDTest : public ::testing::Test { protected: WebPushDTest(LaunchOnlyOnce launchOnlyOnce = LaunchOnlyOnce::Yes) { [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; m_tempDirectory = retainPtr(setUpTestWebPushD(launchOnlyOnce)); auto dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]); dataStoreConfiguration.get().webPushMachServiceName = @"org.webkit.webpushtestdaemon.service"; dataStoreConfiguration.get().webPushDaemonUsesMockBundlesForTesting = YES; m_dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); m_configuration = adoptNS([[WKWebViewConfiguration alloc] init]); m_configuration.get().websiteDataStore = m_dataStore.get(); clearWebsiteDataStore([m_configuration websiteDataStore]); [m_configuration.get().preferences _setNotificationsEnabled:YES]; [m_configuration.get().preferences _setPushAPIEnabled:YES]; for (_WKExperimentalFeature *feature in [WKPreferences _experimentalFeatures]) { if ([feature.key isEqualToString:@"BuiltInNotificationsEnabled"]) [[m_configuration preferences] _setEnabled:YES forFeature:feature]; } m_testMessageHandler = adoptNS([[TestMessageHandler alloc] init]); [[m_configuration userContentController] addScriptMessageHandler:m_testMessageHandler.get() name:@"test"]; m_notificationMessageHandler = adoptNS([[NotificationScriptMessageHandler alloc] init]); [[m_configuration userContentController] addScriptMessageHandler:m_notificationMessageHandler.get() name:@"note"]; } void loadRequest(ASCIILiteral htmlSource, ASCIILiteral serviceWorkerScriptSource) { static constexpr auto constants = R"SRC( const VALID_SERVER_KEY = "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs"; const VALID_SERVER_KEY_THAT_CAUSES_INJECTED_FAILURE = "BEAxaUMo1s8tjORxJfnSSvWhYb4u51kg1hWT2s_9gpV7Zxar1pF_2BQ8AncuAdS2BoLhN4qaxzBy2CwHE8BBzWg"; )SRC"_s; m_server.reset(new TestWebKitAPI::HTTPServer({ { "/"_s, { htmlSource } }, { "/constants.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, constants } }, { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, serviceWorkerScriptSource } } }, TestWebKitAPI::HTTPServer::Protocol::Http)); m_notificationProvider = makeUnique(Vector { [[m_configuration processPool] _notificationManagerForTesting], WKNotificationManagerGetSharedServiceWorkerNotificationManager() }); m_notificationProvider->setPermission(m_server->origin(), true); m_webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:m_configuration.get()]); m_uiDelegate = adoptNS([[NotificationPermissionDelegate alloc] init]); [m_webView setUIDelegate:m_uiDelegate.get()]; [m_webView loadRequest:m_server->request()]; } bool hasPushSubscription() { __block bool done = false; __block bool result = false; [m_dataStore _scopeURL:m_server->request().URL hasPushSubscriptionForTesting:^(BOOL fetchedResult) { result = fetchedResult; done = true; }]; TestWebKitAPI::Util::run(&done); return result; } ~WebPushDTest() { cleanUpTestWebPushD(m_tempDirectory.get()); } RetainPtr injectPushMessage(NSDictionary *pushUserInfo) { String scope = m_server->request().URL.absoluteString; String topic = "com.apple.WebKit.TestWebKitAPI "_str + scope; id obj = @{ @"topic": (NSString *)topic, @"userInfo": pushUserInfo }; NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:0 error:nullptr]; String message { static_cast(data.bytes), static_cast(data.length) }; auto encodedMessage = encodeString(WTFMove(message)); auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service"); sendMessageToDaemonWaitingForReply(utilityConnection.get(), MessageType::InjectEncryptedPushMessageForTesting, encodedMessage); // Fetch push messages __block bool gotMessages = false; __block RetainPtr> messages; [m_dataStore _getPendingPushMessages:^(NSArray *rawMessages) { messages = rawMessages; gotMessages = true; }]; TestWebKitAPI::Util::run(&gotMessages); EXPECT_EQ([messages count], 1u); return [messages objectAtIndex:0]; } RetainPtr m_tempDirectory; RetainPtr m_dataStore; RetainPtr m_configuration; RetainPtr m_testMessageHandler; RetainPtr m_notificationMessageHandler; std::unique_ptr m_server; std::unique_ptr m_notificationProvider; RetainPtr m_webView; RetainPtr> m_uiDelegate; }; class WebPushDInjectedPushTest : public WebPushDTest { protected: void runTest(NSString *expectedMessage, NSDictionary *pushUserInfo); }; class WebPushDMultipleLaunchTest : public WebPushDTest { public: WebPushDMultipleLaunchTest() : WebPushDTest(LaunchOnlyOnce::No) { } }; void WebPushDInjectedPushTest::runTest(NSString *expectedMessage, NSDictionary *pushUserInfo) { static constexpr auto htmlSource = R"SWRESOURCE( )SWRESOURCE"_s; static constexpr auto serviceWorkerSource = R"SWRESOURCE( let port; self.addEventListener("message", (event) => { port = event.data.port; port.postMessage("Ready"); }); self.addEventListener("push", (event) => { try { self.registration.showNotification("notification"); if (!event.data) { port.postMessage("Received: null data"); return; } const value = event.data.text(); port.postMessage("Received: " + value); } catch (e) { port.postMessage("Error: " + e); } }); )SWRESOURCE"_s; __block bool ready = false; [m_testMessageHandler addMessage:@"Ready" withHandler:^{ ready = true; }]; __block bool gotExpectedMessage = false; [m_testMessageHandler addMessage:expectedMessage withHandler:^{ gotExpectedMessage = true; }]; loadRequest(htmlSource, serviceWorkerSource); TestWebKitAPI::Util::run(&ready); auto message = injectPushMessage(pushUserInfo); __block bool pushMessageProcessed = false; __block bool pushMessageProcessedResult = false; [m_dataStore _processPushMessage:message.get() completionHandler:^(bool result) { pushMessageProcessedResult = result; pushMessageProcessed = true; }]; TestWebKitAPI::Util::run(&gotExpectedMessage); TestWebKitAPI::Util::run(&pushMessageProcessed); EXPECT_TRUE(pushMessageProcessedResult); } TEST_F(WebPushDInjectedPushTest, HandleInjectedEmptyPush) { runTest(@"Received: null data", @{ }); } TEST_F(WebPushDInjectedPushTest, HandleInjectedAESGCMPush) { runTest(@"Received: test aesgcm payload", @{ @"content_encoding": @"aesgcm", @"as_publickey": @"BC-AgYMhqmzamH7_Aum0YvId8FV1-umgHweJNe6XQ1IMAm3E29loWXqTRndibxH27kJKWcIbyymundODMfVx_UM", @"as_salt": @"tkPT5xDeN0lAkSc6lZUkNg", @"payload": @"o/u4yvcXI1nap+zyIOBbWXdLqj1qHG2cX+KVhAdBQj1GVAt7lQ==" }); } TEST_F(WebPushDInjectedPushTest, HandleInjectedAES128GCMPush) { // From example in RFC8291 Section 5. String payloadBase64URL = "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN"_s; String payloadBase64 = base64EncodeToString(base64URLDecode(payloadBase64URL).value()); runTest(@"Received: When I grow up, I want to be a watermelon", @{ @"content_encoding": @"aes128gcm", @"payload": (NSString *)payloadBase64 }); } TEST_F(WebPushDTest, SubscribeTest) { static constexpr auto source = R"HTML( )HTML"_s; __block RetainPtr obj = nil; __block bool done = false; [m_notificationMessageHandler setMessageHandler:^(id message) { obj = message; done = true; }]; loadRequest(source, ""_s); TestWebKitAPI::Util::run(&done); ASSERT_TRUE([obj isKindOfClass:[NSDictionary class]]); NSDictionary *subscription = obj.get(); ASSERT_TRUE([subscription[@"endpoint"] hasPrefix:@"https://"]); ASSERT_TRUE([subscription[@"keys"] isKindOfClass:[NSDictionary class]]); // Shared auth secret should be 16 bytes (22 bytes in unpadded base64url). ASSERT_EQ([subscription[@"keys"][@"auth"] length], 22u); // Client public key should be 65 bytes (87 bytes in unpadded base64url). ASSERT_EQ([subscription[@"keys"][@"p256dh"] length], 87u); ASSERT_TRUE(hasPushSubscription()); } TEST_F(WebPushDTest, SubscribeFailureTest) { static constexpr auto source = R"HTML( )HTML"_s; __block RetainPtr obj = nil; __block bool done = false; [m_notificationMessageHandler setMessageHandler:^(id message) { obj = message; done = true; }]; loadRequest(source, ""_s); TestWebKitAPI::Util::run(&done); // Spec says that an error in the push service should be an AbortError. ASSERT_TRUE([obj isKindOfClass:[NSString class]]); ASSERT_TRUE([obj hasPrefix:@"Error: AbortError"]); ASSERT_FALSE(hasPushSubscription()); } TEST_F(WebPushDTest, UnsubscribeTest) { static constexpr auto source = R"HTML( )HTML"_s; __block RetainPtr obj = nil; __block bool done = false; [m_notificationMessageHandler setMessageHandler:^(id message) { obj = message; done = true; }]; loadRequest(source, ""_s); TestWebKitAPI::Util::run(&done); // First unsubscribe should succeed. Second one should fail since the first one removed the record from the database. id expected = @[@(1), @(0)]; ASSERT_TRUE([obj isEqual:expected]); ASSERT_FALSE(hasPushSubscription()); } TEST_F(WebPushDTest, UnsubscribesOnServiceWorkerUnregisterTest) { static constexpr auto source = R"HTML( )HTML"_s; __block RetainPtr unregisterSucceeded = nil; __block bool done = false; [m_notificationMessageHandler setMessageHandler:^(id message) { unregisterSucceeded = message; done = true; }]; loadRequest(source, ""_s); TestWebKitAPI::Util::run(&done); ASSERT_TRUE([unregisterSucceeded isEqual:@YES]); ASSERT_FALSE(hasPushSubscription()); } TEST_F(WebPushDTest, UnsubscribesOnClearingAllWebsiteData) { static constexpr auto source = R"HTML( )HTML"_s; __block RetainPtr result = nil; __block bool done = false; [m_notificationMessageHandler setMessageHandler:^(id message) { result = message; done = true; }]; loadRequest(source, ""_s); TestWebKitAPI::Util::run(&done); ASSERT_TRUE([result isEqualToString:@"Subscribed"]); __block bool removedData = false; [m_dataStore removeDataOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] modifiedSince:[NSDate distantPast] completionHandler:^(void) { removedData = true; }]; TestWebKitAPI::Util::run(&removedData); ASSERT_FALSE(hasPushSubscription()); } TEST_F(WebPushDTest, UnsubscribesOnClearingWebsiteDataForOrigin) { static constexpr auto source = R"HTML( )HTML"_s; __block RetainPtr result = nil; __block bool done = false; [m_notificationMessageHandler setMessageHandler:^(id message) { result = message; done = true; }]; loadRequest(source, ""_s); TestWebKitAPI::Util::run(&done); ASSERT_TRUE([result isEqualToString:@"Subscribed"]); __block bool fetchedRecords = false; __block RetainPtr> records; [m_dataStore fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray *dataRecords) { records = dataRecords; fetchedRecords = true; }]; TestWebKitAPI::Util::run(&fetchedRecords); WKWebsiteDataRecord *filteredRecord = nil; for (WKWebsiteDataRecord *record in records.get()) { for (NSString *originString in record._originsStrings) { if ([originString isEqualToString:m_server->origin()]) { filteredRecord = record; break; } } } ASSERT_TRUE(filteredRecord); __block bool removedData = false; [m_dataStore removeDataOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] forDataRecords:[NSArray arrayWithObject:filteredRecord] completionHandler:^(void) { removedData = true; }]; TestWebKitAPI::Util::run(&removedData); ASSERT_FALSE(hasPushSubscription()); } TEST_F(WebPushDTest, UnsubscribesOnPermissionReset) { static constexpr auto source = R"HTML( )HTML"_s; __block RetainPtr result = nil; __block bool done = false; [m_notificationMessageHandler setMessageHandler:^(id message) { result = message; done = true; }]; loadRequest(source, ""_s); TestWebKitAPI::Util::run(&done); ASSERT_TRUE([result isEqualToString:@"Subscribed"]); ASSERT_TRUE(hasPushSubscription()); m_notificationProvider->resetPermission(m_server->origin()); bool isSubscribed = true; TestWebKitAPI::Util::waitForConditionWithLogging([this, &isSubscribed] { isSubscribed = hasPushSubscription(); if (!isSubscribed) return true; sleep(1); return false; }, 5, @"Timed out waiting for push subscription to be removed."); ASSERT_FALSE(isSubscribed); } #if ENABLE(INSTALL_COORDINATION_BUNDLES) #if USE(APPLE_INTERNAL_SDK) TEST(WebPushD, PermissionManagement) { NSURL *tempDirectory = setUpTestWebPushD(); auto dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]); dataStoreConfiguration.get().webPushMachServiceName = @"org.webkit.webpushtestdaemon.service"; dataStoreConfiguration.get().webPushDaemonUsesMockBundlesForTesting = YES; auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); configuration.get().websiteDataStore = dataStore.get(); [configuration.get().preferences _setNotificationsEnabled:YES]; for (_WKExperimentalFeature *feature in [WKPreferences _experimentalFeatures]) { if ([feature.key isEqualToString:@"BuiltInNotificationsEnabled"]) [[configuration preferences] _setEnabled:YES forFeature:feature]; } auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); auto uiDelegate = adoptNS([[NotificationPermissionDelegate alloc] init]); [webView setUIDelegate:uiDelegate.get()]; [webView synchronouslyLoadHTMLString:@"" baseURL:[NSURL URLWithString:@"https://example.org"]]; [webView evaluateJavaScript:@"Notification.requestPermission().then(() => { alert('done') })" completionHandler:nil]; TestWebKitAPI::Util::run(&alertReceived); static bool originOperationDone = false; static RetainPtr origin; [dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet *origins) { EXPECT_EQ([origins count], 1u); origin = [origins anyObject]; originOperationDone = true; }]; TestWebKitAPI::Util::run(&originOperationDone); EXPECT_WK_STREQ(origin.get().protocol, "https"); EXPECT_WK_STREQ(origin.get().host, "example.org"); // If we failed to retrieve an expected origin, we will have failed the above checks if (!origin) { cleanUpTestWebPushD(tempDirectory); return; } originOperationDone = false; [dataStore _deletePushAndNotificationRegistration:origin.get() completionHandler:^(NSError *error) { EXPECT_FALSE(!!error); originOperationDone = true; }]; TestWebKitAPI::Util::run(&originOperationDone); originOperationDone = false; [dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet *origins) { EXPECT_EQ([origins count], 0u); originOperationDone = true; }]; TestWebKitAPI::Util::run(&originOperationDone); cleanUpTestWebPushD(tempDirectory); } static void deleteAllRegistrationsForDataStore(WKWebsiteDataStore *dataStore) { __block bool originOperationDone = false; __block RetainPtr> originSet; [dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet *origins) { originSet = origins; originOperationDone = true; }]; TestWebKitAPI::Util::run(&originOperationDone); if (![originSet count]) return; __block size_t deletedOrigins = 0; originOperationDone = false; for (WKSecurityOrigin *origin in originSet.get()) { [dataStore _deletePushAndNotificationRegistration:origin completionHandler:^(NSError *error) { EXPECT_FALSE(!!error); if (++deletedOrigins == [originSet count]) originOperationDone = true; }]; } TestWebKitAPI::Util::run(&originOperationDone); originOperationDone = false; [dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet *origins) { EXPECT_EQ([origins count], 0u); originOperationDone = true; }]; TestWebKitAPI::Util::run(&originOperationDone); } static const char* mainBytes = R"WEBPUSHRESOURCE( )WEBPUSHRESOURCE"; TEST(WebPushD, InstallCoordinationBundles) { NSURL *tempDirectory = setUpTestWebPushD(); auto dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]); dataStoreConfiguration.get().webPushMachServiceName = @"org.webkit.webpushtestdaemon.service"; auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); deleteAllRegistrationsForDataStore(dataStore.get()); auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); configuration.get().websiteDataStore = dataStore.get(); [configuration.get().preferences _setNotificationsEnabled:YES]; for (_WKExperimentalFeature *feature in [WKPreferences _experimentalFeatures]) { if ([feature.key isEqualToString:@"BuiltInNotificationsEnabled"]) [[configuration preferences] _setEnabled:YES forFeature:feature]; } auto handler = adoptNS([[TestURLSchemeHandler alloc] init]); [configuration setURLSchemeHandler:handler.get() forURLScheme:@"testing"]; [handler setStartURLSchemeTaskHandler:^(WKWebView *, id task) { auto response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:0 textEncodingName:nil]); [task didReceiveResponse:response.get()]; [task didReceiveData:[NSData dataWithBytes:mainBytes length:strlen(mainBytes)]]; [task didFinish]; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); auto uiDelegate = adoptNS([[NotificationPermissionDelegate alloc] init]); [webView setUIDelegate:uiDelegate.get()]; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"testing://main/index.html"]]]; TestWebKitAPI::Util::run(&alertReceived); alertReceived = false; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"testing://secondary/index.html"]]]; TestWebKitAPI::Util::run(&alertReceived); static bool originOperationDone = false; static RetainPtr> origins; [dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet *rawOrigins) { EXPECT_EQ([rawOrigins count], 2u); origins = rawOrigins; originOperationDone = true; }]; TestWebKitAPI::Util::run(&originOperationDone); for (WKSecurityOrigin *origin in origins.get()) { EXPECT_TRUE([origin.protocol isEqualToString:@"testing"]); EXPECT_TRUE([origin.host isEqualToString:@"main"] || [origin.host isEqualToString:@"secondary"]); } deleteAllRegistrationsForDataStore(dataStore.get()); cleanUpTestWebPushD(tempDirectory); } #endif // #if USE(APPLE_INTERNAL_SDK) #endif // ENABLE(INSTALL_COORDINATION_BUNDLES) TEST_F(WebPushDTest, TooManySilentPushesCausesUnsubscribe) { static constexpr auto htmlSource = R"HTML( )HTML"_s; static constexpr auto serviceWorkerScriptSource = "self.addEventListener('push', (event) => { });"_s; __block RetainPtr message = nil; __block bool gotMessage = false; [m_notificationMessageHandler setMessageHandler:^(id receivedMessage) { message = receivedMessage; gotMessage = true; }]; loadRequest(htmlSource, serviceWorkerScriptSource); TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqualToString:@"Subscribed"]); for (unsigned i = 0; i < WebKit::WebPushD::maxSilentPushCount; i++) { gotMessage = false; [m_webView evaluateJavaScript:@"getPushSubscription()" completionHandler:^(id, NSError*) { }]; TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqualToString:@"Subscribed"]); __block bool processedPush = false; __block bool pushResult = false; auto message = injectPushMessage(@{ }); [m_dataStore _processPushMessage:message.get() completionHandler:^(bool result) { pushResult = result; processedPush = true; }]; TestWebKitAPI::Util::run(&processedPush); // WebContent should fail processing the push since no notification was shown. EXPECT_FALSE(pushResult); } gotMessage = false; [m_webView evaluateJavaScript:@"getPushSubscription()" completionHandler:^(id, NSError*) { }]; TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqualToString:@"Unsubscribed"]); } TEST_F(WebPushDTest, GetPushSubscriptionWithMismatchedPublicToken) { static constexpr auto htmlSource = R"HTML( )HTML"_s; __block RetainPtr message = nil; __block bool gotMessage = false; [m_notificationMessageHandler setMessageHandler:^(id receivedMessage) { message = receivedMessage; gotMessage = true; }]; loadRequest(htmlSource, ""_s); TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqualToString:@"Ready"]); message = nil; gotMessage = false; [m_webView evaluateJavaScript:@"subscribe()" completionHandler:^(id, NSError*) { }]; TestWebKitAPI::Util::run(&gotMessage); RetainPtr subscription = message; ASSERT_FALSE([subscription isEqual:[NSNull null]]); ASSERT_TRUE([subscription isKindOfClass:[NSDictionary class]] && [subscription objectForKey:@"endpoint"]); message = nil; gotMessage = false; [m_webView evaluateJavaScript:@"getSubscription()" completionHandler:^(id, NSError*) { }]; TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqual:subscription.get()]); // If the public token changes, all subscriptions should be invalidated. auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service"); sendMessageToDaemonWaitingForReply(utilityConnection.get(), MessageType::SetPublicTokenForTesting, encodeString("foobar"_s)); message = nil; gotMessage = false; [m_webView evaluateJavaScript:@"getSubscription()" completionHandler:^(id, NSError*) { }]; TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqual:[NSNull null]]); } TEST_F(WebPushDMultipleLaunchTest, GetPushSubscriptionAfterDaemonRelaunch) { static constexpr auto htmlSource = R"HTML( )HTML"_s; __block RetainPtr message = nil; __block bool gotMessage = false; [m_notificationMessageHandler setMessageHandler:^(id receivedMessage) { message = receivedMessage; gotMessage = true; }]; loadRequest(htmlSource, ""_s); TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqualToString:@"Ready"]); message = nil; gotMessage = false; [m_webView evaluateJavaScript:@"getSubscription()" completionHandler:^(id, NSError*) { }]; TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqual:[NSNull null]]); ASSERT_TRUE(restartTestWebPushD()); // Make sure that getSubscription works after killing webpushd. Previously, this didn't work and // would fail with an AbortError because we didn't re-send the connection configuration after // the daemon relaunched. message = nil; gotMessage = false; [m_webView evaluateJavaScript:@"getSubscription()" completionHandler:^(id, NSError*) { }]; TestWebKitAPI::Util::run(&gotMessage); ASSERT_TRUE([message isEqual:[NSNull null]]); } } // namespace TestWebKitAPI #endif // ENABLE(NOTIFICATIONS) && ENABLE(NOTIFICATION_EVENT) && (PLATFORM(MAC) || PLATFORM(IOS))