dncurrency/nano/node/websocket.hpp

383 lines
13 KiB
C++

#pragma once
#include <nano/lib/blocks.hpp>
#include <nano/lib/numbers.hpp>
#include <nano/lib/work.hpp>
#include <nano/node/common.hpp>
#include <nano/node/election.hpp>
#include <nano/node/websocket_stream.hpp>
#include <nano/node/websocketconfig.hpp>
#include <nano/secure/common.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <deque>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace nano
{
class wallets;
class logger_mt;
class vote;
class election_status;
class telemetry_data;
class tls_config;
class node_observers;
enum class election_status_type : uint8_t;
namespace websocket
{
class listener;
class confirmation_options;
class session;
/** Supported topics */
enum class topic
{
invalid = 0,
/** Acknowledgement of prior incoming message */
ack,
/** A confirmation message */
confirmation,
/** Started election message*/
started_election,
/** Stopped election message (dropped elections due to bounding or block lost the elections) */
stopped_election,
/** A vote message **/
vote,
/** Work generation message */
work,
/** A bootstrap message */
bootstrap,
/** A telemetry message */
telemetry,
/** New block arrival message*/
new_unconfirmed_block,
/** Auxiliary length, not a valid topic, must be the last enum */
_length
};
constexpr std::size_t number_topics{ static_cast<std::size_t> (topic::_length) - static_cast<std::size_t> (topic::invalid) };
/** A message queued for broadcasting */
class message final
{
public:
message (nano::websocket::topic topic_a) :
topic (topic_a)
{
}
message (nano::websocket::topic topic_a, boost::property_tree::ptree & tree_a) :
topic (topic_a), contents (tree_a)
{
}
std::string to_string () const;
nano::websocket::topic topic;
boost::property_tree::ptree contents;
};
/** Message builder. This is expanded with new builder functions are necessary. */
class message_builder final
{
public:
message block_confirmed (std::shared_ptr<nano::block> const & block_a, nano::account const & account_a, nano::amount const & amount_a, std::string subtype, bool include_block, nano::election_status const & election_status_a, std::vector<nano::vote_with_weight_info> const & election_votes_a, nano::websocket::confirmation_options const & options_a);
message started_election (nano::block_hash const & hash_a);
message stopped_election (nano::block_hash const & hash_a);
message vote_received (std::shared_ptr<nano::vote> const & vote_a, nano::vote_code code_a);
message work_generation (nano::work_version const version_a, nano::block_hash const & root_a, uint64_t const work_a, uint64_t const difficulty_a, uint64_t const publish_threshold_a, std::chrono::milliseconds const & duration_a, std::string const & peer_a, std::vector<std::string> const & bad_peers_a, bool const completed_a = true, bool const cancelled_a = false);
message work_cancelled (nano::work_version const version_a, nano::block_hash const & root_a, uint64_t const difficulty_a, uint64_t const publish_threshold_a, std::chrono::milliseconds const & duration_a, std::vector<std::string> const & bad_peers_a);
message work_failed (nano::work_version const version_a, nano::block_hash const & root_a, uint64_t const difficulty_a, uint64_t const publish_threshold_a, std::chrono::milliseconds const & duration_a, std::vector<std::string> const & bad_peers_a);
message bootstrap_started (std::string const & id_a, std::string const & mode_a);
message bootstrap_exited (std::string const & id_a, std::string const & mode_a, std::chrono::steady_clock::time_point const start_time_a, uint64_t const total_blocks_a);
message telemetry_received (nano::telemetry_data const &, nano::endpoint const &);
message new_block_arrived (nano::block const & block_a);
private:
/** Set the common fields for messages: timestamp and topic. */
void set_common_fields (message & message_a);
};
/** Options for subscriptions */
class options
{
public:
virtual ~options () = default;
protected:
/**
* Checks if a message should be filtered for default options (no options given).
* @param message_a the message to be checked
* @return false - the message should always be broadcasted
*/
virtual bool should_filter (message const & message_a) const
{
return false;
}
/**
* Update options, if available for a given topic
* @return false on success
*/
virtual bool update (boost::property_tree::ptree const & options_a)
{
return true;
}
friend class session;
};
/**
* Options for block confirmation subscriptions
* Non-filtering options:
* - "include_block" (bool, default true) - if false, do not include block contents. Only account, amount and hash will be included.
* Filtering options:
* - "all_local_accounts" (bool) - will only not filter blocks that have local wallet accounts as source/destination
* - "accounts" (array of std::strings) - will only not filter blocks that have these accounts as source/destination
* @remark Both options can be given, the resulting filter is an intersection of individual filters
* @warn Legacy blocks are always filtered (not broadcasted)
*/
class confirmation_options final : public options
{
public:
confirmation_options (nano::wallets & wallets_a);
confirmation_options (boost::property_tree::ptree const & options_a, nano::wallets & wallets_a, nano::logger_mt & logger_a);
/**
* Checks if a message should be filtered for given block confirmation options.
* @param message_a the message to be checked
* @return false if the message should be broadcasted, true if it should be filtered
*/
bool should_filter (message const & message_a) const override;
/**
* Update some existing options
* Filtering options:
* - "accounts_add" (array of std::strings) - additional accounts for which blocks should not be filtered
* - "accounts_del" (array of std::strings) - accounts for which blocks should be filtered
* @return false
*/
bool update (boost::property_tree::ptree const & options_a) override;
/** Returns whether or not block contents should be included */
bool get_include_block () const
{
return include_block;
}
/** Returns whether or not to include election info, such as tally and duration */
bool get_include_election_info () const
{
return include_election_info;
}
/** Returns whether or not to include election info with votes */
bool get_include_election_info_with_votes () const
{
return include_election_info_with_votes;
}
/** Returns whether or not to include sideband info */
bool get_include_sideband_info () const
{
return include_sideband_info;
}
static constexpr uint8_t const type_active_quorum = 1;
static constexpr uint8_t const type_active_confirmation_height = 2;
static constexpr uint8_t const type_inactive = 4;
static constexpr uint8_t const type_all_active = type_active_quorum | type_active_confirmation_height;
static constexpr uint8_t const type_all = type_all_active | type_inactive;
private:
void check_filter_empty () const;
nano::wallets & wallets;
boost::optional<nano::logger_mt &> logger;
bool include_election_info{ false };
bool include_election_info_with_votes{ false };
bool include_sideband_info{ false };
bool include_block{ true };
bool has_account_filtering_options{ false };
bool all_local_accounts{ false };
uint8_t confirmation_types{ type_all };
std::unordered_set<std::string> accounts;
};
/**
* Filtering options for vote subscriptions
* Possible filtering options:
* * "representatives" (array of std::strings) - will only broadcast votes from these representatives
*/
class vote_options final : public options
{
public:
vote_options (boost::property_tree::ptree const & options_a, nano::logger_mt & logger_a);
/**
* Checks if a message should be filtered for given vote received options.
* @param message_a the message to be checked
* @return false if the message should be broadcasted, true if it should be filtered
*/
bool should_filter (message const & message_a) const override;
private:
std::unordered_set<std::string> representatives;
bool include_replays{ false };
bool include_indeterminate{ false };
};
/** A websocket session managing its own lifetime */
class session final : public std::enable_shared_from_this<session>
{
friend class listener;
public:
#ifdef NANO_SECURE_RPC
/** Constructor that takes ownership over \p socket_a and creates an SSL stream */
explicit session (nano::websocket::listener & listener_a, socket_type socket_a, boost::asio::ssl::context & ctx_a);
#endif
/** Constructor that takes ownership over \p socket_a */
explicit session (nano::websocket::listener & listener_a, socket_type socket_a);
~session ();
/** Perform Websocket handshake and start reading messages */
void handshake ();
/** Close the websocket and end the session */
void close ();
/** Read the next message. This implicitely handles incoming websocket pings. */
void read ();
/** Enqueue \p message_a for writing to the websockets */
void write (nano::websocket::message message_a);
private:
/** The owning listener */
nano::websocket::listener & ws_listener;
/** Websocket stream, supporting both plain and tls connections */
nano::websocket::stream ws;
/** Buffer for received messages */
boost::beast::multi_buffer read_buffer;
/** Outgoing messages. The send queue is protected by accessing it only through the strand */
std::deque<message> send_queue;
/** Hash functor for topic enums */
struct topic_hash
{
template <typename T>
std::size_t operator() (T t) const
{
return static_cast<std::size_t> (t);
}
};
/** Map of subscriptions -> options registered by this session. */
std::unordered_map<topic, std::unique_ptr<options>, topic_hash> subscriptions;
nano::mutex subscriptions_mutex;
/** Handle incoming message */
void handle_message (boost::property_tree::ptree const & message_a);
/** Acknowledge incoming message */
void send_ack (std::string action_a, std::string id_a);
/** Send all queued messages. This must be called from the write strand. */
void write_queued_messages ();
};
/** Creates a new session for each incoming connection */
class listener final : public std::enable_shared_from_this<listener>
{
public:
listener (std::shared_ptr<nano::tls_config> const & tls_config_a, nano::logger_mt & logger_a, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a);
/** Start accepting connections */
void run ();
void accept ();
void on_accept (boost::system::error_code ec_a);
/** Close all websocket sessions and stop listening for new connections */
void stop ();
/** Broadcast block confirmation. The content of the message depends on subscription options (such as "include_block") */
void broadcast_confirmation (std::shared_ptr<nano::block> const & block_a, nano::account const & account_a, nano::amount const & amount_a, std::string const & subtype, nano::election_status const & election_status_a, std::vector<nano::vote_with_weight_info> const & election_votes_a);
/** Broadcast \p message to all session subscribing to the message topic. */
void broadcast (nano::websocket::message message_a);
nano::logger_mt & get_logger () const
{
return logger;
}
std::uint16_t listening_port ()
{
return acceptor.local_endpoint ().port ();
}
nano::wallets & get_wallets () const
{
return wallets;
}
/**
* Per-topic subscribers check. Relies on all sessions correctly increasing and
* decreasing the subscriber counts themselves.
*/
bool any_subscriber (nano::websocket::topic const & topic_a) const
{
return subscriber_count (topic_a) > 0;
}
/** Getter for subscriber count of a specific topic*/
std::size_t subscriber_count (nano::websocket::topic const & topic_a) const
{
return topic_subscriber_count[static_cast<std::size_t> (topic_a)];
}
private:
/** A websocket session can increase and decrease subscription counts. */
friend nano::websocket::session;
/** Adds to subscription count of a specific topic*/
void increase_subscriber_count (nano::websocket::topic const & topic_a);
/** Removes from subscription count of a specific topic*/
void decrease_subscriber_count (nano::websocket::topic const & topic_a);
std::shared_ptr<nano::tls_config> tls_config;
nano::logger_mt & logger;
nano::wallets & wallets;
boost::asio::ip::tcp::acceptor acceptor;
socket_type socket;
nano::mutex sessions_mutex;
std::vector<std::weak_ptr<session>> sessions;
std::array<std::atomic<std::size_t>, number_topics> topic_subscriber_count;
std::atomic<bool> stopped{ false };
};
}
/**
* Wrapper of websocket related functionality that node interacts with
*/
class websocket_server
{
public:
websocket_server (nano::websocket::config &, nano::node_observers &, nano::wallets &, nano::ledger &, boost::asio::io_context &, nano::logger_mt &);
void start ();
void stop ();
private: // Dependencies
nano::websocket::config const & config;
nano::node_observers & observers;
nano::wallets & wallets;
nano::ledger & ledger;
boost::asio::io_context & io_ctx;
nano::logger_mt & logger;
public:
// TODO: Encapsulate, this is public just because existing code needs it
std::shared_ptr<nano::websocket::listener> server;
};
}