331 lines
10 KiB
C++
331 lines
10 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "SerialPermissionRequest.h"
|
|
|
|
#include "SerialLogging.h"
|
|
#include "js/PropertyAndElement.h"
|
|
#include "js/Wrapper.h"
|
|
#include "mozilla/BasePrincipal.h"
|
|
#include "mozilla/JSONStringWriteFuncs.h"
|
|
#include "mozilla/JSONWriter.h"
|
|
#include "mozilla/RandomNum.h"
|
|
#include "mozilla/StaticPrefs_dom.h"
|
|
#include "mozilla/dom/Element.h"
|
|
#include "mozilla/dom/RootedDictionary.h"
|
|
#include "mozilla/dom/ScriptSettings.h"
|
|
#include "mozilla/dom/WindowGlobalParent.h"
|
|
#include "nsContentPermissionHelper.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsIArray.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "xpcpublic.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
constexpr uint32_t kPermissionDeniedBaseDelayMs = 3000;
|
|
constexpr uint32_t kPermissionDeniedMaxRandomDelayMs = 10000;
|
|
|
|
NS_IMPL_ISUPPORTS(SerialPermissionRequest, nsIContentPermissionRequest)
|
|
|
|
SerialPermissionRequest::SerialPermissionRequest(
|
|
WindowGlobalParent* aWindowGlobalParent, bool aAutoselect,
|
|
nsTArray<IPCSerialPortInfo>&& aPorts)
|
|
: mWindowGlobalParent(aWindowGlobalParent),
|
|
mAutoselect(aAutoselect),
|
|
mPorts(std::move(aPorts)) {
|
|
MOZ_ASSERT(mWindowGlobalParent);
|
|
MOZ_ASSERT(mWindowGlobalParent->DocumentPrincipal());
|
|
}
|
|
|
|
nsIPrincipal* SerialPermissionRequest::Principal() const {
|
|
return mWindowGlobalParent->DocumentPrincipal();
|
|
}
|
|
|
|
SerialPermissionRequest::~SerialPermissionRequest() {
|
|
// Defensive: if no path settled the promise, fall back to InternalError so
|
|
// the consumer always sees a decision.
|
|
mPromiseHolder.RejectIfExists(RequestPortReason::InternalError, __func__);
|
|
}
|
|
|
|
bool SerialPermissionRequest::IsSitePermAllow() const {
|
|
return nsContentUtils::IsSitePermAllow(Principal(), "serial"_ns);
|
|
}
|
|
|
|
bool SerialPermissionRequest::IsSitePermDeny() const {
|
|
return nsContentUtils::IsSitePermDeny(Principal(), "serial"_ns);
|
|
}
|
|
|
|
bool SerialPermissionRequest::ShouldShowAddonGate() const {
|
|
nsIPrincipal* principal = Principal();
|
|
return StaticPrefs::dom_webserial_gated() &&
|
|
StaticPrefs::dom_sitepermsaddon_provider_enabled() &&
|
|
!IsSitePermAllow() && !principal->GetIsLoopbackHost() &&
|
|
!principal->SchemeIs("file");
|
|
}
|
|
|
|
RefPtr<SerialChooserPromise> SerialPermissionRequest::Run() {
|
|
AssertIsOnMainThread();
|
|
|
|
RefPtr<SerialChooserPromise> promise = mPromiseHolder.Ensure(__func__);
|
|
|
|
if (!IsSitePermAllow()) {
|
|
if (IsSitePermDeny()) {
|
|
CancelWithRandomizedDelay(RequestPortReason::AddonDenied);
|
|
return promise;
|
|
}
|
|
if (nsContentUtils::IsSitePermDeny(Principal(), "install"_ns) &&
|
|
StaticPrefs::dom_sitepermsaddon_provider_enabled() &&
|
|
!Principal()->GetIsLoopbackHost()) {
|
|
CancelWithRandomizedDelay(RequestPortReason::AddonDenied);
|
|
return promise;
|
|
}
|
|
}
|
|
|
|
// Testing-only autoselect bypass: when the testing pref is on and gated
|
|
// mode is off, we skip the prompt entirely and pick the first port. The
|
|
// bypass is only honored when testing is enabled, so a malicious content
|
|
// process cannot use the autoselect flag to skip the chooser in release.
|
|
if (mAutoselect && StaticPrefs::dom_webserial_testing_enabled() &&
|
|
!StaticPrefs::dom_webserial_gated()) {
|
|
if (mPorts.IsEmpty()) {
|
|
ResolveCancelled(RequestPortReason::UserCancelled);
|
|
return promise;
|
|
}
|
|
ResolveWithPort(mPorts[0]);
|
|
return promise;
|
|
}
|
|
|
|
if (NS_FAILED(DoPrompt())) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
}
|
|
return promise;
|
|
}
|
|
|
|
void SerialPermissionRequest::CancelWithRandomizedDelay(
|
|
RequestPortReason aReason) {
|
|
AssertIsOnMainThread();
|
|
|
|
uint32_t randomDelayMS =
|
|
xpc::IsInAutomation()
|
|
? 0
|
|
: RandomUint64OrDie() % kPermissionDeniedMaxRandomDelayMs;
|
|
auto delay = TimeDuration::FromMilliseconds(kPermissionDeniedBaseDelayMs +
|
|
randomDelayMS);
|
|
NS_NewTimerWithCallback(
|
|
getter_AddRefs(mCancelTimer),
|
|
[self = RefPtr{this}, aReason](auto) { self->ResolveCancelled(aReason); },
|
|
delay, nsITimer::TYPE_ONE_SHOT,
|
|
"SerialPermissionRequest::CancelWithRandomizedDelay"_ns);
|
|
}
|
|
|
|
nsresult SerialPermissionRequest::DoPrompt() {
|
|
return nsContentPermissionUtils::AskPermission(this, /* aWindow */ nullptr);
|
|
}
|
|
|
|
void SerialPermissionRequest::ResolveWithPort(const IPCSerialPortInfo& aPort) {
|
|
mCancelTimer = nullptr;
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
(" friendlyName: %s",
|
|
NS_ConvertUTF16toUTF8(aPort.friendlyName()).get()));
|
|
if (aPort.usbVendorId().isSome()) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
(" usbVendorId: 0x%04x", aPort.usbVendorId().value()));
|
|
}
|
|
if (aPort.usbProductId().isSome()) {
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
(" usbProductId: 0x%04x", aPort.usbProductId().value()));
|
|
}
|
|
|
|
mPromiseHolder.ResolveIfExists(aPort, __func__);
|
|
}
|
|
|
|
void SerialPermissionRequest::ResolveCancelled(RequestPortReason aReason) {
|
|
mCancelTimer = nullptr;
|
|
mPromiseHolder.RejectIfExists(aReason, __func__);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetTypes(nsIArray** aTypes) {
|
|
NS_ENSURE_ARG_POINTER(aTypes);
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
("SerialPermissionRequest::GetTypes called with %zu ports",
|
|
mPorts.Length()));
|
|
|
|
nsTArray<nsString> options;
|
|
|
|
// Pass the autoselect flag to the JS UI so that it can auto-confirm the
|
|
// chooser when testing+gated is in use.
|
|
if (mAutoselect && StaticPrefs::dom_webserial_testing_enabled()) {
|
|
options.AppendElement(u"{\"__autoselect__\":true}"_ns);
|
|
}
|
|
|
|
size_t index = 0;
|
|
for (const auto& port : mPorts) {
|
|
nsCString utf8Json;
|
|
JSONStringRefWriteFunc writeFunc(utf8Json);
|
|
JSONWriter writer(writeFunc, JSONWriter::SingleLineStyle);
|
|
|
|
NS_ConvertUTF16toUTF8 utf8Path(port.path());
|
|
NS_ConvertUTF16toUTF8 utf8FriendlyName(port.friendlyName());
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Debug,
|
|
(" Port[%zu]: path=%s, friendlyName=%s, vid=0x%04x, pid=0x%04x",
|
|
index, utf8Path.get(), utf8FriendlyName.get(),
|
|
port.usbVendorId().valueOr(0), port.usbProductId().valueOr(0)));
|
|
|
|
writer.StartObjectElement();
|
|
writer.StringProperty("path", MakeStringSpan(utf8Path.get()));
|
|
writer.StringProperty("friendlyName",
|
|
MakeStringSpan(utf8FriendlyName.get()));
|
|
writer.IntProperty("usbVendorId", port.usbVendorId().valueOr(0));
|
|
writer.IntProperty("usbProductId", port.usbProductId().valueOr(0));
|
|
writer.EndObject();
|
|
|
|
nsString utf16Json;
|
|
CopyUTF8toUTF16(utf8Json, utf16Json);
|
|
options.AppendElement(std::move(utf16Json));
|
|
++index;
|
|
}
|
|
|
|
return nsContentPermissionUtils::CreatePermissionArray("serial"_ns, options,
|
|
aTypes);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetPrincipal(nsIPrincipal** aPrincipal) {
|
|
NS_IF_ADDREF(*aPrincipal = Principal());
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetTopLevelPrincipal(nsIPrincipal** aPrincipal) {
|
|
NS_IF_ADDREF(*aPrincipal = mWindowGlobalParent->TopWindowContext()
|
|
->DocumentPrincipal());
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetDelegatePrincipal(const nsACString& aType,
|
|
nsIPrincipal** aPrincipal) {
|
|
return PermissionDelegateHandler::GetDelegatePrincipal(aType, this,
|
|
aPrincipal);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetWindow(mozIDOMWindow** aWindow) {
|
|
*aWindow = nullptr;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetElement(Element** aElement) {
|
|
NS_ENSURE_ARG_POINTER(aElement);
|
|
RefPtr<Element> element = mWindowGlobalParent->GetRootOwnerElement();
|
|
element.forget(aElement);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetHasValidTransientUserGestureActivation(bool* aOut) {
|
|
// The content process already rejects requestPort() without transient
|
|
// user activation (Serial::RequestPort step 3), so the parent-side request
|
|
// can report true without re-querying WindowContext (whose transient-
|
|
// activation state is only tracked in-process and would assert here).
|
|
*aOut = true;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetIsRequestDelegatedToUnsafeThirdParty(bool* aOut) {
|
|
// The parent process does not have a permission delegate handler available
|
|
// (that lives on the document) so we conservatively report false.
|
|
*aOut = false;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::GetIgnoreAllowSitePermission(bool* aOut) {
|
|
*aOut = false;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::NotifyShown() { return NS_OK; }
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::Cancel() {
|
|
AssertIsOnMainThread();
|
|
mCancelTimer = nullptr;
|
|
|
|
RequestPortReason reason = RequestPortReason::UserCancelled;
|
|
if (ShouldShowAddonGate()) {
|
|
reason = RequestPortReason::AddonDenied;
|
|
}
|
|
|
|
ResolveCancelled(reason);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
SerialPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) {
|
|
AssertIsOnMainThread();
|
|
mCancelTimer = nullptr;
|
|
|
|
if (!aChoices.isObject()) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
return NS_OK;
|
|
}
|
|
|
|
// The choices object can come in any realm — chrome calls Allow() from JS.
|
|
// Unwrap and enter its realm before reading properties.
|
|
JS::Rooted<JSObject*> obj(RootingCx(), &aChoices.toObject());
|
|
obj = js::CheckedUnwrapStatic(obj);
|
|
if (!obj) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsIGlobalObject* global = xpc::NativeGlobal(obj);
|
|
if (!global) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
return NS_OK;
|
|
}
|
|
|
|
AutoJSAPI jsapi;
|
|
if (!jsapi.Init(global)) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
return NS_OK;
|
|
}
|
|
JSContext* cx = jsapi.cx();
|
|
|
|
JS::Rooted<JS::Value> serialVal(cx);
|
|
if (!JS_GetProperty(cx, obj, "serial", &serialVal) || !serialVal.isString()) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoJSString choice;
|
|
if (!choice.init(cx, serialVal)) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult rv;
|
|
int32_t selectedIndex = choice.ToInteger(&rv);
|
|
if (NS_FAILED(rv) || selectedIndex < 0 ||
|
|
static_cast<size_t>(selectedIndex) >= mPorts.Length()) {
|
|
ResolveCancelled(RequestPortReason::InternalError);
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_LOG(gWebSerialLog, LogLevel::Info,
|
|
(" Selected port at index %d:", selectedIndex));
|
|
ResolveWithPort(mPorts[selectedIndex]);
|
|
return NS_OK;
|
|
}
|
|
|
|
} // namespace mozilla::dom
|