mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-06-11 18:20:43 +09:00
LibWebSocket+RequestServer: Add a WebSocketImpl using libcurl
This implementation can be better improved in the future by ripping out a lot of the manual logic in LibWebSocket and rely on libcurl to parse our message payloads. But for now, this uses the 'raw mode' of curl websockets in connect-only mode to allow for somewhat seamless integration into our event loop.
This commit is contained in:
parent
ad985f3227
commit
71942d53eb
Notes:
github-actions[bot]
2025-02-20 22:06:12 +00:00
Author: https://github.com/ADKaster
Commit: 71942d53eb
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3642
Reviewed-by: https://github.com/alimpfard ✅
7 changed files with 248 additions and 4 deletions
|
@ -27,6 +27,8 @@ public:
|
||||||
virtual bool eof() = 0;
|
virtual bool eof() = 0;
|
||||||
virtual void discard_connection() = 0;
|
virtual void discard_connection() = 0;
|
||||||
|
|
||||||
|
virtual bool handshake_complete_when_connected() const { return false; }
|
||||||
|
|
||||||
Function<void()> on_connected;
|
Function<void()> on_connected;
|
||||||
Function<void()> on_connection_error;
|
Function<void()> on_connection_error;
|
||||||
Function<void()> on_ready_to_read;
|
Function<void()> on_ready_to_read;
|
||||||
|
|
|
@ -42,9 +42,14 @@ void WebSocket::start()
|
||||||
m_impl->on_connected = [this] {
|
m_impl->on_connected = [this] {
|
||||||
if (m_state != WebSocket::InternalState::EstablishingProtocolConnection)
|
if (m_state != WebSocket::InternalState::EstablishingProtocolConnection)
|
||||||
return;
|
return;
|
||||||
set_state(WebSocket::InternalState::SendingClientHandshake);
|
if (m_impl->handshake_complete_when_connected()) {
|
||||||
send_client_handshake();
|
set_state(WebSocket::InternalState::Open);
|
||||||
drain_read();
|
notify_open();
|
||||||
|
} else {
|
||||||
|
set_state(WebSocket::InternalState::SendingClientHandshake);
|
||||||
|
send_client_handshake();
|
||||||
|
drain_read();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
m_impl->on_ready_to_read = [this] {
|
m_impl->on_ready_to_read = [this] {
|
||||||
drain_read();
|
drain_read();
|
||||||
|
|
|
@ -4,6 +4,7 @@ set(CMAKE_AUTOUIC OFF)
|
||||||
|
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
ConnectionFromClient.cpp
|
ConnectionFromClient.cpp
|
||||||
|
WebSocketImplCurl.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ANDROID)
|
if (ANDROID)
|
||||||
|
|
|
@ -77,4 +77,6 @@ private:
|
||||||
NonnullRefPtr<Resolver> m_resolver;
|
NonnullRefPtr<Resolver> m_resolver;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constexpr inline uintptr_t websocket_private_tag = 0x1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
187
Services/RequestServer/WebSocketImplCurl.cpp
Normal file
187
Services/RequestServer/WebSocketImplCurl.cpp
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <RequestServer/WebSocketImplCurl.h>
|
||||||
|
|
||||||
|
namespace RequestServer {
|
||||||
|
|
||||||
|
NonnullRefPtr<WebSocketImplCurl> WebSocketImplCurl::create(CURLM* multi_handle)
|
||||||
|
{
|
||||||
|
return adopt_ref(*new WebSocketImplCurl(multi_handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketImplCurl::WebSocketImplCurl(CURLM* multi_handle)
|
||||||
|
: m_multi_handle(multi_handle)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketImplCurl::~WebSocketImplCurl()
|
||||||
|
{
|
||||||
|
if (m_read_notifier)
|
||||||
|
m_read_notifier->close();
|
||||||
|
if (m_error_notifier)
|
||||||
|
m_error_notifier->close();
|
||||||
|
|
||||||
|
if (m_easy_handle) {
|
||||||
|
curl_multi_remove_handle(m_multi_handle, m_easy_handle);
|
||||||
|
curl_easy_cleanup(m_easy_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto* list : m_curl_string_lists) {
|
||||||
|
curl_slist_free_all(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketImplCurl::connect(WebSocket::ConnectionInfo const& info)
|
||||||
|
{
|
||||||
|
VERIFY(!m_easy_handle);
|
||||||
|
VERIFY(on_connected);
|
||||||
|
VERIFY(on_connection_error);
|
||||||
|
VERIFY(on_ready_to_read);
|
||||||
|
|
||||||
|
m_easy_handle = curl_easy_init();
|
||||||
|
VERIFY(m_easy_handle); // FIXME: Allow failure, and return ENOMEM
|
||||||
|
|
||||||
|
auto set_option = [this](auto option, auto value) -> bool {
|
||||||
|
auto result = curl_easy_setopt(m_easy_handle, option, value);
|
||||||
|
if (result == CURLE_OK)
|
||||||
|
return true;
|
||||||
|
dbgln("WebSocketImplCurl::connect: Failed to set curl option {}={}: {}", to_underlying(option), value, curl_easy_strerror(result));
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
set_option(CURLOPT_PRIVATE, reinterpret_cast<uintptr_t>(this) | websocket_private_tag);
|
||||||
|
set_option(CURLOPT_WS_OPTIONS, CURLWS_RAW_MODE);
|
||||||
|
set_option(CURLOPT_CONNECT_ONLY, 2); // WebSocket mode
|
||||||
|
|
||||||
|
// FIXME: Add a header function to validate the Sec-WebSocket headers that curl currently doesn't validate
|
||||||
|
|
||||||
|
auto const& url = info.url();
|
||||||
|
set_option(CURLOPT_URL, url.to_byte_string().characters());
|
||||||
|
set_option(CURLOPT_PORT, url.port_or_default());
|
||||||
|
|
||||||
|
if (auto root_certs = info.root_certificates_path(); root_certs.has_value())
|
||||||
|
set_option(CURLOPT_CAINFO, root_certs->characters());
|
||||||
|
|
||||||
|
auto const origin_header = ByteString::formatted("Origin: {}", info.origin());
|
||||||
|
curl_slist* curl_headers = curl_slist_append(nullptr, origin_header.characters());
|
||||||
|
|
||||||
|
for (auto const& [name, value] : info.headers().headers()) {
|
||||||
|
// curl will discard headers with empty values unless we pass the header name followed by a semicolon.
|
||||||
|
ByteString header_string;
|
||||||
|
if (value.is_empty())
|
||||||
|
header_string = ByteString::formatted("{};", name);
|
||||||
|
else
|
||||||
|
header_string = ByteString::formatted("{}: {}", name, value);
|
||||||
|
curl_headers = curl_slist_append(curl_headers, header_string.characters());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto const& protocols = info.protocols(); !protocols.is_empty()) {
|
||||||
|
StringBuilder protocol_builder;
|
||||||
|
protocol_builder.append("Sec-WebSocket-Protocol: "sv);
|
||||||
|
protocol_builder.append(ByteString::join(","sv, protocols));
|
||||||
|
curl_headers = curl_slist_append(curl_headers, protocol_builder.to_byte_string().characters());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto const& extensions = info.extensions(); !extensions.is_empty()) {
|
||||||
|
StringBuilder protocol_builder;
|
||||||
|
protocol_builder.append("Sec-WebSocket-Extensions: "sv);
|
||||||
|
protocol_builder.append(ByteString::join(","sv, extensions));
|
||||||
|
curl_headers = curl_slist_append(curl_headers, protocol_builder.to_byte_string().characters());
|
||||||
|
}
|
||||||
|
|
||||||
|
set_option(CURLOPT_HTTPHEADER, curl_headers);
|
||||||
|
m_curl_string_lists.append(curl_headers);
|
||||||
|
|
||||||
|
CURLMcode const err = curl_multi_add_handle(m_multi_handle, m_easy_handle);
|
||||||
|
VERIFY(err == CURLM_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketImplCurl::can_read_line()
|
||||||
|
{
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorOr<ByteBuffer> WebSocketImplCurl::read(int max_size)
|
||||||
|
{
|
||||||
|
auto buffer = TRY(ByteBuffer::create_uninitialized(max_size));
|
||||||
|
auto const read_bytes = TRY(m_read_buffer.read_some(buffer));
|
||||||
|
return buffer.slice(0, read_bytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorOr<ByteString> WebSocketImplCurl::read_line(size_t)
|
||||||
|
{
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketImplCurl::send(ReadonlyBytes bytes)
|
||||||
|
{
|
||||||
|
size_t sent = 0;
|
||||||
|
CURLcode result = CURLE_OK;
|
||||||
|
do {
|
||||||
|
sent = 0;
|
||||||
|
result = curl_easy_send(m_easy_handle, bytes.data(), bytes.size(), &sent);
|
||||||
|
bytes = bytes.slice(sent);
|
||||||
|
} while (bytes.size() > 0 && (result == CURLE_OK || result == CURLE_AGAIN));
|
||||||
|
|
||||||
|
return result == CURLE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebSocketImplCurl::eof()
|
||||||
|
{
|
||||||
|
return m_read_buffer.is_eof();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketImplCurl::discard_connection()
|
||||||
|
{
|
||||||
|
if (m_read_notifier) {
|
||||||
|
m_read_notifier->close();
|
||||||
|
m_read_notifier = nullptr;
|
||||||
|
}
|
||||||
|
if (m_error_notifier) {
|
||||||
|
m_error_notifier->close();
|
||||||
|
m_error_notifier = nullptr;
|
||||||
|
}
|
||||||
|
if (m_easy_handle) {
|
||||||
|
curl_multi_remove_handle(m_multi_handle, m_easy_handle);
|
||||||
|
curl_easy_cleanup(m_easy_handle);
|
||||||
|
m_easy_handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketImplCurl::did_connect()
|
||||||
|
{
|
||||||
|
curl_socket_t socket_fd = CURL_SOCKET_BAD;
|
||||||
|
auto res = curl_easy_getinfo(m_easy_handle, CURLINFO_ACTIVESOCKET, &socket_fd);
|
||||||
|
VERIFY(res == CURLE_OK && socket_fd != CURL_SOCKET_BAD);
|
||||||
|
|
||||||
|
m_read_notifier = Core::Notifier::construct(socket_fd, Core::Notifier::Type::Read);
|
||||||
|
m_read_notifier->on_activation = [this] {
|
||||||
|
u8 buffer[65536];
|
||||||
|
size_t nread = 0;
|
||||||
|
CURLcode const result = curl_easy_recv(m_easy_handle, buffer, sizeof(buffer), &nread);
|
||||||
|
if (result == CURLE_AGAIN)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (result != CURLE_OK) {
|
||||||
|
dbgln("Failed to read from WebSocket: {}", curl_easy_strerror(result));
|
||||||
|
on_connection_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto const err = m_read_buffer.write_until_depleted({ buffer, nread }); err.is_error())
|
||||||
|
on_connection_error();
|
||||||
|
|
||||||
|
on_ready_to_read();
|
||||||
|
};
|
||||||
|
m_error_notifier = Core::Notifier::construct(socket_fd, Core::Notifier::Type::Error | Core::Notifier::Type::HangUp);
|
||||||
|
m_error_notifier->on_activation = [this] {
|
||||||
|
on_connection_error();
|
||||||
|
};
|
||||||
|
|
||||||
|
on_connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
45
Services/RequestServer/WebSocketImplCurl.h
Normal file
45
Services/RequestServer/WebSocketImplCurl.h
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025, Andrew Kaster <andrew@ladybird.org>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/MemoryStream.h>
|
||||||
|
#include <LibCore/Forward.h>
|
||||||
|
#include <LibWebSocket/Impl/WebSocketImpl.h>
|
||||||
|
#include <curl/curl.h>
|
||||||
|
|
||||||
|
namespace RequestServer {
|
||||||
|
|
||||||
|
class WebSocketImplCurl final : public WebSocket::WebSocketImpl {
|
||||||
|
public:
|
||||||
|
virtual ~WebSocketImplCurl() override;
|
||||||
|
|
||||||
|
static NonnullRefPtr<WebSocketImplCurl> create(CURLM*);
|
||||||
|
|
||||||
|
virtual void connect(WebSocket::ConnectionInfo const&) override;
|
||||||
|
virtual bool can_read_line() override;
|
||||||
|
virtual ErrorOr<ByteString> read_line(size_t) override;
|
||||||
|
virtual ErrorOr<ByteBuffer> read(int max_size) override;
|
||||||
|
virtual bool send(ReadonlyBytes) override;
|
||||||
|
virtual bool eof() override;
|
||||||
|
virtual void discard_connection() override;
|
||||||
|
|
||||||
|
virtual bool handshake_complete_when_connected() const override { return true; }
|
||||||
|
|
||||||
|
void did_connect();
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit WebSocketImplCurl(CURLM*);
|
||||||
|
|
||||||
|
CURLM* m_multi_handle { nullptr };
|
||||||
|
CURL* m_easy_handle { nullptr };
|
||||||
|
RefPtr<Core::Notifier> m_read_notifier;
|
||||||
|
RefPtr<Core::Notifier> m_error_notifier;
|
||||||
|
Vector<curl_slist*> m_curl_string_lists;
|
||||||
|
AllocatingMemoryStream m_read_buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -9,7 +9,9 @@
|
||||||
"name": "curl",
|
"name": "curl",
|
||||||
"features": [
|
"features": [
|
||||||
"brotli",
|
"brotli",
|
||||||
"http2"
|
"http2",
|
||||||
|
"ssl",
|
||||||
|
"websockets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue