blob: fd3ce9b0fa162b0435d5bf181cf9d23500109582 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/serial/serial.h"
#include <inttypes.h>
#include <utility>
#include "base/unguessable_token.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h"
#include "third_party/blink/public/mojom/serial/serial.mojom-blink.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_serial_port_filter.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_serial_port_request_options.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/execution_context/navigator_base.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
#include "third_party/blink/renderer/modules/bluetooth/bluetooth_uuid.h"
#include "third_party/blink/renderer/modules/event_target_modules_names.h"
#include "third_party/blink/renderer/modules/serial/serial_port.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
namespace blink {
namespace {
const char kContextGone[] = "Script context has shut down.";
const char kFeaturePolicyBlocked[] =
"Access to the feature \"serial\" is disallowed by permissions policy.";
const char kNoPortSelected[] = "No port selected by the user.";
String TokenToString(const base::UnguessableToken& token) {
// TODO(crbug.com/918702): Implement HashTraits for UnguessableToken.
return String::Format("%016" PRIX64 "%016" PRIX64,
token.GetHighForSerialization(),
token.GetLowForSerialization());
}
// Carries out basic checks for the web-exposed APIs, to make sure the minimum
// requirements for them to be served are met. Returns true if any conditions
// fail to be met, generating an appropriate exception as well. Otherwise,
// returns false to indicate the call should be allowed.
bool ShouldBlockSerialServiceCall(LocalDOMWindow* window,
ExecutionContext* context,
ExceptionState* exception_state) {
if (!context) {
if (exception_state) {
exception_state->ThrowDOMException(DOMExceptionCode::kNotSupportedError,
kContextGone);
}
return true;
}
// Rejects if the top-level frame has an opaque origin.
const SecurityOrigin* security_origin = nullptr;
if (context->IsWindow()) {
security_origin =
window->GetFrame()->Top()->GetSecurityContext()->GetSecurityOrigin();
} else if (context->IsDedicatedWorkerGlobalScope()) {
security_origin = static_cast<WorkerGlobalScope*>(context)
->top_level_frame_security_origin();
} else {
NOTREACHED();
}
if (security_origin->IsOpaque()) {
if (exception_state) {
exception_state->ThrowSecurityError(
"Access to the Web Serial API is denied from contexts where the "
"top-level document has an opaque origin.");
}
return true;
}
if (!context->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kSerial,
ReportOptions::kReportOnFailure)) {
if (exception_state) {
exception_state->ThrowSecurityError(kFeaturePolicyBlocked);
}
return true;
}
return false;
}
} // namespace
const char Serial::kSupplementName[] = "Serial";
Serial* Serial::serial(NavigatorBase& navigator) {
Serial* serial = Supplement<NavigatorBase>::From<Serial>(navigator);
if (!serial) {
serial = MakeGarbageCollected<Serial>(navigator);
ProvideTo(navigator, serial);
}
return serial;
}
Serial::Serial(NavigatorBase& navigator)
: Supplement<NavigatorBase>(navigator),
ExecutionContextLifecycleObserver(navigator.GetExecutionContext()),
service_(navigator.GetExecutionContext()),
receiver_(this, navigator.GetExecutionContext()) {}
ExecutionContext* Serial::GetExecutionContext() const {
return ExecutionContextLifecycleObserver::GetExecutionContext();
}
const AtomicString& Serial::InterfaceName() const {
return event_target_names::kSerial;
}
void Serial::ContextDestroyed() {
for (auto& entry : port_cache_)
entry.value->ContextDestroyed();
}
void Serial::OnPortConnectedStateChanged(
mojom::blink::SerialPortInfoPtr port_info) {
bool connected = port_info->connected;
SerialPort* port = GetOrCreatePort(std::move(port_info));
port->set_connected(connected);
if (connected) {
port->DispatchEvent(*Event::CreateBubble(event_type_names::kConnect));
} else {
port->DispatchEvent(*Event::CreateBubble(event_type_names::kDisconnect));
}
}
ScriptPromise<IDLSequence<SerialPort>> Serial::getPorts(
ScriptState* script_state,
ExceptionState& exception_state) {
if (ShouldBlockSerialServiceCall(GetSupplementable()->DomWindow(),
GetExecutionContext(), &exception_state)) {
return ScriptPromise<IDLSequence<SerialPort>>();
}
auto* resolver =
MakeGarbageCollected<ScriptPromiseResolver<IDLSequence<SerialPort>>>(
script_state, exception_state.GetContext());
get_ports_promises_.insert(resolver);
EnsureServiceConnection();
service_->GetPorts(BindOnce(&Serial::OnGetPorts, WrapPersistent(this),
WrapPersistent(resolver)));
return resolver->Promise();
}
// static
mojom::blink::SerialPortFilterPtr Serial::CreateMojoFilter(
const SerialPortFilter* filter,
ExceptionState& exception_state) {
auto mojo_filter = mojom::blink::SerialPortFilter::New();
if (filter->hasBluetoothServiceClassId()) {
if (filter->hasUsbVendorId() || filter->hasUsbProductId()) {
exception_state.ThrowTypeError(
"A filter cannot specify both bluetoothServiceClassId and "
"usbVendorId or usbProductId.");
return nullptr;
}
mojo_filter->bluetooth_service_class_id =
GetBluetoothUUIDFromV8Value(filter->bluetoothServiceClassId());
if (mojo_filter->bluetooth_service_class_id.empty()) {
exception_state.ThrowTypeError(
"Invalid Bluetooth service class ID filter value.");
return nullptr;
}
return mojo_filter;
}
mojo_filter->has_product_id = filter->hasUsbProductId();
mojo_filter->has_vendor_id = filter->hasUsbVendorId();
if (mojo_filter->has_product_id) {
if (!mojo_filter->has_vendor_id) {
exception_state.ThrowTypeError(
"A filter containing a usbProductId must also specify a "
"usbVendorId.");
return nullptr;
}
mojo_filter->product_id = filter->usbProductId();
}
if (mojo_filter->has_vendor_id) {
mojo_filter->vendor_id = filter->usbVendorId();
} else {
exception_state.ThrowTypeError(
"A filter must provide a property to filter by.");
return nullptr;
}
return mojo_filter;
}
ScriptPromise<SerialPort> Serial::requestPort(
ScriptState* script_state,
const SerialPortRequestOptions* options,
ExceptionState& exception_state) {
if (ShouldBlockSerialServiceCall(GetSupplementable()->DomWindow(),
GetExecutionContext(), &exception_state)) {
return EmptyPromise();
}
if (!LocalFrame::HasTransientUserActivation(DomWindow()->GetFrame())) {
exception_state.ThrowSecurityError(
"Must be handling a user gesture to show a permission request.");
return EmptyPromise();
}
Vector<mojom::blink::SerialPortFilterPtr> filters;
if (options && options->hasFilters()) {
for (const auto& filter : options->filters()) {
auto mojo_filter = CreateMojoFilter(filter, exception_state);
if (!mojo_filter) {
CHECK(exception_state.HadException());
return EmptyPromise();
}
CHECK(!exception_state.HadException());
filters.push_back(std::move(mojo_filter));
}
}
Vector<String> allowed_bluetooth_service_class_ids;
if (options && options->hasAllowedBluetoothServiceClassIds()) {
for (const auto& id : options->allowedBluetoothServiceClassIds()) {
allowed_bluetooth_service_class_ids.push_back(
GetBluetoothUUIDFromV8Value(id));
}
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<SerialPort>>(
script_state, exception_state.GetContext());
request_port_promises_.insert(resolver);
EnsureServiceConnection();
service_->RequestPort(std::move(filters),
std::move(allowed_bluetooth_service_class_ids),
resolver->WrapCallbackInScriptScope(BindOnce(
&Serial::OnRequestPort, WrapPersistent(this))));
return resolver->Promise();
}
void Serial::OpenPort(
const base::UnguessableToken& token,
device::mojom::blink::SerialConnectionOptionsPtr options,
mojo::PendingRemote<device::mojom::blink::SerialPortClient> client,
mojom::blink::SerialService::OpenPortCallback callback) {
EnsureServiceConnection();
service_->OpenPort(token, std::move(options), std::move(client),
std::move(callback));
}
void Serial::ForgetPort(
const base::UnguessableToken& token,
mojom::blink::SerialService::ForgetPortCallback callback) {
EnsureServiceConnection();
service_->ForgetPort(token, std::move(callback));
}
void Serial::Trace(Visitor* visitor) const {
visitor->Trace(service_);
visitor->Trace(receiver_);
visitor->Trace(get_ports_promises_);
visitor->Trace(request_port_promises_);
visitor->Trace(port_cache_);
EventTarget::Trace(visitor);
Supplement<NavigatorBase>::Trace(visitor);
ExecutionContextLifecycleObserver::Trace(visitor);
}
void Serial::AddedEventListener(const AtomicString& event_type,
RegisteredEventListener& listener) {
EventTarget::AddedEventListener(event_type, listener);
if (event_type != event_type_names::kConnect &&
event_type != event_type_names::kDisconnect) {
return;
}
if (ShouldBlockSerialServiceCall(GetSupplementable()->DomWindow(),
GetExecutionContext(), nullptr)) {
return;
}
EnsureServiceConnection();
}
void Serial::EnsureServiceConnection() {
DCHECK(GetExecutionContext());
if (service_.is_bound())
return;
auto task_runner =
GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI);
GetExecutionContext()->GetBrowserInterfaceBroker().GetInterface(
service_.BindNewPipeAndPassReceiver(task_runner));
service_.set_disconnect_handler(
BindOnce(&Serial::OnServiceConnectionError, WrapWeakPersistent(this)));
service_->SetClient(receiver_.BindNewPipeAndPassRemote(task_runner));
}
void Serial::OnServiceConnectionError() {
service_.reset();
receiver_.reset();
// Script may execute during a call to Resolve(). Swap these sets to prevent
// concurrent modification.
HeapHashSet<Member<ScriptPromiseResolver<IDLSequence<SerialPort>>>>
get_ports_promises;
get_ports_promises_.swap(get_ports_promises);
for (auto& resolver : get_ports_promises) {
resolver->Resolve(HeapVector<Member<SerialPort>>());
}
HeapHashSet<Member<ScriptPromiseResolverBase>> request_port_promises;
request_port_promises_.swap(request_port_promises);
for (ScriptPromiseResolverBase* resolver : request_port_promises) {
ScriptState* resolver_script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
resolver_script_state)) {
continue;
}
ScriptState::Scope script_state_scope(resolver_script_state);
resolver->RejectWithDOMException(DOMExceptionCode::kNotFoundError,
kNoPortSelected);
}
}
SerialPort* Serial::GetOrCreatePort(mojom::blink::SerialPortInfoPtr info) {
auto it = port_cache_.find(TokenToString(info->token));
if (it != port_cache_.end()) {
return it->value.Get();
}
SerialPort* port = MakeGarbageCollected<SerialPort>(this, std::move(info));
port_cache_.insert(TokenToString(port->token()), port);
return port;
}
void Serial::OnGetPorts(
ScriptPromiseResolver<IDLSequence<SerialPort>>* resolver,
Vector<mojom::blink::SerialPortInfoPtr> port_infos) {
DCHECK(get_ports_promises_.Contains(resolver));
get_ports_promises_.erase(resolver);
HeapVector<Member<SerialPort>> ports;
for (auto& port_info : port_infos)
ports.push_back(GetOrCreatePort(std::move(port_info)));
resolver->Resolve(ports);
}
void Serial::OnRequestPort(ScriptPromiseResolver<SerialPort>* resolver,
mojom::blink::SerialPortInfoPtr port_info) {
DCHECK(request_port_promises_.Contains(resolver));
request_port_promises_.erase(resolver);
if (!port_info) {
resolver->RejectWithDOMException(DOMExceptionCode::kNotFoundError,
kNoPortSelected);
return;
}
resolver->Resolve(GetOrCreatePort(std::move(port_info)));
}
} // namespace blink