blob: 1bd3b2c1e49dc3eb48ae99e6a0d7ddc6049854bd [file] [log] [blame]
/*
* 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 <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKProcessPoolPrivate.h>
#import <WebKit/WKUIDelegatePrivate.h>
#import <WebKit/WKWebsiteDataRecordPrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/WebPushDaemonConstants.h>
#import <WebKit/_WKExperimentalFeature.h>
#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
#import <mach/mach_init.h>
#import <mach/task.h>
#import <wtf/BlockPtr.h>
#import <wtf/text/Base64.h>
#if ENABLE(NOTIFICATIONS) && ENABLE(NOTIFICATION_EVENT) && (PLATFORM(MAC) || PLATFORM(IOS))
using WebKit::WebPushD::MessageType;
static bool alertReceived = false;
@interface NotificationPermissionDelegate : NSObject<WKUIDelegatePrivate>
@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<WKScriptMessageHandler> {
BlockPtr<void(id)> _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<NSURL> testWebPushDaemonLocation()
{
return [currentExecutableDirectory() URLByAppendingPathComponent:@"webpushd" isDirectory:NO];
}
enum LaunchOnlyOnce : BOOL { No, Yes };
static NSDictionary<NSString *, id> *testWebPushDaemonPList(NSURL *storageLocation, LaunchOnlyOnce launchOnlyOnce)
{
return @{
@"Label" : @"org.webkit.webpushtestdaemon",
@"LaunchOnlyOnce" : @(static_cast<BOOL>(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<NSString *> *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<xpc_object_t> createMessageDictionary(MessageType messageType, const Vector<uint8_t>& 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<uint64_t>(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<uint8_t>& 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<uint8_t>& 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<uint8_t> encodedMessage(42);
encodedMessage.fill(0);
encodedMessage[1] = 1;
encodedMessage[2] = 32;
memcpy(&encodedMessage[10], &token, sizeof(token));
sendMessageToDaemon(connection, MessageType::UpdateConnectionConfiguration, encodedMessage);
}
}
RetainPtr<xpc_connection_t> 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<uint8_t> encodeString(const String& message)
{
ASSERT(message.is8Bit());
auto utf8 = message.utf8();
Vector<uint8_t> result(utf8.length() + 5);
result[0] = static_cast<uint8_t>(utf8.length());
result[1] = static_cast<uint8_t>(utf8.length() >> 8);
result[2] = static_cast<uint8_t>(utf8.length() >> 16);
result[3] = static_cast<uint8_t>(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<uint8_t> 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<uint8_t, 15> 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<TestWebKitAPI::TestNotificationProvider>(Vector<WKNotificationManagerRef> { [[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<NSDictionary> 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<const char *>(data.bytes), static_cast<unsigned>(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<NSArray<NSDictionary *>> messages;
[m_dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *rawMessages) {
messages = rawMessages;
gotMessages = true;
}];
TestWebKitAPI::Util::run(&gotMessages);
EXPECT_EQ([messages count], 1u);
return [messages objectAtIndex:0];
}
RetainPtr<NSURL> m_tempDirectory;
RetainPtr<WKWebsiteDataStore> m_dataStore;
RetainPtr<WKWebViewConfiguration> m_configuration;
RetainPtr<TestMessageHandler> m_testMessageHandler;
RetainPtr<NotificationScriptMessageHandler> m_notificationMessageHandler;
std::unique_ptr<TestWebKitAPI::HTTPServer> m_server;
std::unique_ptr<TestWebKitAPI::TestNotificationProvider> m_notificationProvider;
RetainPtr<WKWebView> m_webView;
RetainPtr<id<WKUIDelegatePrivate>> 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(
<script src="/constants.js"></script>
<script>
function log(msg)
{
window.webkit.messageHandlers.test.postMessage(msg);
}
const channel = new MessageChannel();
channel.port1.onmessage = (event) => log(event.data);
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
registration.active.postMessage({port: channel.port2}, [channel.port2]);
}).catch(function(error) {
log("Registration failed with: " + error);
});
</script>
)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(
<script src="/constants.js"></script>
<script>
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
result = subscription.toJSON();
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
</script>
)HTML"_s;
__block RetainPtr<id> 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(
<script src="/constants.js"></script>
<script>
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY_THAT_CAUSES_INJECTED_FAILURE
});
result = subscription.toJSON();
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
</script>
)HTML"_s;
__block RetainPtr<id> 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(
<script src="/constants.js"></script>
<script>
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
let result1 = await subscription.unsubscribe();
let result2 = await subscription.unsubscribe();
result = [result1, result2];
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
</script>
)HTML"_s;
__block RetainPtr<id> 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(
<script src="/constants.js"></script>
<script>
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
result = await registration.unregister();
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
</script>
)HTML"_s;
__block RetainPtr<id> 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(
<script src="/constants.js"></script>
<script>
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
result = "Subscribed";
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
</script>
)HTML"_s;
__block RetainPtr<id> 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(
<script src="/constants.js"></script>
<script>
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
result = "Subscribed";
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
</script>
)HTML"_s;
__block RetainPtr<id> 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<NSArray<WKWebsiteDataRecord *>> records;
[m_dataStore fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray<WKWebsiteDataRecord *> *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(
<script src="/constants.js"></script>
<script>
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
let subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
result = "Subscribed";
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
</script>
)HTML"_s;
__block RetainPtr<id> 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<WKSecurityOrigin> origin;
[dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet<WKSecurityOrigin *> *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<WKSecurityOrigin *> *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<NSSet<WKSecurityOrigin *>> originSet;
[dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet<WKSecurityOrigin *> *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<WKSecurityOrigin *> *origins) {
EXPECT_EQ([origins count], 0u);
originOperationDone = true;
}];
TestWebKitAPI::Util::run(&originOperationDone);
}
static const char* mainBytes = R"WEBPUSHRESOURCE(
<script>
Notification.requestPermission().then(() => { alert("done") })
</script>
)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<WKURLSchemeTask> 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<NSSet<WKSecurityOrigin *>> origins;
[dataStore _getOriginsWithPushAndNotificationPermissions:^(NSSet<WKSecurityOrigin *> *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(
<script src="/constants.js"></script>
<script>
let pushManager = null;
navigator.serviceWorker.register('/sw.js').then(async () => {
const registration = await navigator.serviceWorker.ready;
let result = null;
try {
pushManager = registration.pushManager;
let subscription = await pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VALID_SERVER_KEY
});
result = "Subscribed";
} catch (e) {
result = "Error: " + e;
}
window.webkit.messageHandlers.note.postMessage(result);
});
function getPushSubscription()
{
pushManager.getSubscription().then((subscription) => {
window.webkit.messageHandlers.note.postMessage(subscription ? "Subscribed" : "Unsubscribed");
});
}
</script>
)HTML"_s;
static constexpr auto serviceWorkerScriptSource = "self.addEventListener('push', (event) => { });"_s;
__block RetainPtr<id> 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(
<script src="/constants.js"></script>
<script>
let postNoteMessage = window.webkit.messageHandlers.note.postMessage.bind(window.webkit.messageHandlers.note);
let getPushManager =
navigator.serviceWorker.register('/sw.js')
.then(() => navigator.serviceWorker.ready)
.then(registration => registration.pushManager);
function subscribe()
{
getPushManager
.then(pushManager => pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VALID_SERVER_KEY }))
.then(subscription => subscription.toJSON())
.then(postNoteMessage)
.catch(e => postNoteMessage(e.toString()));
}
function getSubscription()
{
getPushManager
.then(pushManager => pushManager.getSubscription())
.then(subscription => subscription ? subscription.toJSON() : null)
.then(postNoteMessage)
.catch(e => postNoteMessage(e.toString()));
}
postNoteMessage('Ready');
</script>
)HTML"_s;
__block RetainPtr<id> 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<id> 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(
<script src="/constants.js"></script>
<script>
let postNoteMessage = window.webkit.messageHandlers.note.postMessage.bind(window.webkit.messageHandlers.note);
let getPushManager =
navigator.serviceWorker.register('/sw.js')
.then(() => navigator.serviceWorker.ready)
.then(registration => registration.pushManager);
function getSubscription()
{
getPushManager
.then(pushManager => pushManager.getSubscription())
.then(subscription => subscription ? subscription.toJSON() : null)
.then(postNoteMessage)
.catch(e => postNoteMessage(e.toString()));
}
postNoteMessage('Ready');
</script>
)HTML"_s;
__block RetainPtr<id> 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))