blob: 6d92ce9786af81c784b1244419e6de62803ce820 [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 "TestURLSchemeHandler.h"
#import "TestWKWebView.h"
#import "Utilities.h"
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKUIDelegatePrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/_WKExperimentalFeature.h>
#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
#import <mach/mach_init.h>
#import <mach/task.h>
#if PLATFORM(MAC) || PLATFORM(IOS)
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
namespace TestWebKitAPI {
static RetainPtr<NSURL> testWebPushDaemonLocation()
{
return [currentExecutableDirectory() URLByAppendingPathComponent:@"webpushd" isDirectory:NO];
}
static NSDictionary<NSString *, id> *testWebPushDaemonPList(NSURL *storageLocation)
{
return @{
@"Label" : @"org.webkit.webpushtestdaemon",
@"LaunchOnlyOnce" : @YES,
@"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"
]
};
}
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()
{
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), tempDir);
return tempDir;
}
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(uint8_t 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", 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, uint8_t 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, uint8_t 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, 6, 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(), 5, encodedMessage);
TestWebKitAPI::Util::run(&done);
}
// Echo and wait for a reply
auto dictionary = adoptNS(xpc_dictionary_create(nullptr, nullptr, 0));
auto encodedString = encodeString("hello");
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 const char* mainBytes = R"WEBPUSHRESOURCE(
<script>
Notification.requestPermission().then(() => { alert("done") })
</script>
)WEBPUSHRESOURCE";
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 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);
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, "testing");
EXPECT_WK_STREQ(origin.get().host, "main");
// 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 const char* mainSWBytes = R"SWRESOURCE(
<script>
function log(msg)
{
window.webkit.messageHandlers.sw.postMessage(msg);
}
const channel = new MessageChannel();
channel.port1.onmessage = (event) => log(event.data);
navigator.serviceWorker.register('/sw.js').then((registration) => {
if (registration.active) {
registration.active.postMessage({port: channel.port2}, [channel.port2]);
return;
}
worker = registration.installing;
worker.addEventListener('statechange', function() {
if (worker.state == 'activated')
worker.postMessage({port: channel.port2}, [channel.port2]);
});
}).catch(function(error) {
log("Registration failed with: " + error);
});
</script>
)SWRESOURCE";
static const char* scriptBytes = R"SWRESOURCE(
let port;
self.addEventListener("message", (event) => {
port = event.data.port;
port.postMessage("Ready");
});
self.addEventListener("push", (event) => {
try {
if (!event.data) {
port.postMessage("Received: null data");
return;
}
const value = event.data.text();
port.postMessage("Received: " + value);
if (value != 'Sweet Potatoes')
event.waitUntil(Promise.reject('I want sweet potatoes'));
} catch (e) {
port.postMessage("Got exception " + e);
}
});
)SWRESOURCE";
static void clearWebsiteDataStore(WKWebsiteDataStore *store)
{
__block bool clearedStore = false;
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() {
clearedStore = true;
}];
TestWebKitAPI::Util::run(&clearedStore);
}
TEST(WebPushD, HandleInjectedPush)
{
[WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins];
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();
clearWebsiteDataStore([configuration websiteDataStore]);
[configuration.get().preferences _setNotificationsEnabled:YES];
for (_WKExperimentalFeature *feature in [WKPreferences _experimentalFeatures]) {
if ([feature.key isEqualToString:@"BuiltInNotificationsEnabled"])
[[configuration preferences] _setEnabled:YES forFeature:feature];
}
auto messageHandler = adoptNS([[TestMessageHandler alloc] init]);
[[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"];
__block bool done = false;
[messageHandler addMessage:@"Ready" withHandler:^{
done = true;
}];
[messageHandler addMessage:@"Received: Hello World" withHandler:^{
done = true;
}];
TestWebKitAPI::HTTPServer server({
{ "/", { mainSWBytes } },
{ "/sw.js", { { { "Content-Type", "application/javascript" } }, scriptBytes } }
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
[webView loadRequest:server.request()];
TestWebKitAPI::Util::run(&done);
done = false;
// Inject push message
auto encodedMessage = encodeString("com.apple.WebKit.TestWebKitAPI");
encodedMessage.appendVector(encodeString(server.request().URL.absoluteString));
encodedMessage.appendVector(encodeString("Hello World"));
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
sendMessageToDaemonWaitingForReply(utilityConnection.get(), 7, encodedMessage);
// Fetch push messages
__block RetainPtr<NSArray<NSDictionary *>> messages;
[dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *rawMessages) {
messages = rawMessages;
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
EXPECT_EQ([messages count], 1u);
// Handle push message
__block bool pushMessageProcessed = false;
[dataStore _processPushMessage:[messages firstObject] completionHandler:^(bool result) {
pushMessageProcessed = true;
}];
TestWebKitAPI::Util::run(&done);
TestWebKitAPI::Util::run(&pushMessageProcessed);
cleanUpTestWebPushD(tempDirectory);
}
} // namespace TestWebKitAPI
#endif // PLATFORM(MAC) || PLATFORM(IOS)