mirror of
https://github.com/electron/electron.git
synced 2026-10-29 04:04:54 +08:00
* chore: bump chromium in DEPS to 156.0.8073.0 * 8409748: Update RenderWidgetHostViewBase::host_ lifetime Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8409748 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8410048: [dcheck-to-check] Convert DCHECKs in content/browser/ [34/n] Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8410048 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8401889: [Win] Read GetLastError() right after CreateFile() in ProcessSingleton Upstream now reads GetLastError() inside the TRACE_EVENT0 scope, which is the same fix this patch carried, so drop that hunk. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8401889 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8293899: Android: Enable ThemeService for WebUI NTP Upstream moved the //chrome/browser/themes dependency from a per-platform conditional into the unconditional deps of the mixers target, so the patch now removes it from that list instead. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8293899 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8412026: [mac] Support video capture lock on macOS Keep the compositor unsuspended when the window is headless, visible, or has an active video capture lock. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8412026 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8404074: [posix] Don't call SetNonBlocking() inside DCHECK() in ProcessSingleton Upstream made the same change, so drop the hunk from this patch. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8404074 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8421647: media: Move GPU/graphics client utilities to media/gfx/ Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8421647 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8361191: [api] Add MicrotaskQueue::SetMicrotasksPolicy()/GetMicrotasksPolicy() V8 now exposes the per-queue microtasks policy on v8::MicrotaskQueue, so remove the patch that added it and switch node_util.cc to the upstream SetMicrotasksPolicy()/GetMicrotasksPolicy() names. Ref: https://chromium-review.googlesource.com/c/v8/v8/+/8361191 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: remove upstreamed patches Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8455783 Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8455325 Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8431509 Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8402403 Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8402402 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: update patches * chore: update siso revision * 8426326: Linux: Rename DarkModeManagerLinux to PortalSettingsLinux Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8426326 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8278613: Plumb navigation IDs to MIME handler streams Pass the navigation ID through StreamsPrivateAPI::SendExecuteMimeTypeHandlerEvent to the MIME handler stream manager, and only create the plugin response interceptor throttle for navigation requests, matching the upstream ChromeContentBrowserClient change. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8278613 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8403343: Bump clang revision The new clang package ships the raw_ptr plugin change from crrev.com/c/8358382, after which [chromium-containers] rejects T* template arguments in std::list, std::set and std::vector fields. Store raw_ptr<T> in those Electron containers instead. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8403343 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8362297: viz: Add `is_secure` flag to CopyOutputRequest and friends The new is_secure member brings VideoCaptureParams, together with the use_native_picker field this patch adds, over the chromium-style complexity threshold, so the implicit inline copy constructor is now rejected. Declare out-of-line copy and move constructors and assignment operators for the struct in the patch. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8362297 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8442576: Remove id from ChildProcessData Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8442576 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8421383: [RWS] Remove unused RWS cookie exclusion reason Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8421383 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8240331: Support targeting a network in NetworkService's WebSockets Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8240331 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8426064: Remove flag for restrict frame destinations to navigate The frame-destination check is now unconditional upstream, so drop the feature flag check in net.request. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8426064 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8292633: [atmemory] Add `blink::mojom::ScrollType` to the Autofill scroll callstack Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8292633 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8405125: FSA: rename UserAction to AccessTrigger and add programmatic triggers The former UserAction::kNone case is now split into AccessTrigger::kProgrammaticRead and kProgrammaticWrite; both keep the previous behavior of not granting access automatically and of replying to sensitive entry checks without prompting. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8405125 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8399046: Add chrome:// host and path mapping for component extensions ExtensionConfigMap::GetConfigProvider() now takes an Extension; use the new GetConfigProviderByExtensionId() for the id-based lookup. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8399046 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8445301: [Linux] Decouple //components/dbus from //ui/base and //net The //components/dbus component no longer depends on //components/dbus/menu, so depend on it directly for status_icon_linux_dbus.cc. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8445301 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8360557: [Extensions] Make generated features immutable FeatureProvider::AddFeature() no longer replaces an existing feature, so the entries Electron's feature files used to override core ones now fail a CHECK at startup. Drop the ones that duplicate the core definitions, or differ only in ChromeOS-only branches. Keep chrome.webstorePrivate unavailable by moving its delegated availability check into the core definition with a patch. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8360557 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 8427384: Generate CDP TypeScript definitions directly from in-tree PDL files Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8427384 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fixup! 8403343: Bump clang revision * chore: update patches Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 8397345: Move titlebar_config.h to chrome/browser/win The move switched the color mixers' Windows dependency from the narrow titlebar_config target to all of //chrome/browser/win, although the mixer uses nothing from it. On Windows that pulls //chrome/browser/profiles into Electron's link, where its ProfileKeyedServiceFactory collides with the one Electron provides for the spellchecker. Patch the unused dependency out. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8397345 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 8403320: [WebUI] Disallow devtools:// scheme for WebUI data sources by default URL data sources are no longer served to devtools:// unless they opt in, so DevTools failed to load its frontend. Opt Electron's bundle data source in, and let its theme data source serve colors.css to devtools:// as Chrome's ThemeSource does. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8403320 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 8394377: Migrate RefCountedDeleteOnSequence to start-from-one ref counting (4/4) Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8394377 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: bump chromium in DEPS to 156.0.8075.0 * chore: update patches Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com> * 8467669: [zoom] Key host-less zoom levels on the URL without its fragment Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8467669 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update patches Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: bump chromium in DEPS to 156.0.8076.0 * 8468407: [gtk] Don't leave GDK_BACKEND set in the environment after GTK init Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8468407 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8457646: [snapshot] Include embedder string in version hash Upstream moved Version::Hash() out of line and now builds it with base::Hasher over the version number and embedder string. Fold the v8_version_hash_salt into that hasher the same way instead of carrying a separate FNV-1a helper and hash_combine() call. Ref: https://chromium-review.googlesource.com/c/v8/v8/+/8457646 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: update patches * 8459447: gin: Mark the stack-only helper classes STACK_ALLOCATED() gin::Dictionary is now STACK_ALLOCATED(), and the find-bad-constructs plugin conservatively treats any template instantiated with a stack-allocated type argument as stack-allocated too. Mark the two gin_helper::Promise<gin_helper::Dictionary> fields with STACK_ALLOCATED_IGNORE, since Promise<T> only names T as its resolve type and never stores one. Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8459447 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * 8424587: mojo: Remove unused WrapPlatformFile and UnwrapPlatformFile helpers Ref: https://chromium-review.googlesource.com/c/chromium/src/+/8424587 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fixup! 8403343: Bump clang revision notifications_ is now a std::set<raw_ptr<Notification>>, and libc++ has no heterogeneous set::erase, so erasing by key after Dismiss() built a temporary raw_ptr from the notification. On macOS Dismiss() has already destroyed it by then, so that took a BackupRefPtr reference on freed memory. Find it with the transparent comparator, which compares addresses without constructing a raw_ptr, and erase by iterator. Also drop NotifyIconHost::NotifyIcons, unused since notify_icons_ became a std::vector<raw_ptr<NotifyIcon>>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fixup! 8426064: Remove flag for restrict frame destinations to navigate Drop the network features include; its only use was the kRestrictFrameDestinationsToNavigate check this CL removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com> Co-authored-by: electron-claude[bot] <279139701+electron-claude[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: clavin <clavin@electronjs.org> Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
564 lines
20 KiB
C++
564 lines
20 KiB
C++
// Copyright (c) 2026 Anthropic, PBC.
|
|
// Use of this source code is governed by the MIT license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "shell/browser/api/electron_api_web_socket.h"
|
|
|
|
#include <limits>
|
|
#include <utility>
|
|
|
|
#include "base/containers/span.h"
|
|
#include "base/functional/bind.h"
|
|
#include "base/functional/callback_helpers.h"
|
|
#include "base/strings/strcat.h"
|
|
#include "base/task/sequenced_task_runner.h"
|
|
#include "content/public/browser/storage_partition.h"
|
|
#include "gin/object_template_builder.h"
|
|
#include "gin/persistent.h"
|
|
#include "net/base/isolation_info.h"
|
|
#include "net/base/net_errors.h"
|
|
#include "net/http/http_util.h"
|
|
#include "net/storage_access_api/status.h"
|
|
#include "net/traffic_annotation/network_traffic_annotation.h"
|
|
#include "services/network/public/cpp/constants.h"
|
|
#include "services/network/public/cpp/originating_process_id.h"
|
|
#include "services/network/public/mojom/network_context.mojom.h"
|
|
#include "shell/browser/api/electron_api_session.h"
|
|
#include "shell/browser/electron_browser_context.h"
|
|
#include "shell/browser/javascript_environment.h"
|
|
#include "shell/common/gin_converters/gurl_converter.h"
|
|
#include "shell/common/gin_converters/net_converter.h"
|
|
#include "shell/common/gin_helper/dictionary.h"
|
|
#include "shell/common/gin_helper/object_template_builder.h"
|
|
#include "shell/common/gin_helper/wrappable_pointer_tags.h"
|
|
#include "shell/common/node_util.h"
|
|
#include "shell/common/process_util.h"
|
|
#include "shell/common/v8_util.h"
|
|
#include "url/url_constants.h"
|
|
#include "v8/include/cppgc/allocation.h"
|
|
#include "v8/include/v8-cppgc.h"
|
|
#include "v8/include/v8.h"
|
|
|
|
namespace electron::api {
|
|
|
|
namespace {
|
|
|
|
constexpr net::NetworkTrafficAnnotationTag kWebSocketTrafficAnnotation =
|
|
net::DefineNetworkTrafficAnnotation("electron_net_web_socket", R"(
|
|
semantics {
|
|
sender: "Electron Net module"
|
|
description:
|
|
"Establish a WebSocket connection from the main process using "
|
|
"Chromium's native networking library."
|
|
trigger: "Using net.WebSocket"
|
|
data: "Anything the user wants to send."
|
|
destination: OTHER
|
|
}
|
|
policy {
|
|
cookies_allowed: YES
|
|
cookies_store: "user"
|
|
setting: "This feature cannot be disabled."
|
|
})");
|
|
|
|
} // namespace
|
|
|
|
const gin::WrapperInfo WebSocketWrapper::kWrapperInfo =
|
|
electron::MakeWrapperInfo(electron::kElectronWebSocket);
|
|
|
|
WebSocketWrapper::PendingMessage::PendingMessage(
|
|
network::mojom::WebSocketMessageType type,
|
|
std::vector<uint8_t> data)
|
|
: type(type), data(std::move(data)) {}
|
|
WebSocketWrapper::PendingMessage::PendingMessage(PendingMessage&&) = default;
|
|
WebSocketWrapper::PendingMessage& WebSocketWrapper::PendingMessage::operator=(
|
|
PendingMessage&&) = default;
|
|
WebSocketWrapper::PendingMessage::~PendingMessage() = default;
|
|
|
|
WebSocketWrapper::WebSocketWrapper(
|
|
ElectronBrowserContext* browser_context,
|
|
const GURL& url,
|
|
std::vector<std::string> protocols,
|
|
std::vector<network::mojom::HttpHeaderPtr> headers,
|
|
const url::Origin& origin,
|
|
bool use_session_cookies)
|
|
: browser_context_(browser_context),
|
|
url_(url),
|
|
requested_protocols_(std::move(protocols)),
|
|
additional_headers_(std::move(headers)),
|
|
origin_(origin),
|
|
use_session_cookies_(use_session_cookies),
|
|
readable_watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL),
|
|
writable_watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL) {
|
|
DETACH_FROM_SEQUENCE(sequence_checker_);
|
|
}
|
|
|
|
WebSocketWrapper::~WebSocketWrapper() = default;
|
|
|
|
// static
|
|
WebSocketWrapper* WebSocketWrapper::Create(gin::Arguments* args) {
|
|
if (!electron::IsBrowserProcess()) {
|
|
args->ThrowTypeError("net.WebSocket is only available in the main process");
|
|
return nullptr;
|
|
}
|
|
|
|
gin_helper::Dictionary opts;
|
|
if (!args->GetNext(&opts)) {
|
|
args->ThrowTypeError("Expected an options object");
|
|
return nullptr;
|
|
}
|
|
|
|
GURL url;
|
|
if (!opts.Get("url", &url) || !url.is_valid() ||
|
|
(!url.SchemeIs("ws") && !url.SchemeIs("wss"))) {
|
|
args->ThrowTypeError("Expected a valid 'ws:' or 'wss:' URL");
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<std::string> protocols;
|
|
opts.Get("protocols", &protocols);
|
|
|
|
std::vector<network::mojom::HttpHeaderPtr> headers;
|
|
if (std::vector<std::pair<std::string, std::string>> extra_headers;
|
|
opts.Get("headers", &extra_headers)) {
|
|
for (const auto& [name, value] : extra_headers) {
|
|
if (!net::HttpUtil::IsValidHeaderName(name) ||
|
|
!net::HttpUtil::IsValidHeaderValue(value)) {
|
|
args->ThrowTypeError("Invalid header name or value");
|
|
return nullptr;
|
|
}
|
|
headers.push_back(network::mojom::HttpHeader::New(name, value));
|
|
}
|
|
}
|
|
|
|
// The origin passed to CreateWebSocket() is used as the request initiator
|
|
// (which affects SameSite cookie matching) and the `Origin` header. By
|
|
// default, use the WebSocket URL's HTTP-equivalent origin so the connection
|
|
// looks first-party — like a page on the same host opened the socket — and
|
|
// SameSite cookies can be sent when `useSessionCookies` is enabled.
|
|
url::Origin origin;
|
|
if (std::string origin_str; opts.Get("origin", &origin_str)) {
|
|
origin = url::Origin::Create(GURL(origin_str));
|
|
} else {
|
|
GURL::Replacements replacements;
|
|
replacements.SetSchemeStr(url.SchemeIs(url::kWssScheme) ? url::kHttpsScheme
|
|
: url::kHttpScheme);
|
|
origin = url::Origin::Create(url.ReplaceComponents(replacements));
|
|
}
|
|
|
|
const bool use_session_cookies =
|
|
opts.ValueOrDefault("useSessionCookies", false);
|
|
|
|
std::string partition;
|
|
Session* session = nullptr;
|
|
if (!opts.Get("session", &session)) {
|
|
if (opts.Get("partition", &partition))
|
|
session = Session::FromPartition(args->isolate(), partition);
|
|
else
|
|
session = Session::FromPartition(args->isolate(), "");
|
|
}
|
|
if (!session) {
|
|
args->ThrowTypeError("Failed to resolve session");
|
|
return nullptr;
|
|
}
|
|
|
|
v8::Isolate* isolate = args->isolate();
|
|
auto* wrapper = cppgc::MakeGarbageCollected<WebSocketWrapper>(
|
|
isolate->GetCppHeap()->GetAllocationHandle(), session->browser_context(),
|
|
url, std::move(protocols), std::move(headers), origin,
|
|
use_session_cookies);
|
|
wrapper->Start();
|
|
return wrapper;
|
|
}
|
|
|
|
cppgc::Persistent<gin::WeakCell<WebSocketWrapper>> WebSocketWrapper::WeakRef() {
|
|
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
|
return gin::WrapPersistent(
|
|
weak_factory_.GetWeakCell(isolate->GetCppHeap()->GetAllocationHandle()));
|
|
}
|
|
|
|
void WebSocketWrapper::Start() {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
CHECK(browser_context_);
|
|
|
|
auto handshake_remote = handshake_receiver_.BindNewPipeAndPassRemote();
|
|
handshake_receiver_.set_disconnect_handler(
|
|
base::BindOnce(&WebSocketWrapper::OnMojoDisconnect, WeakRef()));
|
|
|
|
auto* network_context =
|
|
browser_context_->GetDefaultStoragePartition()->GetNetworkContext();
|
|
// Note: no auth_handler is supplied. With a null auth handler the network
|
|
// service fails an HTTP/proxy auth challenge immediately (no credentials),
|
|
// which surfaces here as OnFailure(). Cached credentials in the network
|
|
// process (e.g. from a prior net.request login) are still used.
|
|
network_context->CreateWebSocket(
|
|
url_, requested_protocols_, net::StorageAccessApiStatus::kNone,
|
|
net::IsolationInfo::CreateForInternalRequest(origin_),
|
|
std::move(additional_headers_), network::OriginatingProcessId::browser(),
|
|
origin_, network::mojom::ClientSecurityState::New(),
|
|
use_session_cookies_ ? network::mojom::kWebSocketOptionNone
|
|
: network::mojom::kWebSocketOptionBlockAllCookies,
|
|
net::MutableNetworkTrafficAnnotationTag(kWebSocketTrafficAnnotation),
|
|
std::move(handshake_remote),
|
|
/*url_loader_network_observer=*/mojo::NullRemote(),
|
|
/*auth_handler=*/mojo::NullRemote(),
|
|
/*header_client=*/mojo::NullRemote(),
|
|
/*throttling_profile_id=*/std::nullopt,
|
|
// Browser-initiated WebSocket with no associated document, so no
|
|
// Connection-Allowlist network restrictions apply.
|
|
/*network_restrictions_id=*/network::GetNoOpNetworkRestrictionsId(),
|
|
// No Local Network Access checks for browser-initiated WebSockets.
|
|
/*target_address_space=*/network::mojom::IPAddressSpace::kUnknown,
|
|
// Not bound to a specific network.
|
|
/*target_network=*/std::nullopt);
|
|
}
|
|
|
|
void WebSocketWrapper::OnMojoDisconnect() {
|
|
if (state_ == State::kClosed)
|
|
return;
|
|
Fail("Connection to the network service was lost", net::ERR_FAILED);
|
|
}
|
|
|
|
void WebSocketWrapper::Fail(const std::string& message, int net_error) {
|
|
if (state_ == State::kClosed)
|
|
return;
|
|
state_ = State::kClosed;
|
|
// Per the WebSocket spec, the 'error' Event has no payload. Surface the
|
|
// failure message and net error as the CloseEvent.reason (which is otherwise
|
|
// always empty for network failures) so callers can diagnose without a
|
|
// debugger attached. Failure messages from the network service already embed
|
|
// the net error name.
|
|
std::string reason = message;
|
|
if (reason.find("net::ERR_") == std::string::npos)
|
|
base::StrAppend(&reason, {" (", net::ErrorToString(net_error), ")"});
|
|
Emit("error");
|
|
Emit("close", /*was_clean=*/false, /*code=*/static_cast<uint32_t>(1006),
|
|
reason);
|
|
Teardown();
|
|
}
|
|
|
|
void WebSocketWrapper::EmitCloseAndTeardown(bool was_clean,
|
|
uint32_t code,
|
|
const std::string& reason) {
|
|
Emit("close", was_clean, code, reason);
|
|
Teardown();
|
|
}
|
|
|
|
void WebSocketWrapper::Teardown() {
|
|
state_ = State::kClosed;
|
|
readable_watcher_.Cancel();
|
|
writable_watcher_.Cancel();
|
|
handshake_receiver_.reset();
|
|
client_receiver_.reset();
|
|
websocket_.reset();
|
|
readable_.reset();
|
|
writable_.reset();
|
|
pending_writes_.clear();
|
|
pending_read_data_.clear();
|
|
// Allow this object to be collected once nothing in JS references it.
|
|
keep_alive_.Clear();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// network::mojom::WebSocketHandshakeClient
|
|
|
|
void WebSocketWrapper::OnOpeningHandshakeStarted(
|
|
network::mojom::WebSocketHandshakeRequestPtr request) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
}
|
|
|
|
void WebSocketWrapper::OnFailure(const std::string& message,
|
|
int32_t net_error,
|
|
int32_t response_code) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
// After OnFailure the network service drops the handshake mojo pipe. Avoid
|
|
// double-reporting via the disconnect handler.
|
|
handshake_receiver_.set_disconnect_handler(base::DoNothing());
|
|
Fail(message, net_error);
|
|
}
|
|
|
|
void WebSocketWrapper::OnConnectionEstablished(
|
|
mojo::PendingRemote<network::mojom::WebSocket> websocket,
|
|
mojo::PendingReceiver<network::mojom::WebSocketClient> client_receiver,
|
|
network::mojom::WebSocketHandshakeResponsePtr response,
|
|
mojo::ScopedDataPipeConsumerHandle readable,
|
|
mojo::ScopedDataPipeProducerHandle writable) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
if (state_ != State::kConnecting)
|
|
return;
|
|
|
|
websocket_.Bind(std::move(websocket));
|
|
client_receiver_.Bind(std::move(client_receiver));
|
|
|
|
readable_ = std::move(readable);
|
|
CHECK_EQ(readable_watcher_.Watch(
|
|
readable_.get(), MOJO_HANDLE_SIGNAL_READABLE,
|
|
MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED,
|
|
base::BindRepeating(&WebSocketWrapper::OnReadable, WeakRef())),
|
|
MOJO_RESULT_OK);
|
|
|
|
writable_ = std::move(writable);
|
|
CHECK_EQ(writable_watcher_.Watch(
|
|
writable_.get(), MOJO_HANDLE_SIGNAL_WRITABLE,
|
|
MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED,
|
|
base::BindRepeating(&WebSocketWrapper::OnWritable, WeakRef())),
|
|
MOJO_RESULT_OK);
|
|
|
|
// |handshake_receiver_| will disconnect soon. From now on we watch
|
|
// |client_receiver_| for network-service crashes; |websocket_| terminates at
|
|
// the same WebSocketImpl so a second disconnect handler would be redundant.
|
|
handshake_receiver_.set_disconnect_handler(base::DoNothing());
|
|
client_receiver_.set_disconnect_handler(
|
|
base::BindOnce(&WebSocketWrapper::OnMojoDisconnect, WeakRef()));
|
|
|
|
websocket_->StartReceiving();
|
|
state_ = State::kOpen;
|
|
|
|
Emit("open", response->selected_protocol, response->extensions);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// network::mojom::WebSocketClient
|
|
|
|
void WebSocketWrapper::OnDataFrame(bool fin,
|
|
network::mojom::WebSocketMessageType type,
|
|
uint64_t data_len) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
CHECK_EQ(pending_read_offset_, pending_read_data_.size());
|
|
CHECK(!pending_read_fin_);
|
|
|
|
if (type != network::mojom::WebSocketMessageType::CONTINUATION) {
|
|
// First frame of a new message.
|
|
pending_read_type_ = type;
|
|
}
|
|
|
|
if (data_len == 0) {
|
|
if (fin)
|
|
ProcessCompletedMessage();
|
|
return;
|
|
}
|
|
|
|
const size_t old_size = pending_read_data_.size();
|
|
const uint64_t new_size = old_size + data_len;
|
|
if (data_len > std::numeric_limits<size_t>::max() - old_size ||
|
|
new_size > std::numeric_limits<size_t>::max()) {
|
|
Fail("WebSocket message too large", net::ERR_MSG_TOO_BIG);
|
|
return;
|
|
}
|
|
|
|
pending_read_data_.resize(static_cast<size_t>(new_size));
|
|
pending_read_fin_ = fin;
|
|
// Pause so we don't get the next OnDataFrame until this frame's bytes have
|
|
// been fully drained from the pipe.
|
|
client_receiver_.Pause();
|
|
OnReadable(MOJO_RESULT_OK, mojo::HandleSignalsState());
|
|
}
|
|
|
|
void WebSocketWrapper::OnReadable(MojoResult, const mojo::HandleSignalsState&) {
|
|
if (state_ == State::kClosed)
|
|
return;
|
|
CHECK_LT(pending_read_offset_, pending_read_data_.size());
|
|
|
|
size_t actually_read = 0;
|
|
const MojoResult result = readable_->ReadData(
|
|
MOJO_READ_DATA_FLAG_NONE,
|
|
base::span(pending_read_data_).subspan(pending_read_offset_),
|
|
actually_read);
|
|
if (result == MOJO_RESULT_OK) {
|
|
pending_read_offset_ += actually_read;
|
|
DCHECK_LE(pending_read_offset_, pending_read_data_.size());
|
|
if (pending_read_offset_ < pending_read_data_.size()) {
|
|
readable_watcher_.ArmOrNotify();
|
|
} else {
|
|
client_receiver_.Resume();
|
|
if (pending_read_fin_)
|
|
ProcessCompletedMessage();
|
|
}
|
|
} else if (result == MOJO_RESULT_SHOULD_WAIT) {
|
|
readable_watcher_.ArmOrNotify();
|
|
} else {
|
|
Fail("WebSocket read failed", net::ERR_FAILED);
|
|
}
|
|
}
|
|
|
|
void WebSocketWrapper::ProcessCompletedMessage() {
|
|
std::vector<uint8_t> data;
|
|
data.swap(pending_read_data_);
|
|
pending_read_offset_ = 0;
|
|
pending_read_fin_ = false;
|
|
const bool is_text =
|
|
pending_read_type_ == network::mojom::WebSocketMessageType::TEXT;
|
|
pending_read_type_ = network::mojom::WebSocketMessageType::CONTINUATION;
|
|
|
|
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
|
v8::HandleScope scope(isolate);
|
|
v8::Local<v8::Object> buffer;
|
|
if (!electron::Buffer::Copy(isolate, base::span(data)).ToLocal(&buffer)) {
|
|
Fail("Failed to allocate WebSocket message buffer", net::ERR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
Emit("message", is_text, buffer);
|
|
}
|
|
|
|
void WebSocketWrapper::OnDropChannel(bool was_clean,
|
|
uint16_t code,
|
|
const std::string& reason) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
if (state_ == State::kClosed)
|
|
return;
|
|
// Avoid the disconnect handler firing with a synthetic error after this.
|
|
client_receiver_.set_disconnect_handler(base::DoNothing());
|
|
websocket_.set_disconnect_handler(base::DoNothing());
|
|
state_ = State::kClosed;
|
|
Emit("close", was_clean, static_cast<uint32_t>(code), reason);
|
|
Teardown();
|
|
}
|
|
|
|
void WebSocketWrapper::OnClosingHandshake() {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
if (state_ == State::kOpen)
|
|
state_ = State::kClosing;
|
|
Emit("closing");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Send / Close
|
|
|
|
void WebSocketWrapper::Send(gin::Arguments* args) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
bool is_text = false;
|
|
v8::Local<v8::ArrayBufferView> data;
|
|
if (!args->GetNext(&is_text) || !args->GetNext(&data)) {
|
|
args->ThrowTypeError("Expected (isText, data)");
|
|
return;
|
|
}
|
|
|
|
const auto bytes = electron::util::as_byte_span(data);
|
|
// Per the WebSocket spec, `bufferedAmount` is incremented even when the
|
|
// data will never be sent (e.g. after close() has been called).
|
|
buffered_amount_ += bytes.size();
|
|
if (state_ != State::kOpen || close_requested_)
|
|
return;
|
|
|
|
pending_writes_.emplace_back(
|
|
is_text ? network::mojom::WebSocketMessageType::TEXT
|
|
: network::mojom::WebSocketMessageType::BINARY,
|
|
std::vector<uint8_t>(bytes.begin(), bytes.end()));
|
|
DrainWriteQueue();
|
|
}
|
|
|
|
void WebSocketWrapper::Close(gin::Arguments* args) {
|
|
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
|
if (state_ == State::kClosing || state_ == State::kClosed ||
|
|
close_requested_) {
|
|
return;
|
|
}
|
|
|
|
// Per spec, when no code is supplied, 1005 ("no status received") tells the
|
|
// network service not to send a status code in the Close frame.
|
|
uint32_t code = 1005;
|
|
std::string reason;
|
|
args->GetNext(&code);
|
|
args->GetNext(&reason);
|
|
close_code_ = static_cast<uint16_t>(code);
|
|
close_reason_ = reason;
|
|
close_requested_ = true;
|
|
|
|
if (state_ == State::kConnecting) {
|
|
// Abort the handshake. Per the WebSocket spec, closing while CONNECTING
|
|
// fails the connection: fire 'close' with wasClean=false, code 1006, and
|
|
// do not fire 'error'. The 'close' event must be fired in a queued task,
|
|
// not synchronously from close(), so post it.
|
|
handshake_receiver_.reset();
|
|
state_ = State::kClosed;
|
|
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
|
|
FROM_HERE, base::BindOnce(&WebSocketWrapper::EmitCloseAndTeardown,
|
|
WeakRef(), /*was_clean=*/false,
|
|
/*code=*/1006u, std::string()));
|
|
return;
|
|
}
|
|
|
|
// Drain any in-flight message bytes before sending the Close frame; the
|
|
// network service is waiting for them.
|
|
DrainWriteQueue();
|
|
}
|
|
|
|
void WebSocketWrapper::DrainWriteQueue() {
|
|
while (!pending_writes_.empty()) {
|
|
PendingMessage& msg = pending_writes_.front();
|
|
|
|
if (!msg.announced) {
|
|
websocket_->SendMessage(msg.type, msg.data.size());
|
|
msg.announced = true;
|
|
}
|
|
|
|
while (msg.offset < msg.data.size()) {
|
|
base::span<uint8_t> dest;
|
|
MojoResult result = writable_->BeginWriteData(
|
|
msg.data.size() - msg.offset, MOJO_BEGIN_WRITE_DATA_FLAG_NONE, dest);
|
|
if (result == MOJO_RESULT_SHOULD_WAIT) {
|
|
writable_watcher_.ArmOrNotify();
|
|
return;
|
|
}
|
|
if (result != MOJO_RESULT_OK) {
|
|
Fail("WebSocket write failed", net::ERR_FAILED);
|
|
return;
|
|
}
|
|
const size_t chunk = std::min(dest.size(), msg.data.size() - msg.offset);
|
|
dest.first(chunk).copy_from(
|
|
base::span(msg.data).subspan(msg.offset, chunk));
|
|
writable_->EndWriteData(chunk);
|
|
msg.offset += chunk;
|
|
buffered_amount_ -= chunk;
|
|
}
|
|
|
|
pending_writes_.pop_front();
|
|
}
|
|
|
|
MaybeStartClosingHandshake();
|
|
}
|
|
|
|
void WebSocketWrapper::OnWritable(MojoResult result,
|
|
const mojo::HandleSignalsState&) {
|
|
if (state_ == State::kClosed)
|
|
return;
|
|
if (result != MOJO_RESULT_OK) {
|
|
Fail("WebSocket write failed", net::ERR_FAILED);
|
|
return;
|
|
}
|
|
DrainWriteQueue();
|
|
}
|
|
|
|
void WebSocketWrapper::MaybeStartClosingHandshake() {
|
|
if (!close_requested_ || !pending_writes_.empty() || state_ != State::kOpen)
|
|
return;
|
|
state_ = State::kClosing;
|
|
websocket_->StartClosingHandshake(close_code_, close_reason_);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// gin
|
|
|
|
gin::ObjectTemplateBuilder WebSocketWrapper::GetObjectTemplateBuilder(
|
|
v8::Isolate* isolate) {
|
|
return gin_helper::EventEmitterMixin<
|
|
WebSocketWrapper>::GetObjectTemplateBuilder(isolate)
|
|
.SetMethod("send", &WebSocketWrapper::Send)
|
|
.SetMethod("close", &WebSocketWrapper::Close)
|
|
.SetMethod("getBufferedAmount", &WebSocketWrapper::GetBufferedAmount);
|
|
}
|
|
|
|
const gin::WrapperInfo* WebSocketWrapper::wrapper_info() const {
|
|
return &kWrapperInfo;
|
|
}
|
|
|
|
const char* WebSocketWrapper::GetHumanReadableName() const {
|
|
return "Electron / WebSocketWrapper";
|
|
}
|
|
|
|
void WebSocketWrapper::Trace(cppgc::Visitor* visitor) const {
|
|
gin::Wrappable<WebSocketWrapper>::Trace(visitor);
|
|
visitor->Trace(weak_factory_);
|
|
}
|
|
|
|
} // namespace electron::api
|