blob: f31227e95ca798957bb6b50c520d3bd24f7961c0 [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 "WebPushDaemon.h"
#import "AppBundleRequest.h"
#import "DaemonDecoder.h"
#import "DaemonEncoder.h"
#import "DaemonUtilities.h"
#import "HandleMessage.h"
#import "MockAppBundleRegistry.h"
#import <pal/spi/cocoa/LaunchServicesSPI.h>
#import <wtf/CompletionHandler.h>
#import <wtf/HexNumber.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/Span.h>
using namespace WebKit::WebPushD;
namespace WebPushD {
namespace MessageInfo {
#define FUNCTION(mf) struct mf { static constexpr auto MemberFunction = &WebPushD::Daemon::mf;
#define ARGUMENTS(...) using ArgsTuple = std::tuple<__VA_ARGS__>;
#define REPLY(...) using Reply = CompletionHandler<void(__VA_ARGS__)>; \
static WebPushD::EncodedMessage encodeReply(__VA_ARGS__);
#define END };
FUNCTION(echoTwice)
ARGUMENTS(String)
REPLY(String)
END
FUNCTION(getOriginsWithPushAndNotificationPermissions)
ARGUMENTS()
REPLY(const Vector<String>&)
END
FUNCTION(deletePushAndNotificationRegistration)
ARGUMENTS(String)
REPLY(String)
END
FUNCTION(requestSystemNotificationPermission)
ARGUMENTS(String)
REPLY(bool)
END
FUNCTION(getPendingPushMessages)
ARGUMENTS()
REPLY(const Vector<WebKit::WebPushMessage>&)
END
FUNCTION(setDebugModeIsEnabled)
ARGUMENTS(bool)
END
FUNCTION(updateConnectionConfiguration)
ARGUMENTS(WebPushDaemonConnectionConfiguration)
END
FUNCTION(injectPushMessageForTesting)
ARGUMENTS(PushMessageForTesting)
REPLY(bool)
END
#undef FUNCTION
#undef ARGUMENTS
#undef REPLY
#undef END
WebPushD::EncodedMessage echoTwice::encodeReply(String reply)
{
WebKit::Daemon::Encoder encoder;
encoder << reply;
return encoder.takeBuffer();
}
WebPushD::EncodedMessage getOriginsWithPushAndNotificationPermissions::encodeReply(const Vector<String>& reply)
{
WebKit::Daemon::Encoder encoder;
encoder << reply;
return encoder.takeBuffer();
}
WebPushD::EncodedMessage deletePushAndNotificationRegistration::encodeReply(String reply)
{
WebKit::Daemon::Encoder encoder;
encoder << reply;
return encoder.takeBuffer();
}
WebPushD::EncodedMessage requestSystemNotificationPermission::encodeReply(bool reply)
{
WebKit::Daemon::Encoder encoder;
encoder << reply;
return encoder.takeBuffer();
}
WebPushD::EncodedMessage injectPushMessageForTesting::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;
encoder << reply;
return encoder.takeBuffer();
}
} // namespace MessageInfo
template<typename Info>
void handleWebPushDMessageWithReply(ClientConnection* connection, Span<const uint8_t> encodedMessage, CompletionHandler<void(WebPushD::EncodedMessage&&)>&& replySender)
{
WebKit::Daemon::Decoder decoder(encodedMessage);
std::optional<typename Info::ArgsTuple> arguments;
decoder >> arguments;
if (UNLIKELY(!arguments))
return;
typename Info::Reply completionHandler { [replySender = WTFMove(replySender)] (auto&&... args) mutable {
replySender(Info::encodeReply(args...));
} };
IPC::callMemberFunction(tuple_cat(std::make_tuple(connection), WTFMove(*arguments)), WTFMove(completionHandler), &WebPushD::Daemon::singleton(), Info::MemberFunction);
}
template<typename Info>
void handleWebPushDMessage(ClientConnection* connection, Span<const uint8_t> encodedMessage)
{
WebKit::Daemon::Decoder decoder(encodedMessage);
std::optional<typename Info::ArgsTuple> arguments;
decoder >> arguments;
if (UNLIKELY(!arguments))
return;
IPC::callMemberFunction(tuple_cat(std::make_tuple(connection), WTFMove(*arguments)), &WebPushD::Daemon::singleton(), Info::MemberFunction);
}
Daemon& Daemon::singleton()
{
static NeverDestroyed<Daemon> daemon;
return daemon;
}
void Daemon::broadcastDebugMessage(JSC::MessageLevel messageLevel, const String& message)
{
auto dictionary = adoptOSObject(xpc_dictionary_create(nullptr, nullptr, 0));
xpc_dictionary_set_uint64(dictionary.get(), protocolDebugMessageLevelKey, static_cast<uint64_t>(messageLevel));
xpc_dictionary_set_string(dictionary.get(), protocolDebugMessageKey, message.utf8().data());
for (auto& iterator : m_connectionMap) {
if (iterator.value->debugModeIsEnabled())
xpc_connection_send_message(iterator.key, dictionary.get());
}
}
void Daemon::broadcastAllConnectionIdentities()
{
broadcastDebugMessage((JSC::MessageLevel)4, "===\nCurrent connections:");
auto connections = copyToVector(m_connectionMap.values());
std::sort(connections.begin(), connections.end(), [] (const Ref<ClientConnection>& a, const Ref<ClientConnection>& b) {
return a->identifier() < b->identifier();
});
for (auto& iterator : connections)
iterator->broadcastDebugMessage("");
broadcastDebugMessage((JSC::MessageLevel)4, "===");
}
void Daemon::connectionEventHandler(xpc_object_t request)
{
if (xpc_get_type(request) != XPC_TYPE_DICTIONARY)
return;
if (xpc_dictionary_get_uint64(request, protocolVersionKey) != protocolVersionValue) {
NSLog(@"Received request that was not the current protocol version");
// FIXME: Cut off this connection
return;
}
auto messageType { static_cast<MessageType>(xpc_dictionary_get_uint64(request, protocolMessageTypeKey)) };
size_t dataSize { 0 };
const void* data = xpc_dictionary_get_data(request, protocolEncodedMessageKey, &dataSize);
Span<const uint8_t> encodedMessage { static_cast<const uint8_t*>(data), dataSize };
decodeAndHandleMessage(xpc_dictionary_get_remote_connection(request), messageType, encodedMessage, createReplySender(messageType, request));
}
void Daemon::connectionAdded(xpc_connection_t connection)
{
broadcastDebugMessage((JSC::MessageLevel)0, makeString("New connection: 0x", hex(reinterpret_cast<uint64_t>(connection), WTF::HexConversionMode::Lowercase)));
RELEASE_ASSERT(!m_connectionMap.contains(connection));
m_connectionMap.set(connection, ClientConnection::create(connection));
}
void Daemon::connectionRemoved(xpc_connection_t connection)
{
RELEASE_ASSERT(m_connectionMap.contains(connection));
auto clientConnection = m_connectionMap.take(connection);
clientConnection->connectionClosed();
}
CompletionHandler<void(EncodedMessage&&)> Daemon::createReplySender(MessageType messageType, OSObjectPtr<xpc_object_t>&& request)
{
if (!messageTypeSendsReply(messageType))
return nullptr;
return [request = WTFMove(request)] (EncodedMessage&& message) {
auto reply = adoptNS(xpc_dictionary_create_reply(request.get()));
ASSERT(xpc_get_type(reply.get()) == XPC_TYPE_DICTIONARY);
xpc_dictionary_set_uint64(reply.get(), protocolVersionKey, protocolVersionValue);
xpc_dictionary_set_value(reply.get(), protocolEncodedMessageKey, WebKit::vectorToXPCData(WTFMove(message)).get());
xpc_connection_send_message(xpc_dictionary_get_remote_connection(request.get()), reply.get());
};
}
void Daemon::decodeAndHandleMessage(xpc_connection_t connection, MessageType messageType, Span<const uint8_t> encodedMessage, CompletionHandler<void(EncodedMessage&&)>&& replySender)
{
ASSERT(messageTypeSendsReply(messageType) == !!replySender);
auto* clientConnection = toClientConnection(connection);
switch (messageType) {
case MessageType::EchoTwice:
handleWebPushDMessageWithReply<MessageInfo::echoTwice>(clientConnection, encodedMessage, WTFMove(replySender));
break;
case MessageType::GetOriginsWithPushAndNotificationPermissions:
handleWebPushDMessageWithReply<MessageInfo::getOriginsWithPushAndNotificationPermissions>(clientConnection, encodedMessage, WTFMove(replySender));
break;
case MessageType::DeletePushAndNotificationRegistration:
handleWebPushDMessageWithReply<MessageInfo::deletePushAndNotificationRegistration>(clientConnection, encodedMessage, WTFMove(replySender));
break;
case MessageType::RequestSystemNotificationPermission:
handleWebPushDMessageWithReply<MessageInfo::requestSystemNotificationPermission>(clientConnection, encodedMessage, WTFMove(replySender));
break;
case MessageType::SetDebugModeIsEnabled:
handleWebPushDMessage<MessageInfo::setDebugModeIsEnabled>(clientConnection, encodedMessage);
break;
case MessageType::UpdateConnectionConfiguration:
handleWebPushDMessage<MessageInfo::updateConnectionConfiguration>(clientConnection, encodedMessage);
break;
case MessageType::InjectPushMessageForTesting:
handleWebPushDMessageWithReply<MessageInfo::injectPushMessageForTesting>(clientConnection, encodedMessage, WTFMove(replySender));
break;
case MessageType::GetPendingPushMessages:
handleWebPushDMessageWithReply<MessageInfo::getPendingPushMessages>(clientConnection, encodedMessage, WTFMove(replySender));
break;
}
}
void Daemon::echoTwice(ClientConnection*, const String& message, CompletionHandler<void(const String&)>&& replySender)
{
replySender(makeString(message, message));
}
bool Daemon::canRegisterForNotifications(ClientConnection& connection)
{
if (connection.hostAppCodeSigningIdentifier().isEmpty()) {
NSLog(@"ClientConnection cannot interact with notifications: Unknown host application code signing identifier");
return false;
}
return true;
}
void Daemon::requestSystemNotificationPermission(ClientConnection* connection, const String& originString, CompletionHandler<void(bool)>&& replySender)
{
if (!canRegisterForNotifications(*connection)) {
replySender(false);
return;
}
connection->enqueueAppBundleRequest(makeUnique<AppBundlePermissionsRequest>(*connection, originString, WTFMove(replySender)));
}
void Daemon::getOriginsWithPushAndNotificationPermissions(ClientConnection* connection, CompletionHandler<void(const Vector<String>&)>&& replySender)
{
if (!canRegisterForNotifications(*connection)) {
replySender({ });
return;
}
if (connection->useMockBundlesForTesting()) {
replySender(MockAppBundleRegistry::singleton().getOriginsWithRegistrations(connection->hostAppCodeSigningIdentifier()));
return;
}
// FIXME: This will need platform-specific implementations for real world bundles once implemented.
replySender({ });
}
void Daemon::deletePushAndNotificationRegistration(ClientConnection* connection, const String& originString, CompletionHandler<void(const String&)>&& replySender)
{
if (!canRegisterForNotifications(*connection)) {
replySender("Could not delete push and notification registrations for connection: Unknown host application code signing identifier");
return;
}
connection->enqueueAppBundleRequest(makeUnique<AppBundleDeletionRequest>(*connection, originString, WTFMove(replySender)));
}
void Daemon::setDebugModeIsEnabled(ClientConnection* clientConnection, bool enabled)
{
clientConnection->setDebugModeIsEnabled(enabled);
}
void Daemon::updateConnectionConfiguration(ClientConnection* clientConnection, const WebPushDaemonConnectionConfiguration& configuration)
{
clientConnection->updateConnectionConfiguration(configuration);
}
void Daemon::injectPushMessageForTesting(ClientConnection* connection, const PushMessageForTesting& message, CompletionHandler<void(bool)>&& replySender)
{
if (!connection->hostAppHasPushInjectEntitlement()) {
connection->broadcastDebugMessage("Attempting to inject a push message from an unentitled process");
replySender(false);
return;
}
if (message.targetAppCodeSigningIdentifier.isEmpty() || !message.registrationURL.isValid()) {
connection->broadcastDebugMessage("Attempting to inject an invalid push message");
replySender(false);
return;
}
connection->broadcastDebugMessage(makeString("Injected a test push message for ", message.targetAppCodeSigningIdentifier, " at ", message.registrationURL.string()));
connection->broadcastDebugMessage(message.message);
auto addResult = m_testingPushMessages.ensure(message.targetAppCodeSigningIdentifier, [] {
return Deque<PushMessageForTesting> { };
});
addResult.iterator->value.append(message);
notifyClientPushMessageIsAvailable(message.targetAppCodeSigningIdentifier);
replySender(true);
}
void Daemon::notifyClientPushMessageIsAvailable(const String& clientCodeSigningIdentifier)
{
#if PLATFORM(MAC)
CFArrayRef urls = (__bridge CFArrayRef)@[ [NSURL URLWithString:@"webkit-app-launch://1"] ];
CFStringRef identifier = (__bridge CFStringRef)((NSString *)clientCodeSigningIdentifier);
CFDictionaryRef options = (__bridge CFDictionaryRef)@{
(id)_kLSOpenOptionPreferRunningInstanceKey: @(kLSOpenRunningInstanceBehaviorUseRunningProcess),
(id)_kLSOpenOptionActivateKey: @NO,
(id)_kLSOpenOptionAddToRecentsKey: @NO,
(id)_kLSOpenOptionBackgroundLaunchKey: @YES,
(id)_kLSOpenOptionHideKey: @YES,
};
_LSOpenURLsUsingBundleIdentifierWithCompletionHandler(urls, identifier, options, ^(LSASNRef newProcessSerialNumber, Boolean processWasLaunched, CFErrorRef cfError) { });
#else
// FIXME: Figure out equivalent iOS code here
UNUSED_PARAM(clientCodeSigningIdentifier);
#endif // PLATFORM(MAC)
}
void Daemon::getPendingPushMessages(ClientConnection* connection, CompletionHandler<void(const Vector<WebKit::WebPushMessage>&)>&& replySender)
{
auto hostAppCodeSigningIdentifier = connection->hostAppCodeSigningIdentifier();
if (hostAppCodeSigningIdentifier.isEmpty()) {
replySender({ });
return;
}
Vector<WebKit::WebPushMessage> resultMessages;
auto iterator = m_testingPushMessages.find(hostAppCodeSigningIdentifier);
if (iterator != m_testingPushMessages.end()) {
for (auto& message : iterator->value) {
auto data = message.message.utf8();
resultMessages.append(WebKit::WebPushMessage { Vector<uint8_t> { reinterpret_cast<const uint8_t*>(data.data()), data.length() }, message.registrationURL });
}
m_testingPushMessages.remove(iterator);
}
connection->broadcastDebugMessage(makeString("Fetching ", String::number(resultMessages.size()), " pending push messages"));
replySender(WTFMove(resultMessages));
}
ClientConnection* Daemon::toClientConnection(xpc_connection_t connection)
{
auto clientConnection = m_connectionMap.get(connection);
RELEASE_ASSERT(clientConnection);
return clientConnection;
}
} // namespace WebPushD