// Copyright (c) 2026, The Monero Project // 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. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 // THE COPYRIGHT HOLDER OR 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 "websocket.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "byte_slice.h" // monero/contrib/epee/include #include "byte_stream.h" // monero/contrib/epee/include #include "error.h" #include "lwsf_config.h" namespace lwsf { namespace internal { namespace websocket { using const_flat_buffer = boost::beast::net::const_buffer; struct read_loop { std::shared_ptr self_; std::function receive_; void operator()(boost::system::error_code, std::size_t); }; struct subscription : boost::asio::coroutine { std::shared_ptr self_; copyable_slice topic_; std::function receive_; subscription(subscription&&) = default; subscription(const subscription&) = default; explicit subscription(std::shared_ptr self, epee::byte_slice&& topic, std::function&& receive) : self_(std::move(self)), topic_(std::move(topic)), receive_(std::move(receive)) {} void operator()(boost::system::error_code = {}); }; class stream { std::vector> pings_; boost::asio::io_context::strand strand_; boost::asio::steady_timer timer_; boost::beast::multi_buffer buffer_; bool writing_; bool closing_; bool subscribed_; struct on_timeout { std::shared_ptr self_; void operator()(const boost::system::error_code error) const { LWSF_VERIFY(self_); if (!error || error != boost::asio::error::operation_aborted) { self_->notify_pings(boost::asio::error::operation_aborted); self_->async_close(self_); } } }; protected: template auto bind(F&& f) -> decltype(boost::asio::bind_executor(strand_, std::forward(f))) { return boost::asio::bind_executor(strand_, std::forward(f)); } template void do_async_read(T& sock, F f) { sock.async_read(buffer_, bind(std::move(f))); } template void do_async_write(T& sock, const const_flat_buffer buf, F f) { if (closing_) return f(boost::beast::websocket::error::closed); LWSF_VERIFY(f.self_); if (!writing_) { struct on_write { F f_; void operator()(const boost::system::error_code error, std::size_t) { const std::shared_ptr self = f_.self_; LWSF_VERIFY(self); self->timer_.cancel(); self->writing_ = false; f_(error); if (error) { self->notify_pings(error); self->async_close(self); } else if (!self->pings_.empty()) self->async_ping(on_ping{self}); } }; writing_ = true; timer_.expires_after(config::ping_timeout); // timeout for write not response timer_.async_wait(on_timeout{f.self_}); sock.async_write(buf, bind(on_write{std::move(f)})); } else pings_.push_back(std::bind(&stream::async_write, f.self_, std::move(f))); } template void do_async_ping(T& sock, F&& f) { if (closing_) return f(boost::beast::websocket::error::closed); LWSF_VERIFY(!writing_ && f.self_); writing_ = true; timer_.expires_after(config::ping_timeout); timer_.async_wait(on_timeout{f.self_}); sock.async_ping(boost::beast::websocket::ping_data(), bind(std::forward(f))); } template void do_async_close(T& sock, std::shared_ptr self) { LWSF_VERIFY(self); if (!writing_) { const bool run = !closing_; closing_ = true; if (run) sock.async_close(boost::beast::websocket::close_reason(), bind(std::bind(&stream::async_shutdown, self, self))); } else pings_.push_back(std::bind(&stream::async_close, self, self)); } void do_async_shutdown(tcp_sock& sock, std::shared_ptr) { do_cleanup(sock); } void do_async_shutdown(boost::asio::ssl::stream& sock, std::shared_ptr self) { LWSF_VERIFY(self); sock.async_shutdown(bind(std::bind(&stream::cleanup, self))); } void do_cleanup(tcp_sock& sock) { boost::system::error_code ec{}; sock.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); sock.close(ec); } void do_cleanup(boost::asio::ssl::stream& sock) { do_cleanup(sock.next_layer()); } public: explicit stream(boost::asio::io_context& io) : strand_(io), timer_(io), buffer_(), writing_(false), closing_(false), subscribed_(false) {} //! \return True if this function has never been invoked previously bool subscribe() noexcept { const bool rc = !subscribed_; subscribed_ = true; return rc; } template void dispatch(F&& f) { boost::asio::dispatch(strand_, std::forward(f)); } epee::byte_slice gather_buffer() { epee::byte_stream sink{}; sink.reserve(buffer_.size()); const auto buffers = buffer_.cdata(); const auto end = boost::asio::buffer_sequence_end(buffers); for (auto cur = boost::asio::buffer_sequence_begin(buffers); cur != end; ++cur) { const const_flat_buffer this_buf = *cur; sink.write(reinterpret_cast(this_buf.data()), this_buf.size()); } buffer_.consume(buffer_.size()); return epee::byte_slice{std::move(sink)}; } struct on_ping { std::shared_ptr self_; void operator()(const boost::system::error_code& error) { LWSF_VERIFY(self_); self_->writing_ = false; if (self_->timer_.cancel() || error) { self_->notify_pings(error); if (error) self_->async_close(self_); } } }; void notify_pings(const boost::system::error_code error = {}) { std::vector> pings; pings.swap(pings_); for (const auto& ping : pings) ping(error); } bool add_ping_notifier(std::function&& notifier) { pings_.push_back(std::move(notifier)); return !writing_; } virtual void async_read(read_loop&&) = 0; virtual void async_write(subscription) = 0; virtual void async_ping(on_ping&&) = 0; virtual void async_close(std::shared_ptr) = 0; virtual void async_shutdown(std::shared_ptr) = 0; virtual void cleanup() = 0; }; template class stream_ final : public stream { boost::beast::websocket::stream sock_; public: explicit stream_(T&& sock, boost::asio::io_context& io) : stream(io), sock_(std::move(sock)) { sock_.binary(true); } template void async_handshake(const std::string& host, const std::string& target, std::string&& protocol, F f) { namespace http = boost::beast::http; struct on_control { std::weak_ptr self_; void operator()(boost::beast::websocket::frame_type, boost::beast::string_view) { const auto self = self_.lock(); if (self) self->notify_pings(); } }; LWSF_VERIFY(f.self_); sock_.control_callback(on_control{f.self_}); sock_.set_option( boost::beast::websocket::stream_base::decorator( [protocol = std::move(protocol)] (http::request_header<>& hdr) { hdr.set(http::field::sec_websocket_protocol, protocol); } ) ); sock_.async_handshake(host, target, bind(std::move(f))); } virtual void async_read(read_loop&& handler) override final { do_async_read(sock_, std::move(handler)); } virtual void async_write(subscription handler) override final { std::uint8_t const* const data = handler.topic_.data.data(); const std::size_t size = handler.topic_.data.size(); do_async_write(sock_, const_flat_buffer(data, size), std::move(handler)); } virtual void async_ping(on_ping&& handler) override final { do_async_ping(sock_, std::move(handler)); } virtual void async_close(std::shared_ptr self) override final { do_async_close(sock_, std::move(self)); } virtual void async_shutdown(std::shared_ptr self) override final { do_async_shutdown(sock_.next_layer(), std::move(self)); } virtual void cleanup() override final { do_cleanup(sock_.next_layer()); } }; struct after_connect { std::shared_ptr self_; std::function notifier_; void operator()(const boost::system::error_code error) { LWSF_VERIFY(notifier_); if (error) notifier_(error, nullptr); else notifier_(error, self_); } }; void read_loop::operator()(const boost::system::error_code error, std::size_t) { LWSF_VERIFY(self_ && receive_); stream& self = *self_; receive_(error, self.gather_buffer()); if (!error) self.async_read(std::move(*this)); else self.async_close(self_); } void subscription::operator()(boost::system::error_code error) { LWSF_VERIFY(self_ && receive_); stream& self = *self_; BOOST_ASIO_CORO_REENTER(*this) { if (self.subscribe()) BOOST_ASIO_CORO_YIELD self.async_write(std::move(*this)); else error = boost::asio::error::already_started; if (error) { receive_(error, epee::byte_slice{}); self.async_close(std::move(self_)); } else self.async_read(read_loop{std::move(self_), std::move(receive_)}); } } template inline void do_start(std::shared_ptr> self, const std::string& host, const std::string& target, std::string&& protocol, std::function&& notifier) { self->async_handshake(host, target, std::move(protocol), after_connect{self, std::move(notifier)}); } void async_start(tcp_sock&& sock, boost::asio::io_context& io, std::string&& host, std::string&& target, std::string&& protocol, std::function&& notifier) { do_start(std::make_shared>(std::move(sock), io), host, target, std::move(protocol), std::move(notifier)); } void async_start(boost::asio::ssl::stream&& sock, boost::asio::io_context& io, std::string&& host, std::string&& target, std::string&& protocol, std::function&& notifier) { do_start(std::make_shared>>(std::move(sock), io), host, target, std::move(protocol), std::move(notifier)); } void async_subscribe(std::shared_ptr self, epee::byte_slice&& topic, std::function&& receive) { LWSF_VERIFY(self); self->dispatch(subscription{self, std::move(topic), std::move(receive)}); } void async_ping(std::shared_ptr self, std::function&& notifier) { LWSF_VERIFY(self); self->dispatch([self, notifier = std::move(notifier)] () mutable { if(self->add_ping_notifier(std::move(notifier))) self->async_ping(stream::on_ping{self}); }); } void async_close(std::shared_ptr self) { LWSF_VERIFY(self); self->dispatch(std::bind(&stream::async_close, self, self)); } }}} // lwsf // internal // websocket