Diff
Modified: trunk/Source/WTF/ChangeLog (289352 => 289353)
--- trunk/Source/WTF/ChangeLog 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WTF/ChangeLog 2022-02-08 02:58:30 UTC (rev 289353)
@@ -1,3 +1,14 @@
+2022-02-07 Ben Nham <[email protected]>
+
+ Add PushService
+ https://bugs.webkit.org/show_bug.cgi?id=235857
+
+ Reviewed by Brady Eidson.
+
+ Add PushAPIEnabled to WebKitLegacy so we can enable it on in API tests via _WKExperimentalFeature.
+
+ * Scripts/Preferences/WebPreferencesExperimental.yaml:
+
2022-02-07 Elliott Williams <[email protected]>
Replace "Copy WTF Headers" with native build phases
Modified: trunk/Source/WTF/Scripts/Preferences/WebPreferencesExperimental.yaml (289352 => 289353)
--- trunk/Source/WTF/Scripts/Preferences/WebPreferencesExperimental.yaml 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WTF/Scripts/Preferences/WebPreferencesExperimental.yaml 2022-02-08 02:58:30 UTC (rev 289353)
@@ -1122,6 +1122,8 @@
defaultValue:
WebCore:
default: false
+ WebKitLegacy:
+ default: false
WebKit:
default: false
Modified: trunk/Source/WebKit/ChangeLog (289352 => 289353)
--- trunk/Source/WebKit/ChangeLog 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/ChangeLog 2022-02-08 02:58:30 UTC (rev 289353)
@@ -1,3 +1,101 @@
+2022-02-07 Ben Nham <[email protected]>
+
+ Add PushService
+ https://bugs.webkit.org/show_bug.cgi?id=235857
+
+ Reviewed by Brady Eidson.
+
+ This adds PushService to webpushd. It handles requests to create, remove, and get push
+ subscriptions. It also decrypts incoming pushes and pass them back to the UI process via the
+ existing machinery in WebPushDaemon.
+
+ PushService primarily interacts with two objects:
+
+ 1. PushServiceConnection: this creates and removes push subscriptions from the push server.
+ It also receives incoming pushes from the push server.
+ 2. PushDatabase: this persists push subscriptions to disk.
+
+ Several things are not yet implemented; for instance, we currently always grant a page the
+ permission to receive pushes. This will be fixed in future patches.
+
+ Covered by new API tests. The current tests use a MockPushServiceConnection that vends
+ subscription with fixed data, but this can be improved in the future.
+
+ * Shared/API/Cocoa/WKMain.h:
+ * Shared/API/Cocoa/WKMain.mm:
+ (WKWebPushDaemonMain):
+ * Shared/Cocoa/WebPushMessageCocoa.mm: add ability to distinguish between an empty vs. null payload.
+ (WebKit::WebPushMessage::fromDictionary):
+ (WebKit::WebPushMessage::toDictionary const):
+ * Shared/WebPushDaemonConstants.h:
+ (WebKit::WebPushD::messageTypeSendsReply):
+ * Shared/WebPushMessage.h:
+ (WebKit::WebPushMessage::decode):
+ * WebKit.xcodeproj/project.pbxproj:
+ * webpushd/MockPushServiceConnection.h:
+ * webpushd/MockPushServiceConnection.mm:
+ (WebPushD::MockPushServiceConnection::generateClientKeys):
+ (WebPushD::MockPushServiceConnection::subscribe):
+ (WebPushD::MockPushServiceConnection::unsubscribe):
+ * webpushd/PushService.h: Added.
+ (WebPushD::PushService::connection):
+ (WebPushD::PushService::database):
+ (WebPushD::PushService::didReceivePushMessage):
+ * webpushd/PushService.mm: Added.
+ (WebPushD::updateTopicLists):
+ (WebPushD::PushService::create):
+ (WebPushD::PushService::createMockService):
+ (WebPushD::PushService::PushService):
+ (WebPushD::makePushSubscriptionFromRecord):
+ (WebPushD::makePushTopic):
+ (WebPushD::PushServiceRequest::bundleIdentifier):
+ (WebPushD::PushServiceRequest::scope):
+ (WebPushD::PushServiceRequest::PushServiceRequest):
+ (WebPushD::PushServiceRequestImpl::PushServiceRequestImpl):
+ (WebPushD::PushServiceRequestImpl::fulfill):
+ (WebPushD::PushServiceRequestImpl::reject):
+ (WebPushD::GetSubscriptionRequest::GetSubscriptionRequest):
+ (WebPushD::GetSubscriptionRequest::startInternal):
+ (WebPushD::SubscribeRequest::SubscribeRequest):
+ (WebPushD::SubscribeRequest::startImpl):
+ (WebPushD::SubscribeRequest::attemptToRecoverFromTopicAlreadyInFilterError):
+ (WebPushD::UnsubscribeRequest::UnsubscribeRequest):
+ (WebPushD::UnsubscribeRequest::startInternal):
+ (WebPushD::PushService::enqueuePushServiceRequest):
+ (WebPushD::PushService::finishedPushServiceRequest):
+ (WebPushD::PushService::getSubscription):
+ (WebPushD::PushService::didCompleteGetSubscriptionRequest):
+ (WebPushD::PushService::subscribe):
+ (WebPushD::PushService::didCompleteSubscribeRequest):
+ (WebPushD::PushService::unsubscribe):
+ (WebPushD::PushService::didCompleteUnsubscribeRequest):
+ (WebPushD::makeRawPushMessage):
+ (WebPushD::PushService::didReceivePushMessage):
+ * webpushd/PushServiceConnection.h:
+ * webpushd/PushServiceConnection.mm:
+ (WebPushD::PushServiceConnection::generateClientKeys):
+ * webpushd/WebPushDaemon.h:
+ * webpushd/WebPushDaemon.mm:
+ (WebPushD::MessageInfo::injectEncryptedPushMessageForTesting::encodeReply):
+ (WebPushD::Daemon::startMockPushService):
+ (WebPushD::Daemon::startPushService):
+ (WebPushD::Daemon::setPushService):
+ (WebPushD::Daemon::runAfterStartingPushService):
+ (WebPushD::Daemon::decodeAndHandleMessage):
+ (WebPushD::Daemon::injectEncryptedPushMessageForTesting):
+ (WebPushD::Daemon::handleIncomingPush):
+ (WebPushD::Daemon::getPendingPushMessages):
+ (WebPushD::Daemon::subscribeToPushService):
+ (WebPushD::Daemon::unsubscribeFromPushService):
+ (WebPushD::Daemon::getPushSubscription):
+ (WebPushD::Daemon::getPushPermissionState):
+ * webpushd/WebPushDaemonMain.h:
+ * webpushd/WebPushDaemonMain.mm:
+ (WebKit::WebPushDaemonMain):
+ * webpushd/com.apple.webkit.webpushd.mac.plist:
+ * webpushd/webpushd.cpp:
+ (main):
+
2022-02-07 Chris Dumez <[email protected]>
Add support for sharing Shared Workers (including across WebProcesses)
Modified: trunk/Source/WebKit/Shared/API/Cocoa/WKMain.h (289352 => 289353)
--- trunk/Source/WebKit/Shared/API/Cocoa/WKMain.h 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/Shared/API/Cocoa/WKMain.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -34,7 +34,7 @@
WK_EXPORT int WKXPCServiceMain(int argc, const char** argv) WK_API_AVAILABLE(macos(10.15), ios(13.0));
WK_EXPORT int WKAdAttributionDaemonMain(int argc, const char** argv) WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
-WK_EXPORT int WKWebPushDaemonMain(int argc, const char** argv) WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+WK_EXPORT int WKWebPushDaemonMain(int argc, char** argv) WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
#ifdef __cplusplus
}
Modified: trunk/Source/WebKit/Shared/API/Cocoa/WKMain.mm (289352 => 289353)
--- trunk/Source/WebKit/Shared/API/Cocoa/WKMain.mm 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/Shared/API/Cocoa/WKMain.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -40,7 +40,7 @@
return WebKit::PCMDaemonMain(argc, argv);
}
-int WKWebPushDaemonMain(int argc, const char** argv)
+int WKWebPushDaemonMain(int argc, char** argv)
{
return WebKit::WebPushDaemonMain(argc, argv);
}
Modified: trunk/Source/WebKit/Shared/Cocoa/WebPushMessageCocoa.mm (289352 => 289353)
--- trunk/Source/WebKit/Shared/Cocoa/WebPushMessageCocoa.mm 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/Shared/Cocoa/WebPushMessageCocoa.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -39,21 +39,30 @@
if (!url || ![url isKindOfClass:[NSURL class]])
return std::nullopt;
- NSData *pushData = [dictionary objectForKey:WebKitPushDataKey];
- if (!pushData || ![pushData isKindOfClass:[NSData class]])
+ id pushData = [dictionary objectForKey:WebKitPushDataKey];
+ BOOL isNull = [pushData isEqual:[NSNull null]];
+ BOOL isData = [pushData isKindOfClass:[NSData class]];
+
+ if (!isNull && !isData)
return std::nullopt;
- return { {
- Vector<uint8_t> { static_cast<const uint8_t*>(pushData.bytes), pushData.length },
- URL { url }
- } };
+ WebPushMessage message { { }, URL { url } };
+ if (isData) {
+ NSData *data = "" *)pushData;
+ message.pushData = Vector<uint8_t> { static_cast<const uint8_t*>(data.bytes), data.length };
+ }
+
+ return message;
}
NSDictionary *WebPushMessage::toDictionary() const
{
- auto nsData = adoptNS([[NSData alloc] initWithBytes:pushData.data() length:pushData.size()]);
+ RetainPtr<NSData> nsData;
+ if (pushData)
+ nsData = nsData = adoptNS([[NSData alloc] initWithBytes:pushData->data() length:pushData->size()]);
+
return @{
- WebKitPushDataKey : nsData.get(),
+ WebKitPushDataKey : nsData ? nsData.get() : [NSNull null],
WebKitPushRegistrationURLKey : (NSURL *)registrationURL
};
}
Modified: trunk/Source/WebKit/Shared/WebPushDaemonConstants.h (289352 => 289353)
--- trunk/Source/WebKit/Shared/WebPushDaemonConstants.h 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/Shared/WebPushDaemonConstants.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -43,6 +43,7 @@
SetDebugModeIsEnabled,
UpdateConnectionConfiguration,
InjectPushMessageForTesting,
+ InjectEncryptedPushMessageForTesting,
GetPendingPushMessages,
SubscribeToPushService,
UnsubscribeFromPushService,
@@ -59,6 +60,7 @@
case MessageType::RequestSystemNotificationPermission:
case MessageType::GetPendingPushMessages:
case MessageType::InjectPushMessageForTesting:
+ case MessageType::InjectEncryptedPushMessageForTesting:
case MessageType::SubscribeToPushService:
case MessageType::UnsubscribeFromPushService:
case MessageType::GetPushSubscription:
Modified: trunk/Source/WebKit/Shared/WebPushMessage.h (289352 => 289353)
--- trunk/Source/WebKit/Shared/WebPushMessage.h 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/Shared/WebPushMessage.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -37,7 +37,7 @@
template<class Encoder> void encode(Encoder&) const;
template<class Decoder> static std::optional<WebPushMessage> decode(Decoder&);
- Vector<uint8_t> pushData;
+ std::optional<Vector<uint8_t>> pushData;
URL registrationURL;
#if PLATFORM(COCOA)
@@ -55,7 +55,7 @@
template<class Decoder>
std::optional<WebPushMessage> WebPushMessage::decode(Decoder& decoder)
{
- std::optional<Vector<uint8_t>> pushData;
+ std::optional<std::optional<Vector<uint8_t>>> pushData;
decoder >> pushData;
if (!pushData)
return std::nullopt;
Modified: trunk/Source/WebKit/WebKit.xcodeproj/project.pbxproj (289352 => 289353)
--- trunk/Source/WebKit/WebKit.xcodeproj/project.pbxproj 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/WebKit.xcodeproj/project.pbxproj 2022-02-08 02:58:30 UTC (rev 289353)
@@ -951,7 +951,7 @@
512935E41288D97800A4B695 /* InjectedBundlePageContextMenuClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 512935E21288D97800A4B695 /* InjectedBundlePageContextMenuClient.h */; };
5129EB1223D0DE7B00AF1CD7 /* ContentWorldShared.h in Headers */ = {isa = PBXBuildFile; fileRef = 5129EB1123D0DE7800AF1CD7 /* ContentWorldShared.h */; };
512CD6982721EFC800F7F8EC /* WebPushDaemonConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 512CD6972721EFC300F7F8EC /* WebPushDaemonConnection.h */; };
- 512CD69A2721F0A900F7F8EC /* WebPushDaemonConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 512CD6992721F04900F7F8EC /* WebPushDaemonConstants.h */; };
+ 512CD69A2721F0A900F7F8EC /* WebPushDaemonConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 512CD6992721F04900F7F8EC /* WebPushDaemonConstants.h */; settings = {ATTRIBUTES = (Private, ); }; };
512E34E5130B4D0500ABD19A /* WKApplicationCacheManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 517A33B4130B308C00F80CB5 /* WKApplicationCacheManager.h */; settings = {ATTRIBUTES = (Private, ); }; };
512ECC3527196ADB00089B66 /* PrivateClickMeasurementConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CB930FE26E802160032B1C0 /* PrivateClickMeasurementConnection.h */; };
512F589712A8838800629530 /* AuthenticationChallengeProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 512F588F12A8838800629530 /* AuthenticationChallengeProxy.h */; };
@@ -2003,6 +2003,8 @@
E5CBA76727A318E100DF7858 /* UnifiedSource119.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5CBA76027A3187900DF7858 /* UnifiedSource119.cpp */; };
E5CBA76827A318E100DF7858 /* UnifiedSource117.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5CBA76227A3187900DF7858 /* UnifiedSource117.cpp */; };
E5DEFA6826F8F42600AB68DB /* PhotosUISPI.h in Headers */ = {isa = PBXBuildFile; fileRef = E5DEFA6726F8F42600AB68DB /* PhotosUISPI.h */; };
+ EB36B16827A7B4500050E00D /* PushService.h in Headers */ = {isa = PBXBuildFile; fileRef = EB36B16627A7B4500050E00D /* PushService.h */; };
+ EB36B16927A7B4500050E00D /* PushService.mm in Sources */ = {isa = PBXBuildFile; fileRef = EB36B16727A7B4500050E00D /* PushService.mm */; };
EBA8D3AB27A5E31300CB7900 /* ApplePushServiceSPI.h in Headers */ = {isa = PBXBuildFile; fileRef = EBA8D3AA27A5E31300CB7900 /* ApplePushServiceSPI.h */; };
EBA8D3B227A5E33F00CB7900 /* ApplePushServiceConnection.mm in Sources */ = {isa = PBXBuildFile; fileRef = EBA8D3AC27A5E33E00CB7900 /* ApplePushServiceConnection.mm */; };
EBA8D3B327A5E33F00CB7900 /* MockPushServiceConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = EBA8D3AD27A5E33E00CB7900 /* MockPushServiceConnection.h */; };
@@ -6478,6 +6480,8 @@
E5DEFA6726F8F42600AB68DB /* PhotosUISPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhotosUISPI.h; sourceTree = "<group>"; };
EB0D312D275AE13300863D8F /* com.apple.webkit.webpushd.mac.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.apple.webkit.webpushd.mac.plist; sourceTree = "<group>"; };
EB0D312E275AE13300863D8F /* com.apple.webkit.webpushd.ios.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.apple.webkit.webpushd.ios.plist; sourceTree = "<group>"; };
+ EB36B16627A7B4500050E00D /* PushService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PushService.h; sourceTree = "<group>"; };
+ EB36B16727A7B4500050E00D /* PushService.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PushService.mm; sourceTree = "<group>"; };
EBA8D3AA27A5E31300CB7900 /* ApplePushServiceSPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApplePushServiceSPI.h; sourceTree = "<group>"; };
EBA8D3AC27A5E33E00CB7900 /* ApplePushServiceConnection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ApplePushServiceConnection.mm; sourceTree = "<group>"; };
EBA8D3AD27A5E33E00CB7900 /* MockPushServiceConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MockPushServiceConnection.h; sourceTree = "<group>"; };
@@ -10139,6 +10143,8 @@
5160E958274C0D8800567388 /* PushAppBundle.mm */,
51F7BB792744C50700C45A72 /* PushClientConnection.h */,
51F7BB7A2744C50700C45A72 /* PushClientConnection.mm */,
+ EB36B16627A7B4500050E00D /* PushService.h */,
+ EB36B16727A7B4500050E00D /* PushService.mm */,
EBA8D3AF27A5E33F00CB7900 /* PushServiceConnection.h */,
EBA8D3B127A5E33F00CB7900 /* PushServiceConnection.mm */,
517B5F77275E9795002DC22D /* webpushd.cpp */,
@@ -13291,6 +13297,7 @@
517B5F81275E97B6002DC22D /* PushAppBundle.h in Headers */,
517B5F7D275E97B6002DC22D /* PushClientConnection.h in Headers */,
517B5F95275EBA63002DC22D /* PushMessageForTesting.h in Headers */,
+ EB36B16827A7B4500050E00D /* PushService.h in Headers */,
EBA8D3B527A5E33F00CB7900 /* PushServiceConnection.h in Headers */,
A1E688701F6E2BAB007006A6 /* QuarantineSPI.h in Headers */,
1A0C227E2451130A00ED614D /* QuickLookThumbnailingSoftLink.h in Headers */,
@@ -15555,6 +15562,7 @@
2D54C31B212F4DA60049C174 /* ProcessLauncher.cpp in Sources */,
517B5F82275E97B6002DC22D /* PushAppBundle.mm in Sources */,
517B5F7B275E97A9002DC22D /* PushClientConnection.mm in Sources */,
+ EB36B16927A7B4500050E00D /* PushService.mm in Sources */,
EBA8D3B727A5E33F00CB7900 /* PushServiceConnection.mm in Sources */,
1A0C227F2451130A00ED614D /* QuickLookThumbnailingSoftLink.mm in Sources */,
1A0C225E243575CD00ED614D /* QuickLookThumbnailLoader.mm in Sources */,
Modified: trunk/Source/WebKit/webpushd/MockPushServiceConnection.h (289352 => 289353)
--- trunk/Source/WebKit/webpushd/MockPushServiceConnection.h 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/MockPushServiceConnection.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -34,6 +34,8 @@
MockPushServiceConnection();
~MockPushServiceConnection();
+ WebCore::PushCrypto::ClientKeys generateClientKeys() final;
+
void subscribe(const String& topic, const Vector<uint8_t>& vapidPublicKey, SubscribeHandler&&) override;
void unsubscribe(const String& topic, const Vector<uint8_t>& vapidPublicKey, UnsubscribeHandler&&) override;
Modified: trunk/Source/WebKit/webpushd/MockPushServiceConnection.mm (289352 => 289353)
--- trunk/Source/WebKit/webpushd/MockPushServiceConnection.mm 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/MockPushServiceConnection.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -26,6 +26,10 @@
#import "config.h"
#import "MockPushServiceConnection.h"
+#import <wtf/text/Base64.h>
+
+using namespace WebCore::PushCrypto;
+
namespace WebPushD {
MockPushServiceConnection::MockPushServiceConnection()
@@ -34,14 +38,30 @@
MockPushServiceConnection::~MockPushServiceConnection() = default;
-void MockPushServiceConnection::subscribe(const String&, const Vector<uint8_t>&, SubscribeHandler&& handler)
+ClientKeys MockPushServiceConnection::generateClientKeys()
{
- handler({ }, [NSError errorWithDomain:NSCocoaErrorDomain code:NSFeatureUnsupportedError userInfo:nil]);
+ // Example values from RFC8291 Section 5.
+ auto publicKey = base64URLDecode("BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4"_s).value();
+ auto privateKey = base64URLDecode("q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94"_s).value();
+ auto secret = base64URLDecode("BTBZMqHH6r4Tts7J_aSIgg"_s).value();
+
+ return ClientKeys { P256DHKeyPair { WTFMove(publicKey), WTFMove(privateKey) }, WTFMove(secret) };
}
+void MockPushServiceConnection::subscribe(const String&, const Vector<uint8_t>& vapidPublicKey, SubscribeHandler&& handler)
+{
+ auto alwaysRejectedKey = base64URLDecode("BEAxaUMo1s8tjORxJfnSSvWhYb4u51kg1hWT2s_9gpV7Zxar1pF_2BQ8AncuAdS2BoLhN4qaxzBy2CwHE8BBzWg"_s).value();
+ if (vapidPublicKey == alwaysRejectedKey) {
+ handler({ }, [NSError errorWithDomain:@"WebPush" code:-1 userInfo:nil]);
+ return;
+ }
+
+ handler(@"https://webkit.org/push", nil);
+}
+
void MockPushServiceConnection::unsubscribe(const String&, const Vector<uint8_t>&, UnsubscribeHandler&& handler)
{
- handler({ }, [NSError errorWithDomain:NSCocoaErrorDomain code:NSFeatureUnsupportedError userInfo:nil]);
+ handler(true, nil);
}
Vector<String> MockPushServiceConnection::enabledTopics()
Added: trunk/Source/WebKit/webpushd/PushService.h (0 => 289353)
--- trunk/Source/WebKit/webpushd/PushService.h (rev 0)
+++ trunk/Source/WebKit/webpushd/PushService.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "PushServiceConnection.h"
+#include "WebPushMessage.h"
+#include <WebCore/ExceptionData.h>
+#include <WebCore/PushDatabase.h>
+#include <WebCore/PushSubscriptionData.h>
+#include <wtf/CompletionHandler.h>
+#include <wtf/Deque.h>
+#include <wtf/Expected.h>
+#include <wtf/Function.h>
+#include <wtf/UniqueRef.h>
+#include <wtf/Vector.h>
+#include <wtf/text/WTFString.h>
+
+namespace WebPushD {
+
+class GetSubscriptionRequest;
+class PushServiceRequest;
+class SubscribeRequest;
+class UnsubscribeRequest;
+
+class PushService {
+ WTF_MAKE_FAST_ALLOCATED;
+public:
+ using IncomingPushMessageHandler = Function<void(const String& bundleIdentifier, WebKit::WebPushMessage&&)>;
+
+ static void create(const String& incomingPushServiceName, const String& databasePath, IncomingPushMessageHandler&&, CompletionHandler<void(std::unique_ptr<PushService>&&)>&&);
+ static void createMockService(IncomingPushMessageHandler&&, CompletionHandler<void(std::unique_ptr<PushService>&&)>&&);
+ ~PushService();
+
+ PushServiceConnection& connection() { return m_connection; }
+ WebCore::PushDatabase& database() { return m_database; }
+
+ void getSubscription(const String& bundleIdentifier, const String& scope, CompletionHandler<void(const Expected<std::optional<WebCore::PushSubscriptionData>, WebCore::ExceptionData>&)>&&);
+ void subscribe(const String& bundleIdentifier, const String& scope, const Vector<uint8_t>& vapidPublicKey, CompletionHandler<void(const Expected<WebCore::PushSubscriptionData, WebCore::ExceptionData>&)>&&);
+ void unsubscribe(const String& bundleIdentifier, const String& scope, WebCore::PushSubscriptionIdentifier, CompletionHandler<void(const Expected<bool, WebCore::ExceptionData>&)>&&);
+
+ void didCompleteGetSubscriptionRequest(GetSubscriptionRequest&);
+ void didCompleteSubscribeRequest(SubscribeRequest&);
+ void didCompleteUnsubscribeRequest(UnsubscribeRequest&);
+
+ void didReceivePushMessage(NSString *topic, NSDictionary *userInfo, CompletionHandler<void()>&& = [] { });
+
+private:
+ PushService(UniqueRef<PushServiceConnection>&&, UniqueRef<WebCore::PushDatabase>&&, IncomingPushMessageHandler&&);
+
+ using PushServiceRequestMap = HashMap<std::tuple<String, String>, Deque<std::unique_ptr<PushServiceRequest>>>;
+ void enqueuePushServiceRequest(PushServiceRequestMap&, std::unique_ptr<PushServiceRequest>&&);
+ void finishedPushServiceRequest(PushServiceRequestMap&, PushServiceRequest&);
+
+ UniqueRef<PushServiceConnection> m_connection;
+ UniqueRef<WebCore::PushDatabase> m_database;
+
+ IncomingPushMessageHandler m_incomingPushMessageHandler;
+
+ PushServiceRequestMap m_getSubscriptionRequests;
+ PushServiceRequestMap m_subscribeRequests;
+ PushServiceRequestMap m_unsubscribeRequests;
+};
+
+} // namespace WebPushD
Added: trunk/Source/WebKit/webpushd/PushService.mm (0 => 289353)
--- trunk/Source/WebKit/webpushd/PushService.mm (rev 0)
+++ trunk/Source/WebKit/webpushd/PushService.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -0,0 +1,598 @@
+/*
+ * 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 "PushService.h"
+
+#import "ApplePushServiceConnection.h"
+#import "MockPushServiceConnection.h"
+#import "Logging.h"
+#import <Foundation/Foundation.h>
+#import <WebCore/PushMessageCrypto.h>
+#import <WebCore/SecurityOrigin.h>
+#import <wtf/OSObjectPtr.h>
+#import <wtf/WorkQueue.h>
+#import <wtf/spi/darwin/XPCSPI.h>
+#import <wtf/text/Base64.h>
+
+using namespace WebKit;
+using namespace WebCore;
+
+namespace WebPushD {
+
+static void updateTopicLists(PushServiceConnection& connection, PushDatabase& database, CompletionHandler<void()> completionHandler)
+{
+ database.getTopicsByWakeState([&connection, completionHandler = WTFMove(completionHandler)](auto&& topicMap) mutable {
+ // FIXME: move topics to ignored list based on user preferences.
+ PushServiceConnection::TopicLists topicLists;
+
+ if (auto it = topicMap.find(PushWakeState::Waking); it != topicMap.end())
+ std::swap(topicLists.enabledTopics, it->value);
+ if (auto it = topicMap.find(PushWakeState::Opportunistic); it != topicMap.end())
+ std::swap(topicLists.opportunisticTopics, it->value);
+ if (auto it = topicMap.find(PushWakeState::NonWaking); it != topicMap.end())
+ std::swap(topicLists.nonWakingTopics, it->value);
+
+ connection.setTopicLists(WTFMove(topicLists));
+
+ completionHandler();
+ });
+}
+
+#if HAVE(APPLE_PUSH_SERVICE_URL_TOKEN_SUPPORT)
+
+void PushService::create(const String& incomingPushServiceName, const String& databasePath, IncomingPushMessageHandler&& messageHandler, CompletionHandler<void(std::unique_ptr<PushService>&&)>&& creationHandler)
+{
+ auto transaction = adoptOSObject(os_transaction_create("com.apple.webkit.webpushd.push-service-init"));
+
+ // Create the connection ASAP so that we bootstrap_check_in to the service in a timely manner.
+ auto connection = makeUniqueRef<ApplePushServiceConnection>(incomingPushServiceName);
+
+ PushDatabase::create(databasePath, [transaction = WTFMove(transaction), connection = WTFMove(connection), messageHandler = WTFMove(messageHandler), creationHandler = WTFMove(creationHandler)](auto&& databaseResult) mutable {
+ if (!databaseResult) {
+ RELEASE_LOG(Push, "Push service initialization failed with database error");
+ creationHandler(std::unique_ptr<PushService>());
+ transaction = nullptr;
+ return;
+ }
+
+ auto database = makeUniqueRefFromNonNullUniquePtr(WTFMove(databaseResult));
+ UniqueRef<PushService> service(*new PushService(WTFMove(connection), WTFMove(database), WTFMove(messageHandler)));
+
+ auto& connectionRef = service->connection();
+ auto& databaseRef = service->database();
+
+ // Only provide the service object back to the caller after we've synced the topic lists in
+ // the database with the PushServiceConnection/APSConnection. This ensures that we won't
+ // service any calls to subscribe/unsubscribe/etc. until after the topic lists are up to
+ // date, which APSConnection cares about.
+ updateTopicLists(connectionRef, databaseRef, [transaction = WTFMove(transaction), service = WTFMove(service), creationHandler = WTFMove(creationHandler)]() mutable {
+ creationHandler(service.moveToUniquePtr());
+ transaction = nullptr;
+ });
+ });
+}
+
+#else
+
+void PushService::create(const String&, const String&, IncomingPushMessageHandler&&, CompletionHandler<void(std::unique_ptr<PushService>&&)>&& creationHandler)
+{
+ creationHandler(std::unique_ptr<PushService>());
+}
+
+#endif // HAVE(APPLE_PUSH_SERVICE_URL_TOKEN_SUPPORT)
+
+void PushService::createMockService(IncomingPushMessageHandler&& messageHandler, CompletionHandler<void(std::unique_ptr<PushService>&&)>&& creationHandler)
+{
+ PushDatabase::create(SQLiteDatabase::inMemoryPath(), [messageHandler = WTFMove(messageHandler), creationHandler = WTFMove(creationHandler)](auto&& databaseResult) mutable {
+ if (!databaseResult) {
+ creationHandler(std::unique_ptr<PushService>());
+ return;
+ }
+
+ auto connection = makeUniqueRef<MockPushServiceConnection>();
+ auto database = makeUniqueRefFromNonNullUniquePtr(WTFMove(databaseResult));
+ creationHandler(std::unique_ptr<PushService>(new PushService(WTFMove(connection), WTFMove(database), WTFMove(messageHandler))));
+ });
+}
+
+PushService::PushService(UniqueRef<PushServiceConnection>&& pushServiceConnection, UniqueRef<PushDatabase>&& pushDatabase, IncomingPushMessageHandler&& incomingPushMessageHandler)
+ : m_connection(WTFMove(pushServiceConnection))
+ , m_database(WTFMove(pushDatabase))
+ , m_incomingPushMessageHandler(WTFMove(incomingPushMessageHandler))
+{
+ RELEASE_ASSERT(m_incomingPushMessageHandler);
+
+ m_connection->startListeningForPushMessages([this](NSString *topic, NSDictionary *userInfo) {
+ didReceivePushMessage(topic, userInfo);
+ });
+}
+
+PushService::~PushService() = default;
+
+static PushSubscriptionData makePushSubscriptionFromRecord(PushRecord&& record)
+{
+ return PushSubscriptionData {
+ record.identifier,
+ WTFMove(record.endpoint),
+ WTFMove(record.expirationTime),
+ WTFMove(record.serverVAPIDPublicKey),
+ WTFMove(record.clientPublicKey),
+ WTFMove(record.sharedAuthSecret)
+ };
+}
+
+static String makePushTopic(const String& bundleIdentifier, const String& scope)
+{
+ return bundleIdentifier + " " + scope;
+}
+
+class PushServiceRequest {
+ WTF_MAKE_FAST_ALLOCATED;
+public:
+ virtual ~PushServiceRequest() = default;
+
+ virtual ASCIILiteral description() const = 0;
+
+ const String& bundleIdentifier() { return m_bundleIdentifier; }
+ const String& scope() { return m_scope; };
+
+ virtual void start() = 0;
+
+protected:
+ PushServiceRequest(PushService& service, const String& bundleIdentifier, const String& scope)
+ : m_service(service)
+ , m_connection(service.connection())
+ , m_database(service.database())
+ , m_bundleIdentifier(bundleIdentifier)
+ , m_scope(scope)
+ {
+ }
+
+ virtual void finish() = 0;
+
+ PushService& m_service;
+ PushServiceConnection& m_connection;
+ PushDatabase& m_database;
+ String m_bundleIdentifier;
+ String m_scope;
+ CompletionHandler<void(PushServiceRequest&)> m_completionHandler;
+};
+
+template<typename ResultType>
+class PushServiceRequestImpl : public PushServiceRequest {
+public:
+ void start() final
+ {
+ if (m_bundleIdentifier.isEmpty() || m_scope.isEmpty()) {
+ reject(WebCore::ExceptionData { WebCore::AbortError, "Invalid sender"_s });
+ return;
+ }
+
+ String transactionDescription = String("com.apple.webkit.webpushd:"_s) + description() + ":"_s + m_bundleIdentifier + ":"_s + m_scope;
+ m_transaction = adoptOSObject(os_transaction_create(transactionDescription.utf8().data()));
+ startInternal();
+ }
+
+protected:
+ using ResultHandler = CompletionHandler<void(const Expected<ResultType, WebCore::ExceptionData>&)>;
+
+ PushServiceRequestImpl(PushService& service, const String& bundleIdentifier, const String& scope, ResultHandler&& resultHandler)
+ : PushServiceRequest(service, bundleIdentifier, scope)
+ , m_resultHandler(WTFMove(resultHandler))
+ {
+ }
+ virtual ~PushServiceRequestImpl() = default;
+
+ virtual void startInternal() = 0;
+
+ void fulfill(ResultType result)
+ {
+ m_resultHandler(WTFMove(result));
+ finish();
+ }
+
+ void reject(WebCore::ExceptionData&& data)
+ {
+ m_resultHandler(makeUnexpected(WTFMove(data)));
+ finish();
+ }
+
+private:
+ ResultHandler m_resultHandler;
+ OSObjectPtr<os_transaction_t> m_transaction;
+};
+
+class GetSubscriptionRequest : public PushServiceRequestImpl<std::optional<WebCore::PushSubscriptionData>> {
+public:
+ GetSubscriptionRequest(PushService&, const String& bundleIdentifier, const String& scope, ResultHandler&&);
+ virtual ~GetSubscriptionRequest() = default;
+
+protected:
+ ASCIILiteral description() const final { return "GetSubscriptionRequest"_s; }
+ void startInternal() final;
+ void finish() final { m_service.didCompleteGetSubscriptionRequest(*this); }
+};
+
+GetSubscriptionRequest::GetSubscriptionRequest(PushService& service, const String& bundleIdentifier, const String& scope, ResultHandler&& resultHandler)
+ : PushServiceRequestImpl(service, bundleIdentifier, scope, WTFMove(resultHandler))
+{
+}
+
+// Implements the webpushd side of PushManager.getSubscription.
+void GetSubscriptionRequest::startInternal()
+{
+ m_database.getRecordByBundleIdentifierAndScope(m_bundleIdentifier, m_scope, [this](auto&& result) mutable {
+ if (!result) {
+ fulfill(std::optional<WebCore::PushSubscriptionData> { });
+ return;
+ }
+
+ fulfill(makePushSubscriptionFromRecord(WTFMove(*result)));
+ });
+}
+
+class SubscribeRequest : public PushServiceRequestImpl<WebCore::PushSubscriptionData> {
+public:
+ SubscribeRequest(PushService&, const String& bundleIdentifier, const String& scope, const Vector<uint8_t>& vapidPublicKey, ResultHandler&&);
+ virtual ~SubscribeRequest() = default;
+
+protected:
+ ASCIILiteral description() const final { return "SubscribeRequest"_s; }
+ void startInternal() final { startImpl(IsRetry::No); }
+ void finish() final { m_service.didCompleteSubscribeRequest(*this); }
+
+private:
+ enum class IsRetry { No, Yes };
+ void startImpl(IsRetry);
+ void attemptToRecoverFromTopicAlreadyInFilterError(String&&);
+ Vector<uint8_t> m_vapidPublicKey;
+};
+
+SubscribeRequest::SubscribeRequest(PushService& service, const String& bundleIdentifier, const String& scope, const Vector<uint8_t>& vapidPublicKey, ResultHandler&& resultHandler)
+ : PushServiceRequestImpl(service, bundleIdentifier, scope, WTFMove(resultHandler))
+ , m_vapidPublicKey(vapidPublicKey)
+{
+}
+
+// Implements the webpushd side of PushManager.subscribe().
+void SubscribeRequest::startImpl(IsRetry isRetry)
+{
+ m_database.getRecordByBundleIdentifierAndScope(m_bundleIdentifier, m_scope, [this, isRetry](auto&& result) mutable {
+ if (result) {
+ if (m_vapidPublicKey != result->serverVAPIDPublicKey)
+ reject(WebCore::ExceptionData { WebCore::InvalidStateError, "Provided applicationServerKey does not match the key in the existing subscription."_s });
+ else
+ fulfill(makePushSubscriptionFromRecord(WTFMove(*result)));
+ return;
+ }
+
+ auto topic = makePushTopic(m_bundleIdentifier, m_scope);
+ m_connection.subscribe(topic, m_vapidPublicKey, [this, isRetry, topic](NSString *endpoint, NSError *error) mutable {
+ if (error) {
+#if !HAVE(APPLE_PUSH_SERVICE_URL_TOKEN_SUPPORT)
+ UNUSED_PARAM(isRetry);
+#else
+ // FIXME: use pointer comparison once APSURLTokenErrorDomain is externed.
+ if (isRetry == IsRetry::No && [error.domain isEqualToString:@"APSURLTokenErrorDomain"] && error.code == APSURLTokenErrorCodeTopicAlreadyInFilter) {
+ attemptToRecoverFromTopicAlreadyInFilterError(WTFMove(topic));
+ return;
+ }
+#endif
+
+ RELEASE_LOG(Push, "PushManager.subscribe(bundleID: %{public}s, scope: %{sensitive}s) failed with domain: %{public}s code: %lld)", m_bundleIdentifier.utf8().data(), m_scope.utf8().data(), error.domain.UTF8String, static_cast<int64_t>(error.code));
+ reject(WebCore::ExceptionData { WebCore::AbortError, "Failed due to internal service error"_s });
+ return;
+ }
+
+ auto clientKeys = m_service.connection().generateClientKeys();
+ PushRecord record;
+ record.bundleID = m_bundleIdentifier;
+ record.securityOrigin = SecurityOrigin::createFromString(m_scope)->data().toString();
+ record.scope = m_scope;
+ record.endpoint = endpoint;
+ record.topic = WTFMove(topic);
+ record.serverVAPIDPublicKey = m_vapidPublicKey;
+ record.clientPublicKey = WTFMove(clientKeys.clientP256DHKeyPair.publicKey);
+ record.clientPrivateKey = WTFMove(clientKeys.clientP256DHKeyPair.privateKey);
+ record.sharedAuthSecret = WTFMove(clientKeys.sharedAuthSecret);
+
+ m_database.insertRecord(record, [this](auto&& result) mutable {
+ if (!result) {
+ RELEASE_LOG(Push, "PushManager.subscribe(bundleID: %{public}s, scope: %{sensitive}s) failed with database error", m_bundleIdentifier.utf8().data(), m_scope.utf8().data());
+ reject(WebCore::ExceptionData { WebCore::AbortError, "Failed due to internal database error"_s });
+ return;
+ }
+
+ // FIXME: support partial topic list updates.
+ updateTopicLists(m_connection, m_database, [this, record = WTFMove(*result)]() mutable {
+ fulfill(makePushSubscriptionFromRecord(WTFMove(record)));
+ });
+ });
+ });
+ });
+}
+
+// Tries to recover from a topic being moved to the global paused filter (rdar://88139330).
+void SubscribeRequest::attemptToRecoverFromTopicAlreadyInFilterError(String&& topic)
+{
+#if !HAVE(APPLE_PUSH_SERVICE_URL_TOKEN_SUPPORT)
+ UNUSED_PARAM(topic);
+#else
+ WorkQueue::main().dispatch([this, topic = WTFMove(topic)]() mutable {
+ // This takes ownership of the paused topic and tells apsd to forget about the topic.
+ auto originalTopics = m_connection.ignoredTopics();
+ auto augmentedTopics = originalTopics;
+ augmentedTopics.append(topic);
+ m_connection.setIgnoredTopics(WTFMove(augmentedTopics));
+ m_connection.setIgnoredTopics(WTFMove(originalTopics));
+
+ WorkQueue::main().dispatch([this]() mutable {
+ startImpl(IsRetry::Yes);
+ });
+ });
+#endif
+}
+
+class UnsubscribeRequest : public PushServiceRequestImpl<bool> {
+public:
+ UnsubscribeRequest(PushService&, const String& bundleIdentifier, const String& scope, PushSubscriptionIdentifier, ResultHandler&&);
+ virtual ~UnsubscribeRequest() = default;
+
+protected:
+ ASCIILiteral description() const final { return "UnsubscribeRequest"_s; }
+ void startInternal() final;
+ void finish() final { m_service.didCompleteUnsubscribeRequest(*this); }
+
+private:
+ PushSubscriptionIdentifier m_subscriptionIdentifier;
+};
+
+UnsubscribeRequest::UnsubscribeRequest(PushService& service, const String& bundleIdentifier, const String& scope, PushSubscriptionIdentifier subscriptionIdentifier, ResultHandler&& resultHandler)
+ : PushServiceRequestImpl(service, bundleIdentifier, scope, WTFMove(resultHandler))
+ , m_subscriptionIdentifier(subscriptionIdentifier)
+{
+}
+
+// Implements the webpushd side of PushSubscription.unsubscribe.
+void UnsubscribeRequest::startInternal()
+{
+ m_database.getRecordByBundleIdentifierAndScope(m_bundleIdentifier, m_scope, [this](auto&& result) mutable {
+ if (!result || m_subscriptionIdentifier != result->identifier) {
+ fulfill(false);
+ return;
+ }
+
+ auto topic = makePushTopic(m_bundleIdentifier, m_scope);
+ m_connection.unsubscribe(topic, result->serverVAPIDPublicKey, [this](bool unsubscribed, NSError *error) mutable {
+#if !RELEASE_LOG_DISABLED
+ // If we fail to unsubscribe from apsd, just drop a log. We still want to continue and remove the record from our database in case there's a state mismatch between our database and apsd's database.
+ if (!unsubscribed)
+ RELEASE_LOG(Push, "PushSubscription.unsubscribe(bundleID: %{public}s, scope: %{sensitive}s) failed with domain: %{public}s code: %lld)", m_bundleIdentifier.utf8().data(), m_scope.utf8().data(), error.domain.UTF8String ?: "none", static_cast<int64_t>(error.code));
+#else
+ UNUSED_PARAM(unsubscribed);
+ UNUSED_PARAM(error);
+#endif
+
+ m_database.removeRecordByIdentifier(m_subscriptionIdentifier, [this](bool removed) mutable {
+ if (!removed) {
+ fulfill(false);
+ return;
+ }
+
+ // FIXME: support partial topic list updates.
+ updateTopicLists(m_connection, m_database, [this]() mutable {
+ fulfill(true);
+ });
+ });
+ });
+ });
+}
+
+// Only allow one request per (bundleIdentifier, scope) to proceed at once. For instance, if a given
+// page calls PushManager.subscribe() twice in a row, the second subscribe call won't start until
+// the first one completes.
+void PushService::enqueuePushServiceRequest(PushServiceRequestMap& map, std::unique_ptr<PushServiceRequest>&& request)
+{
+ auto key = std::make_tuple(request->bundleIdentifier(), request->scope());
+ auto addResult = map.ensure(key, []() {
+ return Deque<std::unique_ptr<PushServiceRequest>> { };
+ });
+
+ auto addedRequest = request.get();
+ addResult.iterator->value.append(WTFMove(request));
+
+ if (addResult.isNewEntry)
+ addedRequest->start();
+}
+
+void PushService::finishedPushServiceRequest(PushServiceRequestMap& map, PushServiceRequest& request)
+{
+ auto key = std::make_tuple(request.bundleIdentifier(), request.scope());
+ auto requestQueueIt = map.find(key);
+
+ RELEASE_ASSERT(requestQueueIt != map.end());
+ auto& requestQueue = requestQueueIt->value;
+ RELEASE_ASSERT(requestQueue.size() > 0);
+ auto currentRequest = requestQueue.takeFirst();
+ RELEASE_ASSERT(currentRequest.get() == &request);
+
+ PushServiceRequest* nextRequest = nullptr;
+ if (!requestQueue.size())
+ map.remove(requestQueueIt);
+ else
+ nextRequest = requestQueue.first().get();
+
+ // Even if there's no next request to start, hold on to currentRequest until the next turn of the run loop since we're in the middle of executing the finish() member function of currentRequest.
+ WorkQueue::main().dispatch([currentRequest = WTFMove(currentRequest), nextRequest] {
+ if (nextRequest)
+ nextRequest->start();
+ });
+}
+
+void PushService::getSubscription(const String& bundleIdentifier, const String& scope, CompletionHandler<void(const Expected<std::optional<WebCore::PushSubscriptionData>, WebCore::ExceptionData>&)>&& completionHandler)
+{
+ enqueuePushServiceRequest(m_getSubscriptionRequests, makeUnique<GetSubscriptionRequest>(*this, bundleIdentifier, scope, WTFMove(completionHandler)));
+}
+
+void PushService::didCompleteGetSubscriptionRequest(GetSubscriptionRequest& request)
+{
+ finishedPushServiceRequest(m_getSubscriptionRequests, request);
+}
+
+void PushService::subscribe(const String& bundleIdentifier, const String& scope, const Vector<uint8_t>& vapidPublicKey, CompletionHandler<void(const Expected<WebCore::PushSubscriptionData, WebCore::ExceptionData>&)>&& completionHandler)
+{
+ enqueuePushServiceRequest(m_subscribeRequests, makeUnique<SubscribeRequest>(*this, bundleIdentifier, scope, vapidPublicKey, WTFMove(completionHandler)));
+}
+
+void PushService::didCompleteSubscribeRequest(SubscribeRequest& request)
+{
+ finishedPushServiceRequest(m_subscribeRequests, request);
+}
+
+void PushService::unsubscribe(const String& bundleIdentifier, const String& scope, PushSubscriptionIdentifier subscriptionIdentifier, CompletionHandler<void(const Expected<bool, WebCore::ExceptionData>&)>&& completionHandler)
+{
+ enqueuePushServiceRequest(m_unsubscribeRequests, makeUnique<UnsubscribeRequest>(*this, bundleIdentifier, scope, subscriptionIdentifier, WTFMove(completionHandler)));
+}
+
+void PushService::didCompleteUnsubscribeRequest(UnsubscribeRequest& request)
+{
+ finishedPushServiceRequest(m_unsubscribeRequests, request);
+}
+
+enum class ContentEncoding {
+ Empty,
+ AESGCM,
+ AES128GCM
+};
+
+struct RawPushMessage {
+ String topic;
+ ContentEncoding encoding;
+
+ // Only set if encoding is not ContentEncoding::Empty.
+ Vector<uint8_t> encryptedPayload;
+
+ // Only set if encoding is ContentEncoding::AESGCM.
+ Vector<uint8_t> serverPublicKey;
+ Vector<uint8_t> salt;
+
+};
+
+static std::optional<RawPushMessage> makeRawPushMessage(NSString *topic, NSDictionary* userInfo)
+{
+ RawPushMessage message;
+
+ @autoreleasepool {
+ message.topic = topic;
+ NSString *contentEncoding = userInfo[@"content_encoding"];
+ NSString *payloadBase64 = userInfo[@"payload"];
+
+ if (!contentEncoding.length || !payloadBase64.length) {
+ message.encoding = ContentEncoding::Empty;
+ return message;
+ }
+
+ if ([contentEncoding isEqualToString:@"aes128gcm"])
+ message.encoding = ContentEncoding::AES128GCM;
+ else if ([contentEncoding isEqualToString:@"aesgcm"]) {
+ message.encoding = ContentEncoding::AESGCM;
+
+ NSString *serverPublicKeyBase64URL = userInfo[@"as_publickey"];
+ NSString *saltBase64URL = userInfo[@"as_salt"];
+ if (!serverPublicKeyBase64URL || !saltBase64URL) {
+ RELEASE_LOG(Push, "Dropping aesgcm-encrypted push without required server key and salt");
+ return std::nullopt;
+ }
+
+ auto serverPublicKey = base64URLDecode(serverPublicKeyBase64URL);
+ auto salt = base64URLDecode(saltBase64URL);
+ if (!serverPublicKey || !salt) {
+ RELEASE_LOG(Push, "Dropping aesgcm-encrypted push with improperly encoded server key and salt");
+ return std::nullopt;
+ }
+
+ message.serverPublicKey = WTFMove(*serverPublicKey);
+ message.salt = WTFMove(*salt);
+ } else {
+ RELEASE_LOG(Push, "Dropping push with unknown content encoding: %{public}s", contentEncoding.UTF8String);
+ return std::nullopt;
+ }
+
+ auto payload = base64Decode(payloadBase64);
+ if (!payload) {
+ RELEASE_LOG(Push, "Dropping push with improperly encoded payload");
+ return std::nullopt;
+ }
+ message.encryptedPayload = WTFMove(*payload);
+ }
+
+ return message;
+}
+
+void PushService::didReceivePushMessage(NSString* topic, NSDictionary* userInfo, CompletionHandler<void()>&& completionHandler)
+{
+ auto messageResult = makeRawPushMessage(topic, userInfo);
+ if (!messageResult)
+ return;
+
+ m_database->getRecordByTopic(topic, [this, message = WTFMove(*messageResult), completionHandler = WTFMove(completionHandler)](auto&& recordResult) mutable {
+ if (!recordResult) {
+ RELEASE_LOG(Push, "Dropping incoming push sent to unknown topic: %{sensitive}s", message.topic.utf8().data());
+ completionHandler();
+ return;
+ }
+ auto record = WTFMove(*recordResult);
+
+ if (message.encoding == ContentEncoding::Empty) {
+ m_incomingPushMessageHandler(record.bundleID, WebKit::WebPushMessage { { }, URL(URL(), record.scope) });
+ completionHandler();
+ return;
+ }
+
+ PushCrypto::ClientKeys clientKeys {
+ { WTFMove(record.clientPublicKey), WTFMove(record.clientPrivateKey) },
+ WTFMove(record.sharedAuthSecret)
+ };
+
+ std::optional<Vector<uint8_t>> decryptedPayload;
+ if (message.encoding == ContentEncoding::AES128GCM)
+ decryptedPayload = decryptAES128GCMPayload(clientKeys, message.encryptedPayload);
+ else if (message.encoding == ContentEncoding::AESGCM)
+ decryptedPayload = decryptAESGCMPayload(clientKeys, message.serverPublicKey, message.salt, message.encryptedPayload);
+
+ if (!decryptedPayload) {
+ RELEASE_LOG(Push, "Dropping incoming push due to decryption error for topic %{sensitive}s", message.topic.utf8().data());
+ completionHandler();
+ return;
+ }
+
+ m_incomingPushMessageHandler(record.bundleID, WebKit::WebPushMessage { WTFMove(*decryptedPayload), URL(URL(), record.scope) });
+
+ completionHandler();
+ });
+}
+
+} // namespace WebPushD
Modified: trunk/Source/WebKit/webpushd/PushServiceConnection.h (289352 => 289353)
--- trunk/Source/WebKit/webpushd/PushServiceConnection.h 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/PushServiceConnection.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -25,6 +25,7 @@
#pragma once
+#include <WebCore/PushMessageCrypto.h>
#include <wtf/CompletionHandler.h>
#include <wtf/Deque.h>
#include <wtf/Function.h>
@@ -47,6 +48,8 @@
PushServiceConnection() = default;
virtual ~PushServiceConnection() = default;
+ virtual WebCore::PushCrypto::ClientKeys generateClientKeys();
+
using SubscribeHandler = CompletionHandler<void(NSString *, NSError *)>;
virtual void subscribe(const String& topic, const Vector<uint8_t>& vapidPublicKey, SubscribeHandler&&) = 0;
Modified: trunk/Source/WebKit/webpushd/PushServiceConnection.mm (289352 => 289353)
--- trunk/Source/WebKit/webpushd/PushServiceConnection.mm 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/PushServiceConnection.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -28,8 +28,15 @@
#import <wtf/WorkQueue.h>
+using namespace WebCore;
+
namespace WebPushD {
+PushCrypto::ClientKeys PushServiceConnection::generateClientKeys()
+{
+ return PushCrypto::ClientKeys::generate();
+}
+
void PushServiceConnection::startListeningForPushMessages(IncomingPushMessageHandler&& handler)
{
m_incomingPushMessageHandler = WTFMove(handler);
Modified: trunk/Source/WebKit/webpushd/WebPushDaemon.h (289352 => 289353)
--- trunk/Source/WebKit/webpushd/WebPushDaemon.h 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/WebPushDaemon.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -27,6 +27,7 @@
#include "PushClientConnection.h"
#include "PushMessageForTesting.h"
+#include "PushService.h"
#include "WebPushDaemonConnectionConfiguration.h"
#include "WebPushDaemonConstants.h"
#include "WebPushMessage.h"
@@ -61,6 +62,10 @@
void connectionAdded(xpc_connection_t);
void connectionRemoved(xpc_connection_t);
+ void startMockPushService();
+ void startPushService(const String& incomingPushServiceName, const String& pushDatabasePath);
+ void handleIncomingPush(const String& bundleIdentifier, WebKit::WebPushMessage&&);
+
// Message handlers
void echoTwice(ClientConnection*, const String&, CompletionHandler<void(const String&)>&& replySender);
void requestSystemNotificationPermission(ClientConnection*, const String&, CompletionHandler<void(bool)>&& replySender);
@@ -69,6 +74,7 @@
void setDebugModeIsEnabled(ClientConnection*, bool);
void updateConnectionConfiguration(ClientConnection*, const WebPushDaemonConnectionConfiguration&);
void injectPushMessageForTesting(ClientConnection*, const PushMessageForTesting&, CompletionHandler<void(bool)>&&);
+ void injectEncryptedPushMessageForTesting(ClientConnection*, const String&, CompletionHandler<void(bool)>&&);
void getPendingPushMessages(ClientConnection*, CompletionHandler<void(const Vector<WebKit::WebPushMessage>&)>&& replySender);
void subscribeToPushService(ClientConnection*, const URL& scopeURL, const Vector<uint8_t>& applicationServerKey, CompletionHandler<void(const Expected<WebCore::PushSubscriptionData, WebCore::ExceptionData>&)>&& replySender);
void unsubscribeFromPushService(ClientConnection*, const URL& scopeURL, WebCore::PushSubscriptionIdentifier, CompletionHandler<void(const Expected<bool, WebCore::ExceptionData>&)>&& replySender);
@@ -88,9 +94,17 @@
void notifyClientPushMessageIsAvailable(const String& clientCodeSigningIdentifier);
+ void setPushService(std::unique_ptr<PushService>&&);
+ void runAfterStartingPushService(Function<void()>&&);
+
ClientConnection* toClientConnection(xpc_connection_t);
HashMap<xpc_connection_t, Ref<ClientConnection>> m_connectionMap;
+ std::unique_ptr<PushService> m_pushService;
+ bool m_pushServiceStarted { false };
+ Deque<Function<void()>> m_pendingPushServiceFunctions;
+
+ HashMap<String, Vector<WebKit::WebPushMessage>> m_pushMessages;
HashMap<String, Deque<PushMessageForTesting>> m_testingPushMessages;
};
Modified: trunk/Source/WebKit/webpushd/WebPushDaemon.mm (289352 => 289353)
--- trunk/Source/WebKit/webpushd/WebPushDaemon.mm 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/WebPushDaemon.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -40,6 +40,7 @@
#import <wtf/NeverDestroyed.h>
#import <wtf/Span.h>
#import <wtf/URL.h>
+#import <wtf/WorkQueue.h>
using namespace WebKit::WebPushD;
@@ -91,6 +92,11 @@
REPLY(bool)
END
+FUNCTION(injectEncryptedPushMessageForTesting)
+ARGUMENTS(String)
+REPLY(bool)
+END
+
FUNCTION(subscribeToPushService)
ARGUMENTS(URL, Vector<uint8_t>)
REPLY(const Expected<WebCore::PushSubscriptionData, WebCore::ExceptionData>&)
@@ -151,6 +157,13 @@
return encoder.takeBuffer();
}
+WebPushD::EncodedMessage injectEncryptedPushMessageForTesting::encodeReply(bool reply)
+{
+ WebKit::Daemon::Encoder encoder;
+ encoder << reply;
+ return encoder.takeBuffer();
+}
+
WebPushD::EncodedMessage getPendingPushMessages::encodeReply(const Vector<WebKit::WebPushMessage>& reply)
{
WebKit::Daemon::Encoder encoder;
@@ -224,6 +237,51 @@
return daemon;
}
+void Daemon::startMockPushService()
+{
+ auto messageHandler = [this](const String& bundleIdentifier, WebKit::WebPushMessage&& message) {
+ handleIncomingPush(bundleIdentifier, WTFMove(message));
+ };
+ PushService::createMockService(WTFMove(messageHandler), [this](auto&& pushService) mutable {
+ setPushService(WTFMove(pushService));
+ });
+}
+
+void Daemon::startPushService(const String& incomingPushServiceName, const String& databasePath)
+{
+ auto messageHandler = [this](const String& bundleIdentifier, WebKit::WebPushMessage&& message) {
+ handleIncomingPush(bundleIdentifier, WTFMove(message));
+ };
+ PushService::create(incomingPushServiceName, databasePath, WTFMove(messageHandler), [this](auto&& pushService) mutable {
+ setPushService(WTFMove(pushService));
+ });
+}
+
+void Daemon::setPushService(std::unique_ptr<PushService>&& pushService)
+{
+ m_pushService = WTFMove(pushService);
+ m_pushServiceStarted = true;
+
+ if (!m_pendingPushServiceFunctions.size())
+ return;
+
+ WorkQueue::main().dispatch([this]() {
+ while (m_pendingPushServiceFunctions.size()) {
+ auto function = m_pendingPushServiceFunctions.takeFirst();
+ function();
+ }
+ });
+}
+
+void Daemon::runAfterStartingPushService(Function<void()>&& function)
+{
+ if (!m_pushServiceStarted) {
+ m_pendingPushServiceFunctions.append(WTFMove(function));
+ return;
+ }
+ function();
+}
+
void Daemon::broadcastDebugMessage(JSC::MessageLevel messageLevel, const String& message)
{
auto dictionary = adoptOSObject(xpc_dictionary_create(nullptr, nullptr, 0));
@@ -325,6 +383,9 @@
case MessageType::InjectPushMessageForTesting:
handleWebPushDMessageWithReply<MessageInfo::injectPushMessageForTesting>(clientConnection, encodedMessage, WTFMove(replySender));
break;
+ case MessageType::InjectEncryptedPushMessageForTesting:
+ handleWebPushDMessageWithReply<MessageInfo::injectEncryptedPushMessageForTesting>(clientConnection, encodedMessage, WTFMove(replySender));
+ break;
case MessageType::GetPendingPushMessages:
handleWebPushDMessageWithReply<MessageInfo::getPendingPushMessages>(clientConnection, encodedMessage, WTFMove(replySender));
break;
@@ -431,6 +492,46 @@
replySender(true);
}
+void Daemon::injectEncryptedPushMessageForTesting(ClientConnection* connection, const String& message, CompletionHandler<void(bool)>&& replySender)
+{
+ if (!connection->hostAppHasPushInjectEntitlement()) {
+ connection->broadcastDebugMessage("Attempting to inject a push message from an unentitled process");
+ replySender(false);
+ return;
+ }
+
+ runAfterStartingPushService([this, message = message, replySender = WTFMove(replySender)]() mutable {
+ if (!m_pushService) {
+ replySender(false);
+ return;
+ }
+
+ auto bytes = message.utf8();
+ RetainPtr<NSData> data = "" alloc] initWithBytes:bytes.data() length: bytes.length()]);
+
+ id obj = [NSJSONSerialization JSONObjectWithData:data.get() options:0 error:nullptr];
+ if (!obj || ![obj isKindOfClass:[NSDictionary class]]) {
+ replySender(false);
+ return;
+ }
+ NSLog(@"got obj: %@", obj);
+
+ m_pushService->didReceivePushMessage(obj[@"topic"], obj[@"userInfo"], [replySender = WTFMove(replySender)]() mutable {
+ replySender(true);
+ });
+ });
+}
+
+void Daemon::handleIncomingPush(const String& bundleIdentifier, WebKit::WebPushMessage&& message)
+{
+ auto addResult = m_pushMessages.ensure(bundleIdentifier, [] {
+ return Vector<WebKit::WebPushMessage> { };
+ });
+ addResult.iterator->value.append(WTFMove(message));
+
+ notifyClientPushMessageIsAvailable(bundleIdentifier);
+}
+
void Daemon::notifyClientPushMessageIsAvailable(const String& clientCodeSigningIdentifier)
{
#if PLATFORM(MAC)
@@ -462,6 +563,9 @@
Vector<WebKit::WebPushMessage> resultMessages;
+ if (auto iterator = m_pushMessages.find(hostAppCodeSigningIdentifier); iterator != m_pushMessages.end())
+ std::swap(resultMessages, iterator->value);
+
auto iterator = m_testingPushMessages.find(hostAppCodeSigningIdentifier);
if (iterator != m_testingPushMessages.end()) {
for (auto& message : iterator->value) {
@@ -476,25 +580,50 @@
replySender(WTFMove(resultMessages));
}
-void Daemon::subscribeToPushService(ClientConnection* connection, const URL& scopeURL, const Vector<uint8_t>& applicationServerKey, CompletionHandler<void(const Expected<WebCore::PushSubscriptionData, WebCore::ExceptionData>&)>&& replySender)
+void Daemon::subscribeToPushService(ClientConnection* connection, const URL& scopeURL, const Vector<uint8_t>& vapidPublicKey, CompletionHandler<void(const Expected<WebCore::PushSubscriptionData, WebCore::ExceptionData>&)>&& replySender)
{
- UNUSED_PARAM(applicationServerKey);
- replySender(makeUnexpected(WebCore::ExceptionData { WebCore::NotAllowedError, "Lack permission"_s }));
+ runAfterStartingPushService([this, bundleIdentifier = connection->hostAppCodeSigningIdentifier(), scope = scopeURL.string(), vapidPublicKey, replySender = WTFMove(replySender)]() mutable {
+ if (!m_pushService) {
+ replySender(makeUnexpected(WebCore::ExceptionData { WebCore::InvalidStateError, "Push service initialization failed"_s }));
+ return;
+ }
+
+ m_pushService->subscribe(bundleIdentifier, scope, vapidPublicKey, WTFMove(replySender));
+ });
}
-void Daemon::unsubscribeFromPushService(ClientConnection* connection, const URL& scopeURL, WebCore::PushSubscriptionIdentifier pushSubscriptionIdentifier, CompletionHandler<void(const Expected<bool, WebCore::ExceptionData>&)>&& replySender)
+void Daemon::unsubscribeFromPushService(ClientConnection* connection, const URL& scopeURL, WebCore::PushSubscriptionIdentifier subscriptionIdentifier, CompletionHandler<void(const Expected<bool, WebCore::ExceptionData>&)>&& replySender)
{
- replySender(false);
+ runAfterStartingPushService([this, bundleIdentifier = connection->hostAppCodeSigningIdentifier(), scope = scopeURL.string(), subscriptionIdentifier, replySender = WTFMove(replySender)]() mutable {
+ if (!m_pushService) {
+ replySender(makeUnexpected(WebCore::ExceptionData { WebCore::InvalidStateError, "Push service initialization failed"_s }));
+ return;
+ }
+
+ m_pushService->unsubscribe(bundleIdentifier, scope, subscriptionIdentifier, WTFMove(replySender));
+ });
}
void Daemon::getPushSubscription(ClientConnection* connection, const URL& scopeURL, CompletionHandler<void(const Expected<std::optional<WebCore::PushSubscriptionData>, WebCore::ExceptionData>&)>&& replySender)
{
- replySender(std::optional<WebCore::PushSubscriptionData> { });
+ runAfterStartingPushService([this, bundleIdentifier = connection->hostAppCodeSigningIdentifier(), scope = scopeURL.string(), replySender = WTFMove(replySender)]() mutable {
+ if (!m_pushService) {
+ replySender(makeUnexpected(WebCore::ExceptionData { WebCore::InvalidStateError, "Push service initialization failed"_s }));
+ return;
+ }
+
+ m_pushService->getSubscription(bundleIdentifier, scope, WTFMove(replySender));
+ });
}
void Daemon::getPushPermissionState(ClientConnection* connection, const URL& scopeURL, CompletionHandler<void(const Expected<uint8_t, WebCore::ExceptionData>&)>&& replySender)
{
+ // FIXME
+#if HAVE(APPLE_PUSH_SERVICE_URL_TOKEN_SUPPORT)
+ replySender(static_cast<uint8_t>(WebCore::PushPermissionState::Granted));
+#else
replySender(static_cast<uint8_t>(WebCore::PushPermissionState::Denied));
+#endif
}
ClientConnection* Daemon::toClientConnection(xpc_connection_t connection)
Modified: trunk/Source/WebKit/webpushd/WebPushDaemonMain.h (289352 => 289353)
--- trunk/Source/WebKit/webpushd/WebPushDaemonMain.h 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/WebPushDaemonMain.h 2022-02-08 02:58:30 UTC (rev 289353)
@@ -27,6 +27,6 @@
namespace WebKit {
-int WebPushDaemonMain(int, const char**);
+int WebPushDaemonMain(int, char**);
} // namespace WebKit
Modified: trunk/Source/WebKit/webpushd/WebPushDaemonMain.mm (289352 => 289353)
--- trunk/Source/WebKit/webpushd/WebPushDaemonMain.mm 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/WebPushDaemonMain.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -30,24 +30,21 @@
#import "DaemonDecoder.h"
#import "DaemonEncoder.h"
#import "DaemonUtilities.h"
+#import "LogInitialization.h"
#import "WebPushDaemon.h"
#import <Foundation/Foundation.h>
+#import <WebCore/LogInitialization.h>
+#import <getopt.h>
+#import <wtf/LogInitialization.h>
#import <wtf/MainThread.h>
#import <wtf/spi/darwin/XPCSPI.h>
-#if USE(APPLE_INTERNAL_SDK)
-#import <servers/bootstrap.h>
-#else
-#import <mach/std_types.h>
-extern "C" {
-extern kern_return_t bootstrap_check_in(mach_port_t bootstrapPort, const char *serviceName, mach_port_t*);
-}
-#endif
-
using WebKit::Daemon::EncodedMessage;
using WebPushD::Daemon;
-static const char *incomingPushServiceName = "com.apple.aps.webkit.webpushd.incoming-push";
+static const ASCIILiteral entitlementName = "com.apple.private.webkit.webpush"_s;
+static const ASCIILiteral defaultMachServiceName = "com.apple.webkit.webpushd.service"_s;
+static const ASCIILiteral defaultIncomingPushServiceName = "com.apple.aps.webkit.webpushd.incoming-push"_s;
namespace WebPushD {
@@ -74,23 +71,55 @@
namespace WebKit {
-int WebPushDaemonMain(int argc, const char** argv)
+int WebPushDaemonMain(int argc, char** argv)
{
- if (argc != 3 || strcmp(argv[1], "--machServiceName")) {
- NSLog(@"usage: webpushd --machServiceName <name>");
- return -1;
- }
- const char* machServiceName = argv[2];
-
@autoreleasepool {
- WebKit::startListeningForMachServiceConnections(machServiceName, "com.apple.private.webkit.webpush", connectionAdded, connectionRemoved, connectionEventHandler);
+ WTF::initializeMainThread();
- // TODO: remove this once we actually start using APSConnection.
- mach_port_t incomingMessagePort;
- if (bootstrap_check_in(bootstrap_port, incomingPushServiceName, &incomingMessagePort) != KERN_SUCCESS)
- NSLog(@"Couldn't register for incoming push launch port.");
-
- WTF::initializeMainThread();
+#if !LOG_DISABLED || !RELEASE_LOG_DISABLED
+ WTF::logChannels().initializeLogChannelsIfNecessary();
+ WebCore::logChannels().initializeLogChannelsIfNecessary();
+ WebKit::logChannels().initializeLogChannelsIfNecessary();
+#endif // !LOG_DISABLED || !RELEASE_LOG_DISABLED
+
+ static struct option options[] = {
+ { "machServiceName", required_argument, 0, 'm' },
+ { "incomingPushServiceName", required_argument, 0, 'p' },
+ { "useMockPushService", no_argument, 0, 'f' }
+ };
+
+ const char* machServiceName = defaultMachServiceName;
+ const char* incomingPushServiceName = defaultIncomingPushServiceName;
+ bool useMockPushService = false;
+
+ int c;
+ int optionIndex;
+ while ((c = getopt_long(argc, argv, "", options, &optionIndex)) != -1) {
+ switch (c) {
+ case 'm':
+ machServiceName = optarg;
+ break;
+ case 'p':
+ incomingPushServiceName = optarg;
+ break;
+ case 'f':
+ useMockPushService = true;
+ break;
+ default:
+ fprintf(stderr, "Unknown option: %c\n", optopt);
+ exit(1);
+ }
+ }
+
+ WebKit::startListeningForMachServiceConnections(machServiceName, entitlementName, connectionAdded, connectionRemoved, connectionEventHandler);
+
+ if (useMockPushService)
+ ::WebPushD::Daemon::singleton().startMockPushService();
+ else {
+ String libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0];
+ String pushDatabasePath = FileSystem::pathByAppendingComponents(libraryPath, { "WebKit", "WebPush", "PushDatabase.db" });
+ ::WebPushD::Daemon::singleton().startPushService(incomingPushServiceName, pushDatabasePath);
+ }
}
CFRunLoopRun();
return 0;
Modified: trunk/Source/WebKit/webpushd/com.apple.webkit.webpushd.mac.plist (289352 => 289353)
--- trunk/Source/WebKit/webpushd/com.apple.webkit.webpushd.mac.plist 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/com.apple.webkit.webpushd.mac.plist 2022-02-08 02:58:30 UTC (rev 289353)
@@ -19,7 +19,7 @@
<string>Adaptive</string>
<key>ProgramArguments</key>
<array>
- <string>/System/Library/Frameworks/WebKit.framework/Daemons/webpushd</string>
+ <string>/System/Library/Frameworks/WebKit.framework/Versions/A/Daemons/webpushd</string>
<string>--machServiceName</string>
<string>com.apple.webkit.webpushd.service</string>
</array>
Modified: trunk/Source/WebKit/webpushd/webpushd.cpp (289352 => 289353)
--- trunk/Source/WebKit/webpushd/webpushd.cpp 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Source/WebKit/webpushd/webpushd.cpp 2022-02-08 02:58:30 UTC (rev 289353)
@@ -26,7 +26,7 @@
#include <WebKit/WKMain.h>
#include <os/availability.h>
-int main(int argc, const char** argv)
+int main(int argc, char** argv)
{
WKWebPushDaemonMain(argc, argv);
}
Modified: trunk/Tools/ChangeLog (289352 => 289353)
--- trunk/Tools/ChangeLog 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Tools/ChangeLog 2022-02-08 02:58:30 UTC (rev 289353)
@@ -1,3 +1,25 @@
+2022-02-07 Ben Nham <[email protected]>
+
+ Add PushService
+ https://bugs.webkit.org/show_bug.cgi?id=235857
+
+ Reviewed by Brady Eidson.
+
+ - Modifed the existing injected push test to inject different types of real push objects
+ from APS (an empty push, an aesgcm-encrypted push, and an aes128gcm-encrypted push).
+ - Add a few basic test cases for subscribing and unsubscribing from push.
+
+ * TestWebKitAPI/Tests/WebKitCocoa/WebPushDaemon.mm:
+ (-[NotificationScriptMessageHandler setMessageHandler:]):
+ (-[NotificationScriptMessageHandler userContentController:didReceiveScriptMessage:]):
+ (TestWebKitAPI::testWebPushDaemonPList):
+ (TestWebKitAPI::createMessageDictionary):
+ (TestWebKitAPI::sendMessageToDaemon):
+ (TestWebKitAPI::sendMessageToDaemonWaitingForReply):
+ (TestWebKitAPI::sendConfigurationWithAuditToken):
+ (TestWebKitAPI::TEST):
+ (TestWebKitAPI::function): Deleted.
+
2022-02-07 Brent Fulgham <[email protected]>
Always sync ResourceRequest isAppInitiated request with NSURLRequest attribution value
Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/WebPushDaemon.mm (289352 => 289353)
--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/WebPushDaemon.mm 2022-02-08 02:58:20 UTC (rev 289352)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/WebPushDaemon.mm 2022-02-08 02:58:30 UTC (rev 289353)
@@ -35,13 +35,18 @@
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKUIDelegatePrivate.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 PLATFORM(MAC) || PLATFORM(IOS)
+using WebKit::WebPushD::MessageType;
+
static bool alertReceived = false;
@interface NotificationPermissionDelegate : NSObject<WKUIDelegatePrivate>
@end
@@ -61,6 +66,26 @@
@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()
@@ -79,7 +104,8 @@
@"ProgramArguments" : @[
testWebPushDaemonLocation().get().path,
@"--machServiceName",
- @"org.webkit.webpushtestdaemon.service"
+ @"org.webkit.webpushtestdaemon.service",
+ @"--useMockPushService"
]
};
}
@@ -135,17 +161,17 @@
EXPECT_NULL(error);
}
-static RetainPtr<xpc_object_t> createMessageDictionary(uint8_t messageType, const Vector<uint8_t>& message)
+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", messageType);
+ 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, uint8_t messageType, const Vector<uint8_t>& 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());
@@ -152,7 +178,7 @@
}
// 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)
+void sendMessageToDaemonWaitingForReply(xpc_connection_t connection, MessageType messageType, const Vector<uint8_t>& message)
{
auto dictionary = createMessageDictionary(messageType, message);
@@ -181,7 +207,7 @@
encodedMessage[1] = 1;
encodedMessage[2] = 32;
memcpy(&encodedMessage[10], &token, sizeof(token));
- sendMessageToDaemon(connection, 6, encodedMessage);
+ sendMessageToDaemon(connection, MessageType::UpdateConnectionConfiguration, encodedMessage);
}
}
@@ -252,7 +278,7 @@
auto dictionary = adoptNS(xpc_dictionary_create(nullptr, nullptr, 0));
Vector<uint8_t> encodedMessage(1);
encodedMessage[0] = 1;
- sendMessageToDaemon(connection.get(), 5, encodedMessage);
+ sendMessageToDaemon(connection.get(), MessageType::SetDebugModeIsEnabled, encodedMessage);
TestWebKitAPI::Util::run(&done);
}
@@ -361,54 +387,6 @@
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;
@@ -418,78 +396,317 @@
TestWebKitAPI::Util::run(&clearedStore);
}
-TEST(WebPushD, HandleInjectedPush)
-{
- [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins];
+class WebPushDTest : public ::testing::Test {
+protected:
+ WebPushDTest()
+ {
+ [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins];
- NSURL *tempDirectory = setUpTestWebPushD();
+ m_tempDirectory = retainPtr(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 dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]);
+ dataStoreConfiguration.get().webPushMachServiceName = @"org.webkit.webpushtestdaemon.service";
+ dataStoreConfiguration.get().webPushDaemonUsesMockBundlesForTesting = YES;
+ m_dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]);
- auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
- configuration.get().websiteDataStore = dataStore.get();
- clearWebsiteDataStore([configuration websiteDataStore]);
+ m_configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ m_configuration.get().websiteDataStore = m_dataStore.get();
+ clearWebsiteDataStore([m_configuration websiteDataStore]);
- [configuration.get().preferences _setNotificationsEnabled:YES];
- for (_WKExperimentalFeature *feature in [WKPreferences _experimentalFeatures]) {
- if ([feature.key isEqualToString:@"BuiltInNotificationsEnabled"])
- [[configuration preferences] _setEnabled:YES forFeature:feature];
+ [m_configuration.get().preferences _setNotificationsEnabled:YES];
+ for (_WKExperimentalFeature *feature in [WKPreferences _experimentalFeatures]) {
+ if ([feature.key isEqualToString:@"BuiltInNotificationsEnabled"])
+ [[m_configuration preferences] _setEnabled:YES forFeature:feature];
+ else if ([feature.key isEqualToString:@"PushAPIEnabled"])
+ [[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"];
}
- auto messageHandler = adoptNS([[TestMessageHandler alloc] init]);
- [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"];
- __block bool done = false;
- [messageHandler addMessage:@"Ready" withHandler:^{
- done = true;
+ void loadRequest(const char* htmlSource, const char* serviceWorkerScriptSource)
+ {
+ static const char* constants = R"SRC(
+ const VALID_SERVER_KEY = "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs";
+ const VALID_SERVER_KEY_THAT_CAUSES_INJECTED_FAILURE = "BEAxaUMo1s8tjORxJfnSSvWhYb4u51kg1hWT2s_9gpV7Zxar1pF_2BQ8AncuAdS2BoLhN4qaxzBy2CwHE8BBzWg";
+ )SRC";
+ m_server.reset(new TestWebKitAPI::HTTPServer({
+ { "/", { htmlSource } },
+ { "/constants.js", { { { "Content-Type", "application/_javascript_" } }, constants } },
+ { "/sw.js", { { { "Content-Type", "application/_javascript_" } }, serviceWorkerScriptSource } }
+ }, TestWebKitAPI::HTTPServer::Protocol::Http));
+
+ m_webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:m_configuration.get()]);
+ [m_webView loadRequest:m_server->request()];
+ }
+
+ ~WebPushDTest()
+ {
+ cleanUpTestWebPushD(m_tempDirectory.get());
+ }
+
+ 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;
+ RetainPtr<WKWebView> m_webView;
+};
+
+class WebPushDInjectedPushTest : public WebPushDTest {
+protected:
+ void runTest(NSString *expectedMessage, NSDictionary *pushUserInfo);
+};
+
+void WebPushDInjectedPushTest::runTest(NSString *expectedMessage, NSDictionary *pushUserInfo)
+{
+ static const char* htmlSource = R"SWRESOURCE(
+ <script src=""
+ <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";
+
+ static const char* serviceWorkerSource = 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);
+ } catch (e) {
+ port.postMessage("Error: " + e);
+ }
+ });
+ )SWRESOURCE";
+
+ __block bool ready = false;
+ [m_testMessageHandler addMessage:@"Ready" withHandler:^{
+ ready = true;
}];
- [messageHandler addMessage:@"Received: Hello World" withHandler:^{
- done = true;
+
+ __block bool gotExpectedMessage = false;
+ [m_testMessageHandler addMessage:expectedMessage withHandler:^{
+ gotExpectedMessage = true;
}];
- TestWebKitAPI::HTTPServer server({
- { "/", { mainSWBytes } },
- { "/sw.js", { { { "Content-Type", "application/_javascript_" } }, scriptBytes } }
- }, TestWebKitAPI::HTTPServer::Protocol::Http);
+ loadRequest(htmlSource, serviceWorkerSource);
- auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
- [webView loadRequest:server.request()];
+ TestWebKitAPI::Util::run(&ready);
- TestWebKitAPI::Util::run(&done);
- done = false;
+ String bundleIdentifier = "com.apple.WebKit.TestWebKitAPI"_s;
+ String scope = m_server->request().URL.absoluteString;
+ String topic = bundleIdentifier + " "_s + scope;
- // Inject push message
- auto encodedMessage = encodeString("com.apple.WebKit.TestWebKitAPI");
- encodedMessage.appendVector(encodeString(server.request().URL.absoluteString));
- encodedMessage.appendVector(encodeString("Hello World"));
+ id obj = @{
+ @"topic": (NSString *)topic,
+ @"userInfo": pushUserInfo
+ };
+ NSData *data = "" 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(), 7, encodedMessage);
+ sendMessageToDaemonWaitingForReply(utilityConnection.get(), MessageType::InjectEncryptedPushMessageForTesting, encodedMessage);
// Fetch push messages
+ __block bool gotMessages = false;
__block RetainPtr<NSArray<NSDictionary *>> messages;
- [dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *rawMessages) {
+ [m_dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *rawMessages) {
messages = rawMessages;
- done = true;
+ gotMessages = true;
}];
- TestWebKitAPI::Util::run(&done);
- done = false;
+ TestWebKitAPI::Util::run(&gotMessages);
EXPECT_EQ([messages count], 1u);
// Handle push message
__block bool pushMessageProcessed = false;
- [dataStore _processPushMessage:[messages firstObject] completionHandler:^(bool result) {
+ [m_dataStore _processPushMessage:[messages firstObject] completionHandler:^(bool result) {
pushMessageProcessed = true;
}];
- TestWebKitAPI::Util::run(&done);
+ TestWebKitAPI::Util::run(&gotExpectedMessage);
TestWebKitAPI::Util::run(&pushMessageProcessed);
+}
- cleanUpTestWebPushD(tempDirectory);
+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 const char* source = R"HTML(
+ <script src=""
+ <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";
+
+ __block RetainPtr<id> obj = nil;
+ __block bool done = false;
+ [m_notificationMessageHandler setMessageHandler:^(id message) {
+ obj = message;
+ done = true;
+ }];
+
+ loadRequest(source, "");
+ 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);
+}
+
+TEST_F(WebPushDTest, SubscribeFailureTest)
+{
+ static const char* source = R"HTML(
+ <script src=""
+ <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";
+
+ __block RetainPtr<id> obj = nil;
+ __block bool done = false;
+ [m_notificationMessageHandler setMessageHandler:^(id message) {
+ obj = message;
+ done = true;
+ }];
+
+ loadRequest(source, "");
+ 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"]);
+}
+
+TEST_F(WebPushDTest, UnsubscribeTest)
+{
+ static const char* source = R"HTML(
+ <script src=""
+ <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";
+
+ __block RetainPtr<id> obj = nil;
+ __block bool done = false;
+ [m_notificationMessageHandler setMessageHandler:^(id message) {
+ obj = message;
+ done = true;
+ }];
+
+ loadRequest(source, "");
+ 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]);
+}
+
} // namespace TestWebKitAPI
#endif // PLATFORM(MAC) || PLATFORM(IOS)