mirror of
https://github.com/electron/electron.git
synced 2026-10-28 19:54:46 +08:00
* feat: add net.WebSocket
Add a WHATWG-compatible WebSocket client to the net module, backed by
Chromium's network stack via NetworkContext::CreateWebSocket(). This
gives main-process code a WebSocket that goes through the session's
proxy configuration, certificate verification, and cookie store, with
the same drop-in API shape as the browser/Node global WebSocket.
The implementation has two pieces:
- shell/browser/api/electron_api_web_socket.{h,cc}: a gin-wrapped object
that implements network.mojom.{WebSocketHandshakeClient, WebSocketClient,
WebSocketAuthenticationHandler}, drives the mojo data pipes with
back-pressure on both sides, reassembles fragmented frames, tracks
bufferedAmount, and emits low-level events.
- lib/browser/api/net-websocket.ts: a class WebSocket extends EventTarget
that implements the WHATWG surface (addEventListener, on{open,message,
error,close}, readyState, bufferedAmount, protocol, extensions,
binaryType, send(), close(), CONNECTING/OPEN/CLOSING/CLOSED) plus
Electron-specific options (headers, origin, useSessionCookies, session,
partition).
binaryType supports 'nodebuffer' (default), 'arraybuffer', and 'blob'.
useSessionCookies defaults to false, matching net.request(). The Origin
header defaults to the http(s) equivalent of the WebSocket URL's origin
so that the connection looks first-party for SameSite cookie purposes;
it can be overridden with the origin option.
Main process only for now; webRequest interception is a known follow-up.
* fix: address review findings and migrate WebSocketWrapper to gin::Wrappable
Migrate WebSocketWrapper from gin_helper::DeprecatedWrappable to
gin::Wrappable (cppgc-based, electron/electron#47922):
- Add kElectronWebSocket wrappable pointer tag.
- Replace Pin()/Unpin() with gin_helper::SelfKeepAlive, cleared on teardown.
- Replace base::WeakPtrFactory with gin::WeakCellFactory + gin::WrapPersistent
for callbacks; add Trace() to visit the weak cell.
- Annotate mojo::Receiver/Remote fields with GC_PLUGIN_IGNORE (browser process
doesn't need execution-context tracking), matching UtilityProcessWrapper.
- Public constructor for cppgc::MakeGarbageCollected; Create() returns T*.
Spec and correctness fixes:
- Drop the unused WebSocketAuthenticationHandler. The 'login' event it emitted
was never bridged to the WHATWG-only TS surface, leaving the mojo
OnAuthRequired callback orphaned and the connection hung on a 401/407
challenge until V8 GC. Pass NullRemote() so the network service fails the
challenge fast (matching WebSocketConnectorImpl). Drop the 'authenticated
proxies' claim from the docs.
- Serialize the send queue so a Blob send (which must read asynchronously)
doesn't let a later string/ArrayBuffer send overtake it on the wire.
- Set readyState to CLOSED before dispatching the 'error' event, per the
WebSocket spec ordering rules.
- Stop nulling the wrapper on close so bufferedAmount reflects untransmitted
bytes (it must not reset to zero) and continues to count post-close sends.
- Use WebIDL [Clamp] semantics for the close() code argument.
- Disable permessage-deflate on the test ws server; it isn't the subject under
test and ws@7's deflate close path can race a server-initiated close.
* chore: correct copyright holder in new files
* fix: address review feedback
- Track all bytes queued in the JS send chain (not just Blob reads) so
bufferedAmount is accurate while a non-Blob send is waiting behind an
asynchronous Blob read.
- Surface the network failure message and net error in CloseEvent.reason
for connection failures (the WHATWG 'error' Event has no payload). Failure
reasons from the network service already embed the net error name; only
append it for internally-generated failures.
- Emit a Node Buffer from the C++ side via electron::Buffer::Copy() instead
of a raw ArrayBuffer, avoiding a TS-side wrap on the default 'nodebuffer'
path.
- Drop the redundant disconnect handler on the WebSocket remote; the
WebSocketClient receiver and the remote terminate at the same WebSocketImpl
in the network process.
* chore: remove accidentally committed lock file
193 lines
6.8 KiB
C++
193 lines
6.8 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.
|
|
|
|
#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_WEB_SOCKET_H_
|
|
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_WEB_SOCKET_H_
|
|
|
|
#include <deque>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "base/memory/raw_ptr.h"
|
|
#include "base/sequence_checker.h"
|
|
#include "gin/weak_cell.h"
|
|
#include "gin/wrappable.h"
|
|
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
|
#include "mojo/public/cpp/bindings/pending_remote.h"
|
|
#include "mojo/public/cpp/bindings/receiver.h"
|
|
#include "mojo/public/cpp/bindings/remote.h"
|
|
#include "mojo/public/cpp/system/data_pipe.h"
|
|
#include "mojo/public/cpp/system/simple_watcher.h"
|
|
#include "services/network/public/mojom/websocket.mojom.h"
|
|
#include "shell/browser/event_emitter_mixin.h"
|
|
#include "shell/common/gc_plugin.h"
|
|
#include "shell/common/gin_helper/self_keep_alive.h"
|
|
#include "url/gurl.h"
|
|
#include "url/origin.h"
|
|
#include "v8/include/v8-forward.h"
|
|
|
|
namespace gin {
|
|
class Arguments;
|
|
} // namespace gin
|
|
|
|
namespace electron {
|
|
class ElectronBrowserContext;
|
|
}
|
|
|
|
namespace electron::api {
|
|
|
|
// A browser-process WebSocket client that goes through Chromium's network
|
|
// service. Implements the network.mojom.WebSocketHandshakeClient and
|
|
// WebSocketClient interfaces and translates them into JS events that the
|
|
// `net.WebSocket` TypeScript class consumes.
|
|
class WebSocketWrapper final
|
|
: public gin::Wrappable<WebSocketWrapper>,
|
|
public gin_helper::EventEmitterMixin<WebSocketWrapper>,
|
|
private network::mojom::WebSocketHandshakeClient,
|
|
private network::mojom::WebSocketClient {
|
|
public:
|
|
static WebSocketWrapper* Create(gin::Arguments* args);
|
|
|
|
// gin::Wrappable
|
|
static const gin::WrapperInfo kWrapperInfo;
|
|
const gin::WrapperInfo* wrapper_info() const override;
|
|
const char* GetHumanReadableName() const override;
|
|
static const char* GetClassName() { return "WebSocketWrapper"; }
|
|
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
|
v8::Isolate* isolate) override;
|
|
void Trace(cppgc::Visitor* visitor) const override;
|
|
|
|
// Public for cppgc::MakeGarbageCollected.
|
|
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);
|
|
~WebSocketWrapper() override;
|
|
|
|
WebSocketWrapper(const WebSocketWrapper&) = delete;
|
|
WebSocketWrapper& operator=(const WebSocketWrapper&) = delete;
|
|
|
|
private:
|
|
enum class State {
|
|
kConnecting,
|
|
kOpen,
|
|
kClosing,
|
|
kClosed,
|
|
};
|
|
|
|
struct PendingMessage {
|
|
PendingMessage(network::mojom::WebSocketMessageType type,
|
|
std::vector<uint8_t> data);
|
|
PendingMessage(PendingMessage&&);
|
|
PendingMessage& operator=(PendingMessage&&);
|
|
~PendingMessage();
|
|
|
|
network::mojom::WebSocketMessageType type;
|
|
std::vector<uint8_t> data;
|
|
size_t offset = 0;
|
|
bool announced = false;
|
|
};
|
|
|
|
// JS-callable methods.
|
|
void Send(gin::Arguments* args);
|
|
void Close(gin::Arguments* args);
|
|
uint64_t GetBufferedAmount() const { return buffered_amount_; }
|
|
|
|
// network::mojom::WebSocketHandshakeClient:
|
|
void OnOpeningHandshakeStarted(
|
|
network::mojom::WebSocketHandshakeRequestPtr request) override;
|
|
void OnFailure(const std::string& message,
|
|
int32_t net_error,
|
|
int32_t response_code) override;
|
|
void OnConnectionEstablished(
|
|
mojo::PendingRemote<network::mojom::WebSocket> websocket,
|
|
mojo::PendingReceiver<network::mojom::WebSocketClient> client_receiver,
|
|
network::mojom::WebSocketHandshakeResponsePtr response,
|
|
mojo::ScopedDataPipeConsumerHandle readable,
|
|
mojo::ScopedDataPipeProducerHandle writable) override;
|
|
|
|
// network::mojom::WebSocketClient:
|
|
void OnDataFrame(bool fin,
|
|
network::mojom::WebSocketMessageType type,
|
|
uint64_t data_len) override;
|
|
void OnDropChannel(bool was_clean,
|
|
uint16_t code,
|
|
const std::string& reason) override;
|
|
void OnClosingHandshake() override;
|
|
|
|
// Connection lifecycle.
|
|
void Start();
|
|
// Vends a persistent handle to a gin::WeakCell so that callbacks bound to
|
|
// `this` are no-ops once the object has been collected.
|
|
cppgc::Persistent<gin::WeakCell<WebSocketWrapper>> WeakRef();
|
|
void OnMojoDisconnect();
|
|
void Fail(const std::string& message, int net_error);
|
|
void EmitCloseAndTeardown(bool was_clean,
|
|
uint32_t code,
|
|
const std::string& reason);
|
|
void Teardown();
|
|
|
|
// Read side.
|
|
void OnReadable(MojoResult result, const mojo::HandleSignalsState& state);
|
|
void ProcessCompletedMessage();
|
|
|
|
// Write side.
|
|
void OnWritable(MojoResult result, const mojo::HandleSignalsState& state);
|
|
void DrainWriteQueue();
|
|
void MaybeStartClosingHandshake();
|
|
|
|
SEQUENCE_CHECKER(sequence_checker_);
|
|
|
|
State state_ = State::kConnecting;
|
|
raw_ptr<ElectronBrowserContext> browser_context_;
|
|
GURL url_;
|
|
std::vector<std::string> requested_protocols_;
|
|
std::vector<network::mojom::HttpHeaderPtr> additional_headers_;
|
|
url::Origin origin_;
|
|
bool use_session_cookies_ = false;
|
|
|
|
// Mojo plumbing.
|
|
GC_PLUGIN_IGNORE(
|
|
"Context tracking of receiver is not needed in the browser process.")
|
|
mojo::Receiver<network::mojom::WebSocketHandshakeClient> handshake_receiver_{
|
|
this};
|
|
GC_PLUGIN_IGNORE(
|
|
"Context tracking of receiver is not needed in the browser process.")
|
|
mojo::Receiver<network::mojom::WebSocketClient> client_receiver_{this};
|
|
GC_PLUGIN_IGNORE(
|
|
"Context tracking of remote is not needed in the browser process.")
|
|
mojo::Remote<network::mojom::WebSocket> websocket_;
|
|
mojo::ScopedDataPipeConsumerHandle readable_;
|
|
mojo::SimpleWatcher readable_watcher_;
|
|
mojo::ScopedDataPipeProducerHandle writable_;
|
|
mojo::SimpleWatcher writable_watcher_;
|
|
|
|
// Read state. A WebSocket message may span multiple frames; we accumulate
|
|
// them in |pending_read_data_| until we see a frame with fin=true.
|
|
std::vector<uint8_t> pending_read_data_;
|
|
size_t pending_read_offset_ = 0;
|
|
bool pending_read_fin_ = false;
|
|
network::mojom::WebSocketMessageType pending_read_type_ =
|
|
network::mojom::WebSocketMessageType::CONTINUATION;
|
|
|
|
// Write state.
|
|
std::deque<PendingMessage> pending_writes_;
|
|
uint64_t buffered_amount_ = 0;
|
|
bool close_requested_ = false;
|
|
uint16_t close_code_ = 0;
|
|
std::string close_reason_;
|
|
|
|
// Roots this object in the cppgc heap while the connection is live so it is
|
|
// not collected if the JS wrapper goes out of scope mid-connection. Cleared
|
|
// in Teardown().
|
|
gin_helper::SelfKeepAlive<WebSocketWrapper> keep_alive_{this};
|
|
gin::WeakCellFactory<WebSocketWrapper> weak_factory_{this};
|
|
};
|
|
|
|
} // namespace electron::api
|
|
|
|
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_WEB_SOCKET_H_
|