xplat/FlipperWebSocket/WebSocketClient.cpp (200 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #ifdef FB_SONARKIT_ENABLED #include "WebSocketClient.h" #include <Flipper/ConnectionContextStore.h> #include <Flipper/FlipperTransportTypes.h> #include <Flipper/FlipperURLSerializer.h> #include <Flipper/Log.h> #include <folly/String.h> #include <folly/futures/Future.h> #include <folly/io/async/AsyncSocketException.h> #include <folly/io/async/SSLContext.h> #include <folly/json.h> #include <websocketpp/common/memory.hpp> #include <websocketpp/common/thread.hpp> #include <cctype> #include <iomanip> #include <sstream> #include <stdexcept> #include <string> #include <thread> namespace facebook { namespace flipper { WebSocketClient::WebSocketClient( FlipperConnectionEndpoint endpoint, std::unique_ptr<FlipperSocketBasePayload> payload, folly::EventBase* eventBase) : WebSocketClient( std::move(endpoint), std::move(payload), eventBase, nullptr) {} WebSocketClient::WebSocketClient( FlipperConnectionEndpoint endpoint, std::unique_ptr<FlipperSocketBasePayload> payload, folly::EventBase* eventBase, ConnectionContextStore* connectionContextStore) : BaseClient( std::move(endpoint), std::move(payload), eventBase, connectionContextStore) { status_ = Status::Unconnected; socket_.clear_access_channels(websocketpp::log::alevel::all); socket_.clear_error_channels(websocketpp::log::elevel::all); socket_.init_asio(); socket_.start_perpetual(); thread_ = websocketpp::lib::make_shared<websocketpp::lib::thread>( &SocketClient::run, &socket_); } WebSocketClient::~WebSocketClient() { disconnect(); } bool WebSocketClient::connect(FlipperConnectionManager* manager) { if (status_ != Status::Unconnected) { return false; } status_ = Status::Connecting; std::string connectionURL = endpoint_.secure ? "wss://" : "ws://"; connectionURL += endpoint_.host; connectionURL += ":"; connectionURL += std::to_string(endpoint_.port); auto serializer = URLSerializer{}; payload_->serialize(serializer); auto payload = serializer.serialize(); if (payload.size()) { connectionURL += "/?"; connectionURL += payload; } auto uri = websocketpp::lib::make_shared<websocketpp::uri>(connectionURL); websocketpp::lib::error_code ec; connection_ = socket_.get_connection(uri, ec); if (ec) { status_ = Status::Failed; return false; } handle_ = connection_->get_handle(); connection_->set_open_handler(websocketpp::lib::bind( &WebSocketClient::onOpen, this, &socket_, websocketpp::lib::placeholders::_1)); connection_->set_message_handler(websocketpp::lib::bind( &WebSocketClient::onMessage, this, &socket_, websocketpp::lib::placeholders::_1, websocketpp::lib::placeholders::_2)); connection_->set_fail_handler(websocketpp::lib::bind( &WebSocketClient::onFail, this, &socket_, websocketpp::lib::placeholders::_1)); connection_->set_close_handler(websocketpp::lib::bind( &WebSocketClient::onClose, this, &socket_, websocketpp::lib::placeholders::_1)); auto connected = connected_.get_future(); socket_.connect(connection_); auto state = connected.wait_for(std::chrono::seconds(10)); if (state == std::future_status::ready) { return connected.get(); } disconnect(); return false; } void WebSocketClient::disconnect() { socket_.stop_perpetual(); if (status_ == Status::Connecting || status_ == Status::Open || status_ == Status::Failed) { websocketpp::lib::error_code ec; socket_.close(handle_, websocketpp::close::status::going_away, "", ec); } socket_.stop(); status_ = Status::Closed; if (thread_ && thread_->joinable()) { thread_->join(); } thread_ = nullptr; eventBase_->add( [eventHandler = eventHandler_]() { eventHandler(SocketEvent::CLOSE); }); } void WebSocketClient::send( const folly::dynamic& message, SocketSendHandler completion) { std::string json = folly::toJson(message); send(json, std::move(completion)); } void WebSocketClient::send( const std::string& message, SocketSendHandler completion) { websocketpp::lib::error_code ec; socket_.send( handle_, &message[0], message.size(), websocketpp::frame::opcode::text, ec); completion(); } /** Only ever used for insecure connections to receive the device_id from a signCertificate request. If the intended usage ever changes, then a better approach needs to be put in place. */ void WebSocketClient::sendExpectResponse( const std::string& message, SocketSendExpectResponseHandler completion) { connection_->set_message_handler( [completion, eventBase = eventBase_]( websocketpp::connection_hdl hdl, SocketClient::message_ptr msg) { const std::string& payload = msg->get_payload(); eventBase->add([completion, payload] { completion(payload, false); }); }); websocketpp::lib::error_code ec; socket_.send( handle_, &message[0], message.size(), websocketpp::frame::opcode::text, ec); if (ec) { auto reason = ec.message(); completion(reason, true); } } void WebSocketClient::onOpen(SocketClient* c, websocketpp::connection_hdl hdl) { if (status_ == Status::Connecting) { connected_.set_value(true); } status_ = Status::Initializing; eventBase_->add( [eventHandler = eventHandler_]() { eventHandler(SocketEvent::OPEN); }); } void WebSocketClient::onMessage( SocketClient* c, websocketpp::connection_hdl hdl, SocketClient::message_ptr msg) { const std::string& payload = msg->get_payload(); if (messageHandler_) { eventBase_->add([payload, messageHandler = messageHandler_]() { messageHandler(payload); }); } } void WebSocketClient::onFail(SocketClient* c, websocketpp::connection_hdl hdl) { SocketClient::connection_ptr con = c->get_con_from_hdl(hdl); auto server = con->get_response_header("Server"); auto reason = con->get_ec().message(); if (status_ == Status::Connecting) { connected_.set_value(false); } status_ = Status::Failed; eventBase_->add( [eventHandler = eventHandler_]() { eventHandler(SocketEvent::ERROR); }); } void WebSocketClient::onClose( SocketClient* c, websocketpp::connection_hdl hdl) { status_ = Status::Closed; eventBase_->add( [eventHandler = eventHandler_]() { eventHandler(SocketEvent::CLOSE); }); } } // namespace flipper } // namespace facebook #endif