blob: 13a861fdd6e1a6d1de2c5e14be060ac6866dd24a [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.
*/
#include "config.h"
#include "PushManager.h"
#if ENABLE(SERVICE_WORKER)
#include "DOMWindow.h"
#include "DocumentInlines.h"
#include "EventLoop.h"
#include "Exception.h"
#include "JSPushPermissionState.h"
#include "JSPushSubscription.h"
#include "Logging.h"
#include "NotificationClient.h"
#include "PushCrypto.h"
#include "ScriptExecutionContext.h"
#include "ServiceWorkerRegistration.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/Vector.h>
#include <wtf/text/Base64.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(PushManager);
PushManager::PushManager(ServiceWorkerRegistration& serviceWorkerRegistration)
: m_serviceWorkerRegistration(serviceWorkerRegistration)
{
}
PushManager::~PushManager() = default;
Vector<String> PushManager::supportedContentEncodings()
{
return Vector<String> { "aesgcm"_s, "aes128gcm"_s };
}
void PushManager::ref() const
{
m_serviceWorkerRegistration.ref();
}
void PushManager::deref() const
{
m_serviceWorkerRegistration.deref();
}
void PushManager::subscribe(ScriptExecutionContext& context, std::optional<PushSubscriptionOptionsInit>&& options, DOMPromiseDeferred<IDLInterface<PushSubscription>>&& promise)
{
RELEASE_ASSERT(context.isSecureContext());
context.eventLoop().queueTask(TaskSource::Networking, [this, protectedThis = Ref { *this }, context = Ref { context }, options = WTFMove(options), promise = WTFMove(promise)]() mutable {
if (!options || !options->userVisibleOnly) {
promise.reject(Exception { NotAllowedError, "Subscribing for push requires userVisibleOnly to be true"_s });
return;
}
if (!options || !options->applicationServerKey) {
promise.reject(Exception { NotSupportedError, "Subscribing for push requires an applicationServerKey"_s });
return;
}
using KeyDataResult = ExceptionOr<Vector<uint8_t>>;
auto keyDataResult = WTF::switchOn(*options->applicationServerKey, [](RefPtr<JSC::ArrayBuffer>& value) -> KeyDataResult {
if (!value)
return Vector<uint8_t> { };
return Vector<uint8_t> { reinterpret_cast<const uint8_t*>(value->data()), value->byteLength() };
}, [](RefPtr<JSC::ArrayBufferView>& value) -> KeyDataResult {
if (!value)
return Vector<uint8_t> { };
return Vector<uint8_t> { reinterpret_cast<const uint8_t*>(value->baseAddress()), value->byteLength() };
}, [](String& value) -> KeyDataResult {
auto decoded = base64URLDecode(value);
if (!decoded)
return Exception { InvalidCharacterError, "applicationServerKey is not properly base64url-encoded"_s };
return WTFMove(decoded.value());
});
if (keyDataResult.hasException()) {
promise.reject(keyDataResult.releaseException());
return;
}
if (!PushCrypto::validateP256DHPublicKey(keyDataResult.returnValue())) {
promise.reject(Exception { InvalidAccessError, "applicationServerKey must contain a valid P-256 public key"_s });
return;
}
if (!m_serviceWorkerRegistration.active()) {
promise.reject(Exception { InvalidStateError, "Subscribing for push requires an active service worker"_s });
return;
}
auto client = context->notificationClient();
auto permission = client ? client->checkPermission(context.ptr()) : NotificationPermission::Denied;
if (permission == NotificationPermission::Denied) {
promise.reject(Exception { NotAllowedError, "User denied push permission"_s });
return;
}
if (permission == NotificationPermission::Default && !context->isDocument()) {
promise.reject(Exception { NotAllowedError, "User denied push permission"_s });
return;
}
if (permission == NotificationPermission::Default) {
RELEASE_ASSERT(client);
RELEASE_ASSERT(context->isDocument());
auto& document = downcast<Document>(context.get());
if (!document.isSameOriginAsTopDocument()) {
promise.reject(Exception { NotAllowedError, "Cannot request permission from cross-origin iframe"_s });
return;
}
auto* window = document.frame() ? document.frame()->window() : nullptr;
if (!window || !window->consumeTransientActivation()) {
Seconds lastActivationDuration = window ? MonotonicTime::now() - window->lastActivationTimestamp() : Seconds::infinity();
RELEASE_LOG_ERROR(Push, "Failing PushManager.subscribe call due to failed transient activation check; last activated %.2f sec ago", lastActivationDuration.value());
auto errorMessage = "Push notification prompting can only be done from a user gesture."_s;
document.addConsoleMessage(MessageSource::Security, MessageLevel::Error, errorMessage);
promise.reject(Exception { NotAllowedError, errorMessage });
return;
}
client->requestPermission(context, [this, protectedThis = WTFMove(protectedThis), keyData = keyDataResult.releaseReturnValue(), promise = WTFMove(promise)](auto permission) mutable {
if (permission != NotificationPermission::Granted) {
promise.reject(Exception { NotAllowedError, "User denied push permission"_s });
return;
}
m_serviceWorkerRegistration.subscribeToPushService(WTFMove(keyData), WTFMove(promise));
});
return;
}
RELEASE_ASSERT(permission == NotificationPermission::Granted);
m_serviceWorkerRegistration.subscribeToPushService(keyDataResult.releaseReturnValue(), WTFMove(promise));
});
}
void PushManager::getSubscription(ScriptExecutionContext& context, DOMPromiseDeferred<IDLNullable<IDLInterface<PushSubscription>>>&& promise)
{
context.eventLoop().queueTask(TaskSource::Networking, [this, protectedThis = Ref { *this }, promise = WTFMove(promise)]() mutable {
m_serviceWorkerRegistration.getPushSubscription(WTFMove(promise));
});
}
void PushManager::permissionState(ScriptExecutionContext& context, std::optional<PushSubscriptionOptionsInit>&&, DOMPromiseDeferred<IDLEnumeration<PushPermissionState>>&& promise)
{
context.eventLoop().queueTask(TaskSource::Networking, [context = Ref { context }, promise = WTFMove(promise)]() mutable {
auto client = context->notificationClient();
auto permission = client ? client->checkPermission(context.ptr()) : NotificationPermission::Denied;
switch (permission) {
case NotificationPermission::Default:
promise.resolve(PushPermissionState::Prompt);
break;
case NotificationPermission::Granted:
promise.resolve(PushPermissionState::Granted);
break;
default:
promise.resolve(PushPermissionState::Denied);
break;
}
});
}
} // namespace WebCore
#endif // ENABLE(SERVICE_WORKER)