blob: 711b85d2d0cf16f0b92983419ed223e2ea4f3ae2 [file] [edit]
/*
* Copyright (C) 2026 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 "WebSocketServer.h"
#if PLATFORM(COCOA) && ENABLE(WEBDRIVER_BIDI)
#include "Logging.h"
#include "LoopbackCocoa.h"
#include "WebDriverNetworkSPI.h"
#include <wtf/OSObjectPtr.h>
#include <wtf/WeakPtr.h>
#include <wtf/darwin/DispatchExtras.h>
#include <wtf/text/CString.h>
#include <wtf/text/WTFString.h>
// The BiDi WebSocket server is an nw_listener with the WebSocket protocol, which does the
// handshake and the framing, so each receive delivers a whole message. It listens on 127.0.0.1
// only, rejects handshakes from pages that aren't on this machine, and answers each handshake
// with a handler on that connection's own WebSocket options, which is the only way to know which
// resource a connection requested.
//
// The requested resource is only available through nw_ws_request_copy_path(), which is new in
// macOS 26. Before macOS 26, every handshake is rejected.
namespace WebDriver {
// Network.framework takes the response of a handshake handler at +0, as code built with ARC returns
// it autoreleased, so the handlers keep the responses they return.
static OSObjectPtr<nw_ws_response_t> handshakeResponse(nw_ws_response_status_t status)
{
return adoptOSObject(nw_ws_response_create(status, nullptr));
}
static String originOfHandshake(nw_ws_request_t request)
{
__block String origin;
nw_ws_request_enumerate_additional_headers(request, ^bool(const char* name, const char* value) {
if (!equalLettersIgnoringASCIICase(StringView::fromLatin1(name), "origin"_s))
return true;
origin = String::fromUTF8(value);
return false;
});
return origin;
}
static nw_ws_opcode_t webSocketOpcode(nw_content_context_t context)
{
OSObjectPtr definition = adoptOSObject(nw_protocol_copy_ws_definition());
OSObjectPtr metadata = adoptOSObject(nw_content_context_copy_protocol_metadata(context, definition.get()));
return metadata ? nw_ws_metadata_get_opcode(metadata.get()) : nw_ws_opcode_invalid;
}
static void sendText(nw_connection_t connection, std::span<const char8_t> text)
{
OSObjectPtr metadata = adoptOSObject(nw_ws_create_metadata(nw_ws_opcode_text));
OSObjectPtr context = adoptOSObject(nw_content_context_create("send"));
nw_content_context_set_metadata_for_protocol(context.get(), metadata.get());
OSObjectPtr data = adoptOSObject(dispatch_data_create(text.data(), text.size(), mainDispatchQueueSingleton(), DISPATCH_DATA_DESTRUCTOR_DEFAULT));
nw_connection_send(connection, data.get(), context.get(), true, ^(nw_error_t error) {
if (error)
RELEASE_LOG_ERROR(WebDriverBiDi, "Sending a WebSocket message failed with error %d", nw_error_get_error_code(error));
});
}
static void closeConnection(const WebSocketMessageHandler::Connection& connection)
{
OSObjectPtr metadata = adoptOSObject(nw_ws_create_metadata(nw_ws_opcode_close));
nw_ws_metadata_set_close_code(metadata.get(), nw_ws_close_code_normal_closure);
OSObjectPtr context = adoptOSObject(nw_content_context_create("close"));
nw_content_context_set_metadata_for_protocol(context.get(), metadata.get());
WebSocketMessageHandler::Connection closedConnection = connection;
nw_connection_send(connection.get(), nullptr, context.get(), true, ^(nw_error_t) {
nw_connection_cancel(closedConnection.get());
});
}
std::optional<String> WebSocketServer::listen(const String& host, unsigned port)
{
if (!validateListeningHost(host))
return std::nullopt;
OSObjectPtr parameters = adoptOSObject(nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION));
nw_parameters_set_local_endpoint(parameters.get(), adoptOSObject(nw_endpoint_create_host("127.0.0.1", String::number(port).utf8().legacyCStringPointer())).get());
OSObjectPtr webSocketOptions = adoptOSObject(nw_ws_create_options(nw_ws_version_13));
nw_ws_options_set_auto_reply_ping(webSocketOptions.get(), true);
// Each connection gets its own handshake handler in startConnection(), so a handshake that
// reaches this one belongs to no known connection.
OSObjectPtr rejection = handshakeResponse(nw_ws_response_status_reject);
nw_ws_options_set_client_request_handler(webSocketOptions.get(), mainDispatchQueueSingleton(), ^nw_ws_response_t(nw_ws_request_t) {
return rejection.get();
});
OSObjectPtr protocolStack = adoptOSObject(nw_parameters_copy_default_protocol_stack(parameters.get()));
nw_protocol_stack_prepend_application_protocol(protocolStack.get(), webSocketOptions.get());
OSObjectPtr listener = adoptOSObject(nw_listener_create(parameters.get()));
if (!listener) {
RELEASE_LOG(WebDriverBiDi, "Failed to start WebSocket server at port %u", port);
return std::nullopt;
}
auto weakThis = WeakPtr { *this };
nw_listener_set_new_connection_handler(listener.get(), ^(nw_connection_t connection) {
WebSocketMessageHandler::Connection newConnection = connection;
dispatch_async(mainDispatchQueueSingleton(), ^{
if (RefPtr protectedThis = weakThis)
protectedThis->startConnection(WebSocketMessageHandler::Connection { newConnection });
else
nw_connection_cancel(newConnection.get());
});
});
// The listener's handlers run on a queue of their own, so that this can wait for the listener to
// be ready before the run loop runs.
OSObjectPtr queue = adoptOSObject(dispatch_queue_create("org.webkit.minidriver.WebSocketServer", serialQueueWithAutoreleasePoolAttrSingleton()));
nw_listener_set_queue(listener.get(), queue.get());
OSObjectPtr semaphore = adoptOSObject(dispatch_semaphore_create(0));
__block bool isReady = false;
__block bool didReport = false;
nw_listener_set_state_changed_handler(listener.get(), ^(nw_listener_state_t state, nw_error_t error) {
if (didReport) {
if (state == nw_listener_state_failed)
RELEASE_LOG_ERROR(WebDriverBiDi, "WebSocket listener failed with error %d", nw_error_get_error_code(error));
return;
}
if (state != nw_listener_state_ready && state != nw_listener_state_failed && state != nw_listener_state_waiting && state != nw_listener_state_cancelled)
return;
isReady = state == nw_listener_state_ready;
didReport = true;
dispatch_semaphore_signal(semaphore.get());
});
nw_listener_start(listener.get());
dispatch_semaphore_wait(semaphore.get(), DISPATCH_TIME_FOREVER);
if (!isReady) {
RELEASE_LOG(WebDriverBiDi, "Failed to start WebSocket server at port %u", port);
nw_listener_cancel(listener.get());
return std::nullopt;
}
unsigned boundPort = nw_listener_get_port(listener.get());
m_networkListener = WTF::move(listener);
// "/session" is where clients start BiDi-only sessions, with session.new. startListening() adds the
// resource of each session. https://w3c.github.io/webdriver-bidi/#establishing
m_listener = WebSocketListener::create("127.0.0.1"_s, boundPort, false, { getResourceName(nullString()) });
return getWebSocketURL(m_listener, nullString());
}
// The listener gives every connection a copy of its parameters, so a handshake handler set on these
// options only answers this connection's handshake.
static OSObjectPtr<nw_protocol_options_t> webSocketOptions(nw_connection_t connection)
{
OSObjectPtr parameters = adoptOSObject(nw_connection_copy_parameters(connection));
OSObjectPtr protocolStack = adoptOSObject(nw_parameters_copy_default_protocol_stack(parameters.get()));
OSObjectPtr webSocketDefinition = adoptOSObject(nw_protocol_copy_ws_definition());
__block OSObjectPtr<nw_protocol_options_t> result;
nw_protocol_stack_iterate_application_protocols(protocolStack.get(), ^(nw_protocol_options_t options) {
OSObjectPtr definition = adoptOSObject(nw_protocol_options_copy_definition(options));
if (nw_protocol_definition_is_equal(definition.get(), webSocketDefinition.get()))
result = options;
});
return result;
}
void WebSocketServer::startConnection(WebSocketMessageHandler::Connection&& connection)
{
WebSocketMessageHandler::Connection protectedConnection = WTF::move(connection);
nw_connection_t networkConnection = protectedConnection.get();
OSObjectPtr options = webSocketOptions(networkConnection);
if (!options) {
nw_connection_cancel(networkConnection);
return;
}
__block String resourcePath;
auto weakThis = WeakPtr { *this };
OSObjectPtr acceptance = handshakeResponse(nw_ws_response_status_accept);
OSObjectPtr rejection = handshakeResponse(nw_ws_response_status_reject);
nw_ws_options_set_client_request_handler(options.get(), mainDispatchQueueSingleton(), ^nw_ws_response_t(nw_ws_request_t request) {
RefPtr protectedThis = weakThis;
if (!protectedThis)
return rejection.get();
// Browsers send the origin of the page that opens a WebSocket. Only accept pages from this
// machine. Clients that aren't browsers usually send no origin.
auto origin = originOfHandshake(request);
if (!origin.isNull() && !isLoopbackURL(origin)) {
RELEASE_LOG(WebDriverBiDi, "Rejecting a WebSocket handshake from origin %s", origin.utf8());
return rejection.get();
}
if (__builtin_available(macOS 26.0, *)) {
String path;
if (char* rawPath = nw_ws_request_copy_path(request)) {
path = String::fromUTF8(rawPath);
free(rawPath);
}
if (auto queryStart = path.find('?'); queryStart != notFound)
path = path.left(queryStart);
if (!protectedThis->m_messageHandler.acceptHandshake({ "GET"_s, path, nullptr, 0 }))
return rejection.get();
resourcePath = WTF::move(path);
return acceptance.get();
}
return rejection.get();
});
nw_connection_set_state_changed_handler(networkConnection, ^(nw_connection_state_t state, nw_error_t) {
RefPtr protectedThis = weakThis;
switch (state) {
case nw_connection_state_ready:
if (protectedThis)
protectedThis->didOpenConnection(protectedConnection, resourcePath);
else
nw_connection_cancel(protectedConnection.get());
return;
case nw_connection_state_failed:
nw_connection_cancel(protectedConnection.get());
return;
case nw_connection_state_cancelled:
// This is the last state. Forget the handler, which keeps the connection alive.
nw_connection_set_state_changed_handler(protectedConnection.get(), nullptr);
if (protectedThis)
protectedThis->didCloseConnection(protectedConnection);
return;
default:
return;
}
});
nw_connection_set_queue(networkConnection, mainDispatchQueueSingleton());
nw_connection_start(networkConnection);
}
void WebSocketServer::didOpenConnection(const WebSocketMessageHandler::Connection& connection, const String& resourcePath)
{
// A connection becomes ready even when its handshake was rejected.
if (resourcePath.isNull()) {
nw_connection_cancel(connection.get());
return;
}
// Check the resource again, in case the session ended during the handshake.
if (!m_messageHandler.acceptHandshake({ "GET"_s, resourcePath, nullptr, 0 })) {
closeConnection(connection);
return;
}
m_openConnections.add(connection);
// Delayed steps from the end of the handshake, as now we have a connection object.
if (resourcePath == "/session"_s)
addStaticConnection(WebSocketMessageHandler::Connection { connection });
else
addConnection(WebSocketMessageHandler::Connection { connection }, getSessionID(resourcePath));
receiveMessage(connection);
}
void WebSocketServer::receiveMessage(const WebSocketMessageHandler::Connection& connection)
{
auto weakThis = WeakPtr { *this };
WebSocketMessageHandler::Connection protectedConnection = connection;
nw_connection_receive_message(connection.get(), ^(dispatch_data_t content, nw_content_context_t context, bool, nw_error_t error) {
RefPtr protectedThis = weakThis;
if (!protectedThis || !protectedThis->m_openConnections.contains(protectedConnection))
return;
auto opcode = context ? webSocketOpcode(context) : nw_ws_opcode_invalid;
if (content && (opcode == nw_ws_opcode_text || opcode == nw_ws_opcode_binary))
protectedThis->didReceiveMessage(protectedConnection, content, opcode);
// The connection ends with a close frame, which needs one in reply (RFC 6455, section 5.5.1), an
// error, or the final context when the client goes away without a close frame.
if (opcode == nw_ws_opcode_close) {
closeConnection(protectedConnection);
return;
}
if (error || (context && nw_content_context_get_is_final(context))) {
nw_connection_cancel(protectedConnection.get());
return;
}
if (protectedThis->m_openConnections.contains(protectedConnection))
protectedThis->receiveMessage(protectedConnection);
});
}
void WebSocketServer::didReceiveMessage(const WebSocketMessageHandler::Connection& connection, dispatch_data_t content, nw_ws_opcode_t opcode)
{
// https://w3c.github.io/webdriver-bidi/#handle-an-incoming-message
if (opcode != nw_ws_opcode_text) {
RELEASE_LOG(WebDriverBiDi, "websocket message handler received non-text message. error return");
auto errorReply = WebSocketMessageHandler::Message::fail(CommandResult::ErrorCode::InvalidArgument, std::nullopt, { "Non-text message received"_s });
sendText(connection.get(), errorReply.payload.span());
return;
}
const void* bytes = nullptr;
size_t size = 0;
OSObjectPtr contiguousContent = adoptOSObject(dispatch_data_create_map(content, &bytes, &size));
UTF8CString payload { unsafeMakeSpan(static_cast<const char8_t*>(bytes), size) };
m_messageHandler.handleMessage({ connection, WTF::move(payload) }, [](WebSocketMessageHandler::Message&& message) {
if (!message.connection) {
RELEASE_LOG(WebDriverBiDi, "No connection found when trying to send message: %s", message.payload);
return;
}
sendText(message.connection.get(), message.payload.span());
});
}
void WebSocketServer::didCloseConnection(const WebSocketMessageHandler::Connection& connection)
{
// Connections that this side closed aren't reported, as with soup.
if (m_openConnections.remove(connection))
m_messageHandler.clientDisconnected(connection);
}
void WebSocketServer::sendMessage(WebSocketMessageHandler::Connection connection, const String& message)
{
ASSERT(connection);
RELEASE_LOG(WebDriverBiDi, "Sending message: %s", message.utf8());
sendText(connection.get(), message.utf8().span());
}
void WebSocketServer::disconnect()
{
if (!m_networkListener)
return;
nw_listener_cancel(m_networkListener.get());
m_networkListener = nullptr;
for (auto& connection : std::exchange(m_openConnections, { }))
closeConnection(connection);
}
void WebSocketServer::disconnectSession(const String& sessionId)
{
// Close every connection of the session. As with soup, they aren't reported as closed by the
// client.
for (auto& [connection, connectionSessionID] : m_connectionToSession) {
if (connectionSessionID == sessionId && m_openConnections.remove(connection))
closeConnection(connection);
}
}
} // namespace WebDriver
#endif // PLATFORM(COCOA) && ENABLE(WEBDRIVER_BIDI)