diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index becd34d5f4..379a633f03 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -14,16 +14,22 @@ #include #include #include +#include #include +#include #include #include #include +#include #include #include #include #include #include +#include +#include #include +#include namespace { @@ -96,6 +102,130 @@ std::string map_moonraker_state(std::string state) namespace Slic3r { +struct MoonrakerWebsocket::Impl +{ + using PlainWebsocket = websocket::stream; + using SecureWebsocket = websocket::stream>; + using PlainPtr = std::unique_ptr; + using SecurePtr = std::unique_ptr; + + explicit Impl(bool secure, std::string api_key) : secure(secure), api_key(std::move(api_key)), ssl_context(net::ssl::context::tls_client) + { + if (this->secure) { + websocket = std::make_unique(ioc, ssl_context); + } else { + websocket = std::make_unique(beast::tcp_stream{ioc}); + } + } + + bool secure; + std::string api_key; + net::io_context ioc; + net::ssl::context ssl_context; + std::variant websocket; +}; + +MoonrakerWebsocket::MoonrakerWebsocket(bool secure, std::string api_key) : m_impl(std::make_unique(secure, std::move(api_key))) {} + +MoonrakerWebsocket::~MoonrakerWebsocket() = default; + +void MoonrakerWebsocket::connect(const std::string& host, const std::string& port, std::chrono::seconds timeout) +{ + tcp::resolver resolver(m_impl->ioc); + std::visit( + [&](auto& websocket_ptr) { + auto& stream = beast::get_lowest_layer(*websocket_ptr); + stream.expires_after(timeout); + stream.connect(resolver.resolve(host, port)); + }, + m_impl->websocket); +} + +void MoonrakerWebsocket::tls_handshake(const std::string& host) +{ + if (!m_impl->secure) { + return; + } + + auto& websocket = *std::get(m_impl->websocket); + auto& tls_stream = websocket.next_layer(); + if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), host.c_str())) { + throw std::runtime_error("Moonraker WSS: failed to set TLS server name"); + } + + // Match Http's existing printer-host behavior: encrypt the connection + // while accepting self-signed/local printer certificates. + tls_stream.set_verify_mode(net::ssl::verify_none); + tls_stream.handshake(net::ssl::stream_base::client); +} + +void MoonrakerWebsocket::handshake(const std::string& host, const std::string& target) +{ + std::visit( + [&](auto& websocket_ptr) { + websocket_ptr->set_option(websocket::stream_base::decorator([api_key = m_impl->api_key](websocket::request_type& req) { + req.set(http::field::user_agent, "OrcaSlicer"); + if (!api_key.empty()) { + req.set("X-Api-Key", api_key); + } + })); + websocket_ptr->handshake(host, target); + }, + m_impl->websocket); +} + +void MoonrakerWebsocket::text(bool enabled) +{ + std::visit([&](auto& websocket_ptr) { websocket_ptr->text(enabled); }, m_impl->websocket); +} + +void MoonrakerWebsocket::write(const std::string& body) +{ + std::visit([&](auto& websocket_ptr) { websocket_ptr->write(net::buffer(body)); }, m_impl->websocket); +} + +MoonrakerWebsocket::ReadResult MoonrakerWebsocket::read(std::string& payload, std::string& error_message) +{ + beast::flat_buffer buffer; + beast::error_code error; + std::visit([&](auto& websocket_ptr) { websocket_ptr->read(buffer, error); }, m_impl->websocket); + + if (error == beast::error::timeout) { + return ReadResult::timeout; + } + if (error == websocket::error::closed) { + return ReadResult::closed; + } + if (error) { + error_message = error.message(); + return ReadResult::error; + } + + payload = beast::buffers_to_string(buffer.data()); + return ReadResult::message; +} + +void MoonrakerWebsocket::close() +{ + beast::error_code error; + std::visit([&](auto& websocket_ptr) { websocket_ptr->close(websocket::close_code::normal, error); }, m_impl->websocket); +} + +void MoonrakerWebsocket::expires_after(std::chrono::seconds timeout) +{ + std::visit([&](auto& websocket_ptr) { beast::get_lowest_layer(*websocket_ptr).expires_after(timeout); }, m_impl->websocket); +} + +void MoonrakerWebsocket::abort() +{ + std::visit( + [](auto& websocket_ptr) { + beast::error_code error; + beast::get_lowest_layer(*websocket_ptr).socket().shutdown(tcp::socket::shutdown_both, error); + }, + m_impl->websocket); +} + const std::string MoonrakerPrinterAgent_VERSION = "1.0.0"; bool moonraker_is_light_name(const std::string& name) @@ -217,12 +347,6 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip"; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } - - // why: Moonraker/print-host serves plain HTTP (nginx :80 or Moonraker :7125), never - // https:443; MachineObject::connect defaults use_ssl=true -> forced https -> refused. - // Pin http. (matches feature/printer-agent-port-pristine) - use_ssl = false; - std::string base_url; std::string api_key; uint64_t gen; @@ -1567,7 +1691,7 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman } WsEndpoint endpoint; - if (!parse_ws_endpoint(base_url, endpoint) || endpoint.secure) { + if (!parse_ws_endpoint(base_url, endpoint)) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: send_ws_rpc has no usable websocket for base_url=" << base_url; return false; @@ -1589,36 +1713,26 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman for (const auto& port : ports) { try { - net::io_context ioc; - tcp::resolver resolver{ioc}; - beast::tcp_stream stream{ioc}; - stream.expires_after(std::chrono::seconds(5)); - stream.connect(resolver.resolve(endpoint.host, port)); - - websocket::stream ws{std::move(stream)}; - ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { - req.set(http::field::user_agent, "OrcaSlicer"); - if (!api_key.empty()) { - req.set("X-Api-Key", api_key); - } - })); + MoonrakerWebsocket ws{endpoint.secure, api_key}; + ws.connect(endpoint.host, port, std::chrono::seconds(5)); + ws.tls_handshake(endpoint.host); std::string host_header = endpoint.host; - if (!port.empty() && port != "80") { + if (!port.empty() && port != (endpoint.secure ? "443" : "80")) { host_header += ":" + port; } ws.handshake(host_header, endpoint.target); ws.text(true); - ws.write(net::buffer(body)); + ws.write(body); - ws.next_layer().expires_after(std::chrono::seconds(2)); - beast::flat_buffer buffer; - beast::error_code read_ec; - ws.read(buffer, read_ec); + ws.expires_after(std::chrono::seconds(2)); + std::string response; + std::string read_error; + ws.read(response, read_error); - beast::error_code close_ec; - ws.close(websocket::close_code::normal, close_ec); - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over ws to " + ws.close(); + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over " + << (endpoint.secure ? "wss" : "ws") << " to " << endpoint.host << ":" << port; return true; } catch (const std::exception& e) { @@ -2070,11 +2184,6 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket endpoint invalid for base_url=" << base_url; return; } - if (endpoint.secure) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket wss not supported for base_url=" << base_url; - return; - } - // Reconnection logic ws_reconnect_requested.store(false); // Reset reconnect flag int retry_count = 0; @@ -2085,15 +2194,9 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect try { - net::io_context ioc; - tcp::resolver resolver{ioc}; - beast::tcp_stream stream{ioc}; - - stream.expires_after(std::chrono::seconds(10)); - auto const results = resolver.resolve(endpoint.host, endpoint.port); - stream.connect(results); - - websocket::stream ws{std::move(stream)}; + MoonrakerWebsocket ws{endpoint.secure, api_key}; + ws.connect(endpoint.host, endpoint.port, std::chrono::seconds(10)); + ws.tls_handshake(endpoint.host); // Allow stop_status_stream() to force this socket shut so a blocked // synchronous ws.read()/ws.write() returns with an error (Beast's @@ -2108,20 +2211,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba { std::lock_guard lock(ws_abort_mutex); ws_abort_io = [&ws] { - beast::error_code ec; - ws.next_layer().socket().shutdown(tcp::socket::shutdown_both, ec); + ws.abort(); }; } - ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) { - req.set(http::field::user_agent, "OrcaSlicer"); - if (!api_key.empty()) { - req.set("X-Api-Key", api_key); - } - })); - std::string host_header = endpoint.host; - if (!endpoint.port.empty() && endpoint.port != "80") { + if (!endpoint.port.empty() && endpoint.port != (endpoint.secure ? "443" : "80")) { host_header += ":" + endpoint.port; } ws.handshake(host_header, endpoint.target); @@ -2136,7 +2231,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba identify["params"]["type"] = "agent"; identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer"; identify["id"] = 0; - ws.write(net::buffer(identify.dump())); + ws.write(identify.dump()); std::set subscribe_objects = {"print_stats", "virtual_sdcard"}; std::set available_objects; @@ -2188,7 +2283,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba } subscribe["params"]["objects"] = std::move(objects); subscribe["id"] = 1; - ws.write(net::buffer(subscribe.dump())); + ws.write(subscribe.dump()); // Eager fetch so AMS data is available immediately after connecting, // without waiting on the loop's own refresh clock below. @@ -2200,11 +2295,11 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba while (!ws_stop.load()) { on_status_loop_tick(dev_id); - ws.next_layer().expires_after(std::chrono::seconds(2)); - beast::flat_buffer buffer; - beast::error_code ec; - ws.read(buffer, ec); - if (ec == beast::error::timeout) { + ws.expires_after(std::chrono::seconds(2)); + std::string payload; + std::string read_error; + const auto read_result = ws.read(payload, read_error); + if (read_result == MoonrakerWebsocket::ReadResult::timeout) { const auto now_ms = static_cast( std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); const auto last_ms = ws_last_emit_ms.load(); @@ -2219,12 +2314,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba } continue; } - if (ec == websocket::error::closed) { + if (read_result == MoonrakerWebsocket::ReadResult::closed) { connection_lost = true; break; } - if (ec) { - BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << ec.message(); + if (read_result == MoonrakerWebsocket::ReadResult::error) { + BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << read_error; connection_lost = true; break; } @@ -2239,7 +2334,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba ams_last_fetch_ms.store(now_ms); } } - handle_ws_message(dev_id, beast::buffers_to_string(buffer.data()), base_url, api_key); + handle_ws_message(dev_id, std::move(payload), base_url, api_key); // Check if handle_ws_message triggered reconnection request` if (ws_reconnect_requested.exchange(false)) { connection_lost = true; @@ -2247,8 +2342,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba } } - beast::error_code ec; - ws.close(websocket::close_code::normal, ec); + ws.close(); // Only reset retry count on clean shutdown (not connection_lost) if (!connection_lost && !ws_stop.load()) { diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index ab9f2dcb94..e7b97663ba 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,35 @@ namespace Slic3r { bool moonraker_is_light_name(const std::string& name); +class MoonrakerWebsocket +{ +public: + enum class ReadResult + { + message, + timeout, + closed, + error, + }; + + MoonrakerWebsocket(bool secure, std::string api_key); + ~MoonrakerWebsocket(); + + void connect(const std::string& host, const std::string& port, std::chrono::seconds timeout); + void tls_handshake(const std::string& host); + void handshake(const std::string& host, const std::string& target); + void text(bool enabled); + void write(const std::string& body); + ReadResult read(std::string& payload, std::string& error_message); + void close(); + void expires_after(std::chrono::seconds timeout); + void abort(); + +private: + struct Impl; + std::unique_ptr m_impl; +}; + class MoonrakerPrinterAgent : public IPrinterAgent { public: