IPC 2.0 (#2487)
* IPC 2.0 * Use nano::locked for confirmation subscriber vector * Remove unused local * Update toml tests, fix some const issues * Some access permission improvements * Comments and nano::locked improvements * Guilherme review feedback: formatting, advanced cmake flags, disallow deny for roles * Wesley feedback * Try to please win build on CI * Add generated file to git ignore * install api/flatbuffers/* depending on platform * correct path for api in MacOS app * add api to docker image Co-authored-by: Russel Waters <vaelstrom@gmail.com>
This commit is contained in:
parent
8c3ae38f1c
commit
9d9d377fce
55 changed files with 3390 additions and 473 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -63,6 +63,9 @@ qrc_resources.cpp
|
|||
qt_system
|
||||
resources.qrc.depends
|
||||
|
||||
# Autogenerated Flatbuffers source files
|
||||
nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h
|
||||
|
||||
# CMake artifacts
|
||||
_CPack_Packages
|
||||
CPack*
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -21,3 +21,6 @@
|
|||
[submodule "nano-pow-server"]
|
||||
path = nano-pow-server
|
||||
url = https://github.com/nanocurrency/nano-pow-server.git
|
||||
[submodule "flatbuffers"]
|
||||
path = flatbuffers
|
||||
url = https://github.com/google/flatbuffers.git
|
||||
|
|
|
@ -235,6 +235,9 @@ endif ()
|
|||
include_directories(cpptoml/include)
|
||||
add_subdirectory(crypto/ed25519-donna)
|
||||
|
||||
add_subdirectory(nano/ipc_flatbuffers_lib)
|
||||
add_subdirectory(nano/ipc_flatbuffers_test)
|
||||
|
||||
set (UPNPC_BUILD_SHARED OFF CACHE BOOL "")
|
||||
add_subdirectory (miniupnp/miniupnpc EXCLUDE_FROM_ALL)
|
||||
# FIXME: This fixes miniupnpc include directories without modifying miniupnpc's
|
||||
|
|
256
api/flatbuffers/nanoapi.fbs
Executable file
256
api/flatbuffers/nanoapi.fbs
Executable file
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
Flatbuffer schema for the Nano IPC API.
|
||||
Please see https://google.github.io/flatbuffers/md__schemas.html for recommended schema evolution practices.
|
||||
*/
|
||||
namespace nanoapi;
|
||||
|
||||
/** Returns the voting weight for the given account */
|
||||
table AccountWeight {
|
||||
/** A nano_ address */
|
||||
account: string (required);
|
||||
}
|
||||
|
||||
/** Response to AccountWeight */
|
||||
table AccountWeightResponse {
|
||||
/** Voting weight as a decimal number*/
|
||||
voting_weight: string (required);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block subtype for state blocks.
|
||||
* Note that the node makes no distinction between open and receive subtypes.
|
||||
*/
|
||||
enum BlockSubType: byte {
|
||||
invalid = 0,
|
||||
receive,
|
||||
send,
|
||||
change,
|
||||
epoch
|
||||
}
|
||||
|
||||
/** Block union */
|
||||
union Block {
|
||||
BlockState,
|
||||
BlockOpen,
|
||||
BlockReceive,
|
||||
BlockSend,
|
||||
BlockChange
|
||||
}
|
||||
|
||||
table BlockOpen {
|
||||
/** Hash of this block */
|
||||
hash: string;
|
||||
/** Account being opened */
|
||||
account: string;
|
||||
/** Hash of send block */
|
||||
source: string;
|
||||
/** Representative address */
|
||||
representative: string;
|
||||
/** Signature as a hex string */
|
||||
signature: string;
|
||||
/** Work is a hex string representing work. This is a string as the work value may not fit in native numeric types. */
|
||||
work: string;
|
||||
}
|
||||
|
||||
table BlockReceive {
|
||||
/** Hash of this block */
|
||||
hash: string;
|
||||
/** Hash of previous block */
|
||||
previous: string;
|
||||
/** Source hash */
|
||||
source: string;
|
||||
/** Signature as a hex string */
|
||||
signature: string;
|
||||
/** Work is a hex string representing work. This is a string as the work value may not fit in native numeric types. */
|
||||
work: string;
|
||||
}
|
||||
|
||||
table BlockSend {
|
||||
/** Hash of this block */
|
||||
hash: string;
|
||||
/** Hash of previous block */
|
||||
previous: string;
|
||||
/** Destination account */
|
||||
destination: string;
|
||||
/** Balance in raw */
|
||||
balance: string;
|
||||
/** Signature as a hex string */
|
||||
signature: string;
|
||||
/** Work is a hex string representing work. This is a string as the work value may not fit in native numeric types. */
|
||||
work: string;
|
||||
}
|
||||
|
||||
table BlockChange {
|
||||
/** Hash of this block */
|
||||
hash: string;
|
||||
/** Hash of previous block */
|
||||
previous: string;
|
||||
/** Representative address */
|
||||
representative: string;
|
||||
/** Signature as a hex string */
|
||||
signature: string;
|
||||
/** Work is a hex string representing work. This is a string as the work value may not fit in native numeric types. */
|
||||
work: string;
|
||||
}
|
||||
|
||||
table BlockState {
|
||||
/** Hash of this block */
|
||||
hash: string;
|
||||
/** Account as nano_ string */
|
||||
account: string;
|
||||
/** Hash of previous block */
|
||||
previous: string;
|
||||
/** Representative as nano_ string */
|
||||
representative: string;
|
||||
/** Balance in raw */
|
||||
balance: string;
|
||||
/** Link as a hex string */
|
||||
link: string;
|
||||
/** Link interpreted as a nano_ address */
|
||||
link_as_account: string;
|
||||
/** Signature as a hex string */
|
||||
signature: string;
|
||||
/** Work is a hex string representing work. This is a string as the work value may not fit in native numeric types. */
|
||||
work: string;
|
||||
/** Subtype of this state block */
|
||||
subtype: BlockSubType;
|
||||
}
|
||||
|
||||
/** Information about a block */
|
||||
table BlockInfo {
|
||||
block: Block;
|
||||
}
|
||||
|
||||
/** Called by a service (usually an external process) to register itself */
|
||||
table ServiceRegister {
|
||||
service_name: string;
|
||||
}
|
||||
|
||||
/** Request the node to send an EventServiceStop event to the given service */
|
||||
table ServiceStop {
|
||||
/** Name of service to stop. */
|
||||
service_name: string (required);
|
||||
/** If true, restart the service */
|
||||
restart: bool = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe or unsubscribe to EventServiceStop events.
|
||||
* The service must first have registered itself on the same session.
|
||||
*/
|
||||
table TopicServiceStop {
|
||||
/** Set to true to unsubscribe */
|
||||
unsubscribe: bool;
|
||||
}
|
||||
|
||||
/** Sent to a service to request it to stop itself */
|
||||
table EventServiceStop {
|
||||
}
|
||||
|
||||
/**
|
||||
* All subscriptions are acknowledged. Use the envelope's correlation id
|
||||
* if you need to match the ack with the subscription.
|
||||
*/
|
||||
table EventAck {
|
||||
}
|
||||
|
||||
/** Requested confirmation type */
|
||||
enum TopicConfirmationTypeFilter : byte { all, active, active_quorum, active_confirmation_height, inactive }
|
||||
|
||||
/** Type of block confirmation */
|
||||
enum TopicConfirmationType : byte { active_quorum, active_confirmation_height, inactive }
|
||||
|
||||
/** Subscribe or unsubscribe to block confirmations of type EventConfirmation */
|
||||
table TopicConfirmation {
|
||||
/** Set to true to unsubscribe */
|
||||
unsubscribe: bool;
|
||||
options: TopicConfirmationOptions;
|
||||
}
|
||||
|
||||
table TopicConfirmationOptions {
|
||||
confirmation_type_filter: TopicConfirmationTypeFilter = all;
|
||||
all_local_accounts: bool;
|
||||
accounts: [string];
|
||||
include_block: bool = true;
|
||||
include_election_info: bool = true;
|
||||
}
|
||||
|
||||
/** Notification of block confirmation. */
|
||||
table EventConfirmation {
|
||||
confirmation_type: TopicConfirmationType;
|
||||
account: string;
|
||||
amount: string;
|
||||
hash: string;
|
||||
block: Block;
|
||||
election_info: ElectionInfo;
|
||||
}
|
||||
|
||||
table ElectionInfo {
|
||||
duration: uint64;
|
||||
time: uint64;
|
||||
tally: string;
|
||||
request_count: uint64;
|
||||
block_count: uint64;
|
||||
voter_count: uint64;
|
||||
}
|
||||
|
||||
/** Error response. All fields are optional */
|
||||
table Error {
|
||||
/** Error code. May be negative or positive. */
|
||||
code: int;
|
||||
/** Error category code */
|
||||
category: int;
|
||||
/** Error message */
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A general purpose success response for messages that don't return a message.
|
||||
* The response includes an optional message text.
|
||||
*/
|
||||
table Success {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** IsAlive request and response. Any node issues will be reported through an error in the envelope. */
|
||||
table IsAlive {
|
||||
}
|
||||
|
||||
/**
|
||||
* A union is the idiomatic way in Flatbuffers to transmit messages of multiple types.
|
||||
* All top-level message types (including response types) must be listed here.
|
||||
* @warning To ensure compatibility, only append and deprecate message types.
|
||||
*/
|
||||
union Message {
|
||||
Error,
|
||||
Success,
|
||||
IsAlive,
|
||||
EventAck,
|
||||
BlockInfo,
|
||||
AccountWeight,
|
||||
AccountWeightResponse,
|
||||
TopicConfirmation,
|
||||
EventConfirmation,
|
||||
ServiceRegister,
|
||||
ServiceStop,
|
||||
TopicServiceStop,
|
||||
EventServiceStop
|
||||
}
|
||||
|
||||
/**
|
||||
* All messages are wrapped in an envelope which contains information such as
|
||||
* message type, credentials and correlation id. For responses, the message may be an Error.
|
||||
*/
|
||||
table Envelope {
|
||||
/** Milliseconds since epoch when the message was sent. */
|
||||
time: uint64;
|
||||
/** An optional and arbitrary string used for authentication. The corresponding http header for api keys is "nano-api-key" */
|
||||
credentials: string;
|
||||
/** Correlation id is an optional and arbitrary string. The corresponding http header is "nano-correlation-id" */
|
||||
correlation_id: string;
|
||||
/** The contained message. A 'message_type' property will be automatically added to JSON messages. */
|
||||
message: Message;
|
||||
}
|
||||
|
||||
/** The Envelope is the type marshalled over IPC, and also serves as the top-level JSON type */
|
||||
root_type Envelope;
|
|
@ -24,6 +24,7 @@ RUN groupadd --gid 1000 nanocurrency && \
|
|||
COPY --from=0 /tmp/build/nano_node /usr/bin
|
||||
COPY --from=0 /tmp/build/nano_rpc /usr/bin
|
||||
COPY --from=0 /tmp/build/nano_pow_server /usr/bin
|
||||
COPY --from=0 /tmp/src/api/ /usr/bin/api/
|
||||
COPY --from=0 /etc/nano-network /etc
|
||||
COPY docker/node/entry.sh /usr/bin/entry.sh
|
||||
COPY docker/node/config /usr/share/nano/config
|
||||
|
|
1
flatbuffers
Submodule
1
flatbuffers
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 3b458f7a170154ed4c4a3a2a9f6116fb2d415ad5
|
|
@ -1,6 +1,8 @@
|
|||
#include <nano/core_test/testutil.hpp>
|
||||
#include <nano/lib/ipc_client.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/lib/tomlconfig.hpp>
|
||||
#include <nano/node/ipc/ipc_access_config.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/testing.hpp>
|
||||
#include <nano/rpc/rpc.hpp>
|
||||
|
||||
|
@ -24,7 +26,7 @@ TEST (ipc, asynchronous)
|
|||
nano::ipc::ipc_server ipc (*system.nodes[0], node_rpc_config);
|
||||
nano::ipc::ipc_client client (system.nodes[0]->io_ctx);
|
||||
|
||||
auto req (nano::ipc::prepare_request (nano::ipc::payload_encoding::json_legacy, std::string (R"({"action": "block_count"})")));
|
||||
auto req (nano::ipc::prepare_request (nano::ipc::payload_encoding::json_v1, std::string (R"({"action": "block_count"})")));
|
||||
auto res (std::make_shared<std::vector<uint8_t>> ());
|
||||
std::atomic<bool> call_completed{ false };
|
||||
client.async_connect ("::1", 24077, [&client, &req, &res, &call_completed](nano::error err) {
|
||||
|
@ -56,6 +58,7 @@ TEST (ipc, asynchronous)
|
|||
{
|
||||
ASSERT_NO_ERROR (system.poll ());
|
||||
}
|
||||
ipc.stop ();
|
||||
}
|
||||
|
||||
TEST (ipc, synchronous)
|
||||
|
@ -71,7 +74,7 @@ TEST (ipc, synchronous)
|
|||
std::atomic<bool> call_completed{ false };
|
||||
std::thread client_thread ([&client, &call_completed]() {
|
||||
client.connect ("::1", 24077);
|
||||
std::string response (nano::ipc::request (client, std::string (R"({"action": "block_count"})")));
|
||||
std::string response (nano::ipc::request (nano::ipc::payload_encoding::json_v1, client, std::string (R"({"action": "block_count"})")));
|
||||
std::stringstream ss;
|
||||
ss << response;
|
||||
// Make sure the response is valid json
|
||||
|
@ -88,6 +91,7 @@ TEST (ipc, synchronous)
|
|||
{
|
||||
ASSERT_NO_ERROR (system.poll ());
|
||||
}
|
||||
ipc.stop ();
|
||||
}
|
||||
|
||||
TEST (ipc, config_upgrade_v0_v1)
|
||||
|
@ -108,3 +112,107 @@ TEST (ipc, config_upgrade_v0_v1)
|
|||
ASSERT_LE (1, local2.get<int> ("version"));
|
||||
ASSERT_FALSE (local2.get<bool> ("allow_unsafe"));
|
||||
}
|
||||
|
||||
TEST (ipc, permissions_default_user)
|
||||
{
|
||||
// Test empty/nonexistant access config. The default user still exists with default permissions.
|
||||
std::stringstream ss;
|
||||
ss << R"toml(
|
||||
)toml";
|
||||
|
||||
nano::tomlconfig toml;
|
||||
toml.read (ss);
|
||||
|
||||
nano::ipc::access access;
|
||||
access.deserialize_toml (toml);
|
||||
ASSERT_TRUE (access.has_access ("", nano::ipc::access_permission::api_account_weight));
|
||||
}
|
||||
|
||||
TEST (ipc, permissions_deny_default)
|
||||
{
|
||||
// All users have api_account_weight permissions by default. This removes the permission for a specific user.
|
||||
std::stringstream ss;
|
||||
ss << R"toml(
|
||||
[[user]]
|
||||
id = "user1"
|
||||
deny = "api_account_weight"
|
||||
)toml";
|
||||
|
||||
nano::tomlconfig toml;
|
||||
toml.read (ss);
|
||||
|
||||
nano::ipc::access access;
|
||||
access.deserialize_toml (toml);
|
||||
ASSERT_FALSE (access.has_access ("user1", nano::ipc::access_permission::api_account_weight));
|
||||
}
|
||||
|
||||
TEST (ipc, permissions_groups)
|
||||
{
|
||||
// Make sure role permissions are adopted by user
|
||||
std::stringstream ss;
|
||||
ss << R"toml(
|
||||
[[role]]
|
||||
id = "mywalletadmin"
|
||||
allow = "wallet_read, wallet_write"
|
||||
|
||||
[[user]]
|
||||
id = "user1"
|
||||
roles = "mywalletadmin"
|
||||
deny = "api_account_weight"
|
||||
)toml";
|
||||
|
||||
nano::tomlconfig toml;
|
||||
toml.read (ss);
|
||||
|
||||
nano::ipc::access access;
|
||||
access.deserialize_toml (toml);
|
||||
ASSERT_FALSE (access.has_access ("user1", nano::ipc::access_permission::api_account_weight));
|
||||
ASSERT_TRUE (access.has_access_to_all ("user1", { nano::ipc::access_permission::wallet_read, nano::ipc::access_permission::wallet_write }));
|
||||
}
|
||||
|
||||
TEST (ipc, permissions_oneof)
|
||||
{
|
||||
// Test one of two permissions
|
||||
std::stringstream ss;
|
||||
ss << R"toml(
|
||||
[[user]]
|
||||
id = "user1"
|
||||
allow = "api_account_weight"
|
||||
[[user]]
|
||||
id = "user2"
|
||||
allow = "api_account_weight, account_query"
|
||||
[[user]]
|
||||
id = "user3"
|
||||
deny = "api_account_weight, account_query"
|
||||
)toml";
|
||||
|
||||
nano::tomlconfig toml;
|
||||
toml.read (ss);
|
||||
|
||||
nano::ipc::access access;
|
||||
access.deserialize_toml (toml);
|
||||
ASSERT_TRUE (access.has_access ("user1", nano::ipc::access_permission::api_account_weight));
|
||||
ASSERT_TRUE (access.has_access ("user2", nano::ipc::access_permission::api_account_weight));
|
||||
ASSERT_FALSE (access.has_access ("user3", nano::ipc::access_permission::api_account_weight));
|
||||
ASSERT_TRUE (access.has_access_to_oneof ("user1", { nano::ipc::access_permission::account_query, nano::ipc::access_permission::api_account_weight }));
|
||||
ASSERT_TRUE (access.has_access_to_oneof ("user2", { nano::ipc::access_permission::account_query, nano::ipc::access_permission::api_account_weight }));
|
||||
ASSERT_FALSE (access.has_access_to_oneof ("user3", { nano::ipc::access_permission::account_query, nano::ipc::access_permission::api_account_weight }));
|
||||
}
|
||||
|
||||
TEST (ipc, permissions_default_user_order)
|
||||
{
|
||||
// If changing the default user, it must come first
|
||||
std::stringstream ss;
|
||||
ss << R"toml(
|
||||
[[user]]
|
||||
id = "user1"
|
||||
[[user]]
|
||||
id = ""
|
||||
)toml";
|
||||
|
||||
nano::tomlconfig toml;
|
||||
toml.read (ss);
|
||||
|
||||
nano::ipc::access access;
|
||||
ASSERT_TRUE (access.deserialize_toml (toml));
|
||||
}
|
||||
|
|
|
@ -225,6 +225,8 @@ TEST (toml, daemon_config_deserialize_defaults)
|
|||
ASSERT_EQ (conf.node.ipc_config.transport_tcp.io_timeout, defaults.node.ipc_config.transport_tcp.io_timeout);
|
||||
ASSERT_EQ (conf.node.ipc_config.transport_tcp.io_threads, defaults.node.ipc_config.transport_tcp.io_threads);
|
||||
ASSERT_EQ (conf.node.ipc_config.transport_tcp.port, defaults.node.ipc_config.transport_tcp.port);
|
||||
ASSERT_EQ (conf.node.ipc_config.flatbuffers.skip_unexpected_fields_in_json, defaults.node.ipc_config.flatbuffers.skip_unexpected_fields_in_json);
|
||||
ASSERT_EQ (conf.node.ipc_config.flatbuffers.verify_buffers, defaults.node.ipc_config.flatbuffers.verify_buffers);
|
||||
|
||||
ASSERT_EQ (conf.node.diagnostics_config.txn_tracking.enable, defaults.node.diagnostics_config.txn_tracking.enable);
|
||||
ASSERT_EQ (conf.node.diagnostics_config.txn_tracking.ignore_writes_below_block_processor_max_time, defaults.node.diagnostics_config.txn_tracking.ignore_writes_below_block_processor_max_time);
|
||||
|
@ -440,6 +442,10 @@ TEST (toml, daemon_config_deserialize_no_defaults)
|
|||
io_threads = 999
|
||||
port = 999
|
||||
|
||||
[node.ipc.flatbuffers]
|
||||
skip_unexpected_fields_in_json = false
|
||||
verify_buffers = false
|
||||
|
||||
[node.logging]
|
||||
bulk_pull = true
|
||||
flush = false
|
||||
|
@ -615,6 +621,8 @@ TEST (toml, daemon_config_deserialize_no_defaults)
|
|||
ASSERT_NE (conf.node.ipc_config.transport_tcp.io_timeout, defaults.node.ipc_config.transport_tcp.io_timeout);
|
||||
ASSERT_NE (conf.node.ipc_config.transport_tcp.io_threads, defaults.node.ipc_config.transport_tcp.io_threads);
|
||||
ASSERT_NE (conf.node.ipc_config.transport_tcp.port, defaults.node.ipc_config.transport_tcp.port);
|
||||
ASSERT_NE (conf.node.ipc_config.flatbuffers.skip_unexpected_fields_in_json, defaults.node.ipc_config.flatbuffers.skip_unexpected_fields_in_json);
|
||||
ASSERT_NE (conf.node.ipc_config.flatbuffers.verify_buffers, defaults.node.ipc_config.flatbuffers.verify_buffers);
|
||||
|
||||
ASSERT_NE (conf.node.diagnostics_config.txn_tracking.enable, defaults.node.diagnostics_config.txn_tracking.enable);
|
||||
ASSERT_NE (conf.node.diagnostics_config.txn_tracking.ignore_writes_below_block_processor_max_time, defaults.node.diagnostics_config.txn_tracking.ignore_writes_below_block_processor_max_time);
|
||||
|
|
49
nano/ipc_flatbuffers_lib/CMakeLists.txt
Normal file
49
nano/ipc_flatbuffers_lib/CMakeLists.txt
Normal file
|
@ -0,0 +1,49 @@
|
|||
# Build flatc from the Flatbuffers submodule
|
||||
set (FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "")
|
||||
set (FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "")
|
||||
mark_as_advanced (
|
||||
FLATBUFFERS_BUILD_CPP17
|
||||
FLATBUFFERS_BUILD_FLATC
|
||||
FLATBUFFERS_BUILD_FLATHASH
|
||||
FLATBUFFERS_BUILD_FLATLIB
|
||||
FLATBUFFERS_BUILD_GRPCTEST
|
||||
FLATBUFFERS_BUILD_LEGACY
|
||||
FLATBUFFERS_BUILD_SHAREDLIB
|
||||
FLATBUFFERS_BUILD_TESTS
|
||||
FLATBUFFERS_CODE_COVERAGE
|
||||
FLATBUFFERS_CODE_SANITIZE
|
||||
FLATBUFFERS_INSTALL
|
||||
FLATBUFFERS_LIBCXX_WITH_CLANG
|
||||
FLATBUFFERS_PACKAGE_DEBIAN
|
||||
FLATBUFFERS_PACKAGE_REDHAT
|
||||
FLATBUFFERS_STATIC_FLATC)
|
||||
add_subdirectory(../../flatbuffers ${CMAKE_CURRENT_BINARY_DIR}/flatbuffers-build EXCLUDE_FROM_ALL)
|
||||
|
||||
# Generate Flatbuffers files into the ipc_flatbuffers_lib library, which will be rebuilt
|
||||
# whenever any of the fbs files change. Note that while this supports multiple fbs files,
|
||||
# we currently only use one, to avoid include-file issues with certain language bindings.
|
||||
file(MAKE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/generated/flatbuffers)
|
||||
if (APPLE)
|
||||
install (DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../api/flatbuffers/ DESTINATION Nano.app/Contents/MacOS/api/flatbuffers)
|
||||
else()
|
||||
install (DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../api/flatbuffers/ DESTINATION ./api/flatbuffers)
|
||||
endif()
|
||||
|
||||
file(GLOB files "${CMAKE_CURRENT_SOURCE_DIR}/../../api/flatbuffers/nanoapi*.fbs")
|
||||
foreach(file ${files})
|
||||
get_filename_component(flatbuffers_filename ${file} NAME_WE)
|
||||
message("Generating flatbuffers code for: ${flatbuffers_filename} into ${CMAKE_CURRENT_SOURCE_DIR}/generated/flatbuffers")
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/generated/flatbuffers/${flatbuffers_filename}_generated.h
|
||||
COMMAND "$<TARGET_FILE:flatc>" --force-empty-vectors --reflect-names --gen-mutable --gen-name-strings --gen-object-api --strict-json --cpp -o ${CMAKE_CURRENT_SOURCE_DIR}/generated/flatbuffers ${CMAKE_CURRENT_SOURCE_DIR}/../../api/flatbuffers/${flatbuffers_filename}.fbs
|
||||
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../../api/flatbuffers/${flatbuffers_filename}.fbs
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
endforeach()
|
||||
|
||||
add_library (ipc_flatbuffers_lib
|
||||
generated/flatbuffers/nanoapi_generated.h
|
||||
flatbuffer_producer.hpp
|
||||
flatbuffer_producer.cpp
|
||||
)
|
||||
|
||||
target_link_libraries (ipc_flatbuffers_lib flatbuffers)
|
35
nano/ipc_flatbuffers_lib/flatbuffer_producer.cpp
Normal file
35
nano/ipc_flatbuffers_lib/flatbuffer_producer.cpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#include <nano/ipc_flatbuffers_lib/flatbuffer_producer.hpp>
|
||||
|
||||
nano::ipc::flatbuffer_producer::flatbuffer_producer ()
|
||||
{
|
||||
fbb = std::make_shared<flatbuffers::FlatBufferBuilder> ();
|
||||
}
|
||||
|
||||
nano::ipc::flatbuffer_producer::flatbuffer_producer (std::shared_ptr<flatbuffers::FlatBufferBuilder> const & builder_a) :
|
||||
fbb (builder_a)
|
||||
{
|
||||
}
|
||||
|
||||
void nano::ipc::flatbuffer_producer::make_error (int code, std::string const & message)
|
||||
{
|
||||
auto msg = fbb->CreateString (message);
|
||||
nanoapi::ErrorBuilder builder (*fbb);
|
||||
builder.add_code (code);
|
||||
builder.add_message (msg);
|
||||
create_builder_response (builder);
|
||||
}
|
||||
|
||||
void nano::ipc::flatbuffer_producer::set_correlation_id (std::string const & correlation_id_a)
|
||||
{
|
||||
correlation_id = correlation_id_a;
|
||||
}
|
||||
|
||||
void nano::ipc::flatbuffer_producer::set_credentials (std::string const & credentials_a)
|
||||
{
|
||||
credentials = credentials_a;
|
||||
}
|
||||
|
||||
std::shared_ptr<flatbuffers::FlatBufferBuilder> nano::ipc::flatbuffer_producer::get_shared_flatbuffer () const
|
||||
{
|
||||
return fbb;
|
||||
}
|
91
nano/ipc_flatbuffers_lib/flatbuffer_producer.hpp
Normal file
91
nano/ipc_flatbuffers_lib/flatbuffer_producer.hpp
Normal file
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
#include <nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
namespace nano
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
/** Produces Nano API compliant Flatbuffers from objects and builders */
|
||||
class flatbuffer_producer
|
||||
{
|
||||
public:
|
||||
flatbuffer_producer ();
|
||||
flatbuffer_producer (std::shared_ptr<flatbuffers::FlatBufferBuilder> const & builder_a);
|
||||
|
||||
template <typename T>
|
||||
static std::shared_ptr<flatbuffers::FlatBufferBuilder> make_buffer (T & object_a, std::string const & correlation_id_a = {}, std::string const & credentials_a = {})
|
||||
{
|
||||
nano::ipc::flatbuffer_producer producer;
|
||||
producer.set_correlation_id (correlation_id_a);
|
||||
producer.set_credentials (credentials_a);
|
||||
producer.create_response (object_a);
|
||||
return producer.fbb;
|
||||
}
|
||||
|
||||
void make_error (int code, std::string const & message);
|
||||
|
||||
/** Every message is put in an envelope, which contains the message type and other sideband information */
|
||||
template <typename T>
|
||||
auto make_envelope (flatbuffers::Offset<T> obj)
|
||||
{
|
||||
auto correlation_id_string = fbb->CreateString (correlation_id);
|
||||
auto credentials_string = fbb->CreateString (credentials);
|
||||
nanoapi::EnvelopeBuilder envelope_builder (*fbb);
|
||||
envelope_builder.add_time (std::chrono::duration_cast<std::chrono::milliseconds> (std::chrono::system_clock::now ().time_since_epoch ()).count ());
|
||||
envelope_builder.add_message_type (nanoapi::MessageTraits<T>::enum_value);
|
||||
envelope_builder.add_message (obj.Union ());
|
||||
|
||||
if (!correlation_id.empty ())
|
||||
{
|
||||
envelope_builder.add_correlation_id (correlation_id_string);
|
||||
}
|
||||
if (!credentials.empty ())
|
||||
{
|
||||
envelope_builder.add_credentials (credentials_string);
|
||||
}
|
||||
return envelope_builder.Finish ();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void create_response (flatbuffers::Offset<T> offset)
|
||||
{
|
||||
auto root = make_envelope (offset);
|
||||
fbb->Finish (root);
|
||||
}
|
||||
|
||||
template <typename T, typename = std::enable_if_t<std::is_base_of<flatbuffers::NativeTable, T>::value>>
|
||||
void create_response (T const & obj)
|
||||
{
|
||||
create_response (T::TableType::Pack (*fbb, &obj));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void create_builder_response (T builder)
|
||||
{
|
||||
auto offset = builder.Finish ();
|
||||
auto root = make_envelope (offset);
|
||||
fbb->Finish (root);
|
||||
}
|
||||
|
||||
/** Set the correlation id. This will be added to the envelope. */
|
||||
void set_correlation_id (std::string const & correlation_id_a);
|
||||
/** Set the credentials. This will be added to the envelope. */
|
||||
void set_credentials (std::string const & credentials_a);
|
||||
/** Returns the flatbuffer */
|
||||
std::shared_ptr<flatbuffers::FlatBufferBuilder> get_shared_flatbuffer () const;
|
||||
|
||||
private:
|
||||
/** The builder managed by this instance */
|
||||
std::shared_ptr<flatbuffers::FlatBufferBuilder> fbb;
|
||||
/** Correlation id, if available */
|
||||
std::string correlation_id;
|
||||
/** Credentials, if available */
|
||||
std::string credentials;
|
||||
};
|
||||
}
|
||||
}
|
12
nano/ipc_flatbuffers_test/CMakeLists.txt
Normal file
12
nano/ipc_flatbuffers_test/CMakeLists.txt
Normal file
|
@ -0,0 +1,12 @@
|
|||
add_executable (ipc_flatbuffers_test_client
|
||||
entry.cpp)
|
||||
|
||||
target_link_libraries (ipc_flatbuffers_test_client
|
||||
nano_lib
|
||||
Boost::filesystem
|
||||
Boost::log_setup
|
||||
Boost::log
|
||||
Boost::program_options
|
||||
Boost::system
|
||||
Boost::thread
|
||||
Boost::boost)
|
80
nano/ipc_flatbuffers_test/entry.cpp
Normal file
80
nano/ipc_flatbuffers_test/entry.cpp
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include <nano/ipc_flatbuffers_lib/flatbuffer_producer.hpp>
|
||||
#include <nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h>
|
||||
#include <nano/lib/asio.hpp>
|
||||
#include <nano/lib/ipc.hpp>
|
||||
#include <nano/lib/ipc_client.hpp>
|
||||
|
||||
#include <boost/endian/conversion.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
void read_message_loop (std::shared_ptr<nano::ipc::ipc_client> const & connection)
|
||||
{
|
||||
auto buffer (std::make_shared<std::vector<uint8_t>> ());
|
||||
connection->async_read_message (buffer, std::chrono::seconds::max (), [buffer, connection](nano::error error_a, size_t size_a) {
|
||||
if (!error_a)
|
||||
{
|
||||
auto verifier (flatbuffers::Verifier (buffer->data (), buffer->size ()));
|
||||
if (!nanoapi::VerifyEnvelopeBuffer (verifier))
|
||||
{
|
||||
std::cerr << "Invalid message" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto envelope (nanoapi::GetEnvelope (buffer->data ()));
|
||||
if (envelope->message_type () == nanoapi::Message_EventConfirmation)
|
||||
{
|
||||
std::cout << "Confirmation at " << envelope->time () << std::endl;
|
||||
auto conf (envelope->message_as_EventConfirmation ());
|
||||
std::cout << " Account : " << conf->account ()->str () << std::endl;
|
||||
std::cout << " Amount : " << conf->amount ()->str () << std::endl;
|
||||
std::cout << " Block type : " << nanoapi::EnumNamesBlock ()[conf->block_type ()] << std::endl;
|
||||
auto state_block = conf->block_as_BlockState ();
|
||||
if (state_block)
|
||||
{
|
||||
std::cout << " Balance : " << state_block->balance ()->str () << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
read_message_loop (connection);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** A sample IPC/flatbuffers client that subscribes to confirmations from a local node */
|
||||
int main (int argc, char * const * argv)
|
||||
{
|
||||
boost::asio::io_context io_ctx;
|
||||
auto connection (std::make_shared<nano::ipc::ipc_client> (io_ctx));
|
||||
// The client only connects to a local live node for now; the test will
|
||||
// be improved later to handle various options, including port and address.
|
||||
std::string ipc_address = "::1";
|
||||
uint16_t ipc_port = 7077;
|
||||
connection->async_connect (ipc_address, ipc_port, [connection](nano::error err) {
|
||||
if (!err)
|
||||
{
|
||||
nanoapi::TopicConfirmationT conf;
|
||||
connection->async_write (nano::ipc::shared_buffer_from (conf), [connection](nano::error err, size_t size) {
|
||||
if (!err)
|
||||
{
|
||||
std::cout << "Awaiting confirmations..." << std::endl;
|
||||
read_message_loop (connection);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << err.get_message () << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
io_ctx.run ();
|
||||
return 0;
|
||||
}
|
|
@ -70,6 +70,7 @@ target_link_libraries (nano_lib
|
|||
ed25519
|
||||
crypto_lib
|
||||
blake2
|
||||
ipc_flatbuffers_lib
|
||||
${CRYPTOPP_LIBRARY}
|
||||
${CMAKE_DL_LIBS}
|
||||
Boost::boost)
|
||||
|
|
|
@ -34,4 +34,16 @@ async_write (AsyncWriteStream & s, nano::shared_const_buffer const & buffer, Wri
|
|||
{
|
||||
return boost::asio::async_write (s, buffer, std::move (handler));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to nano::async_write where scatter/gather is desired for best performance, and where
|
||||
* the buffer originates from Flatbuffers.
|
||||
* @warning The buffers must be captured in the handler to ensure their lifetimes are properly extended.
|
||||
*/
|
||||
template <typename AsyncWriteStream, typename BufferType, typename WriteHandler>
|
||||
BOOST_ASIO_INITFN_RESULT_TYPE (WriteHandler, void(boost::system::error_code, std::size_t))
|
||||
unsafe_async_write (AsyncWriteStream & s, BufferType && buffer, WriteHandler && handler)
|
||||
{
|
||||
return boost::asio::async_write (s, buffer, std::move (handler));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,4 +60,8 @@ std::string get_qtwallet_toml_config_path (boost::filesystem::path const & data_
|
|||
{
|
||||
return (data_path / "config-qtwallet.toml").string ();
|
||||
}
|
||||
std::string get_access_toml_config_path (boost::filesystem::path const & data_path)
|
||||
{
|
||||
return (data_path / "config-access.toml").string ();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,6 +175,7 @@ std::string get_config_path (boost::filesystem::path const & data_path);
|
|||
std::string get_rpc_config_path (boost::filesystem::path const & data_path);
|
||||
std::string get_node_toml_config_path (boost::filesystem::path const & data_path);
|
||||
std::string get_rpc_toml_config_path (boost::filesystem::path const & data_path);
|
||||
std::string get_access_toml_config_path (boost::filesystem::path const & data_path);
|
||||
std::string get_qtwallet_toml_config_path (boost::filesystem::path const & data_path);
|
||||
|
||||
/** Called by gtest_main to enforce test network */
|
||||
|
|
|
@ -10,6 +10,8 @@ std::string nano::error_common_messages::message (int ev) const
|
|||
{
|
||||
case nano::error_common::generic:
|
||||
return "Unknown error";
|
||||
case nano::error_common::access_denied:
|
||||
return "Access denied";
|
||||
case nano::error_common::missing_account:
|
||||
return "Missing account";
|
||||
case nano::error_common::missing_balance:
|
||||
|
@ -394,6 +396,11 @@ nano::error::operator std::error_code () const
|
|||
return code;
|
||||
}
|
||||
|
||||
int nano::error::error_code_as_int () const
|
||||
{
|
||||
return code.value ();
|
||||
}
|
||||
|
||||
/** Implicit bool conversion; true if there's an error */
|
||||
nano::error::operator bool () const
|
||||
{
|
||||
|
|
|
@ -17,6 +17,7 @@ enum class error_common
|
|||
{
|
||||
generic = 1,
|
||||
exception,
|
||||
access_denied,
|
||||
account_not_found,
|
||||
account_not_found_wallet,
|
||||
account_exists,
|
||||
|
@ -267,6 +268,11 @@ public:
|
|||
explicit operator bool () const;
|
||||
explicit operator std::string () const;
|
||||
std::string get_message () const;
|
||||
/**
|
||||
* The error code as an integer. Note that some error codes have platform dependent values.
|
||||
* A return value of 0 signifies there is no error.
|
||||
*/
|
||||
int error_code_as_int () const;
|
||||
error & on_error (std::string message_a);
|
||||
error & on_error (std::error_code code_a, std::string message_a);
|
||||
error & set (std::string message_a, std::error_code code_a = nano::error_common::generic);
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace ipc
|
|||
};
|
||||
|
||||
/**
|
||||
* Payload encodings; add protobuf, flatbuffers and so on as needed.
|
||||
* Payload encodings.
|
||||
*/
|
||||
enum class payload_encoding : uint8_t
|
||||
{
|
||||
|
@ -57,9 +57,20 @@ namespace ipc
|
|||
* Request is preamble followed by 32-bit BE payload length and payload bytes.
|
||||
* Response is 32-bit BE payload length followed by payload bytes.
|
||||
*/
|
||||
json_legacy = 0x1,
|
||||
/** Request/response is same as json_legacy and exposes unsafe RPC's */
|
||||
json_unsafe = 0x2
|
||||
json_v1 = 0x1,
|
||||
|
||||
/** Request/response is same as json_v1, but exposes unsafe RPC's */
|
||||
json_v1_unsafe = 0x2,
|
||||
|
||||
/**
|
||||
* Request is preamble followed by 32-bit BE payload length and payload bytes.
|
||||
* Response is 32-bit BE payload length followed by payload bytes.
|
||||
* Payloads must be flatbuffer encoded.
|
||||
*/
|
||||
flatbuffers = 0x3,
|
||||
|
||||
/** JSON -> Flatbuffers -> JSON */
|
||||
flatbuffers_json = 0x4
|
||||
};
|
||||
|
||||
/** IPC transport interface */
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <boost/endian/conversion.hpp>
|
||||
#include <boost/polymorphic_cast.hpp>
|
||||
|
||||
#include <deque>
|
||||
#include <future>
|
||||
|
||||
namespace
|
||||
|
@ -18,8 +19,18 @@ namespace
|
|||
class channel
|
||||
{
|
||||
public:
|
||||
virtual void async_read (std::shared_ptr<std::vector<uint8_t>> buffer_a, size_t size_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) = 0;
|
||||
virtual void async_read (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, size_t size_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) = 0;
|
||||
virtual void async_write (nano::shared_const_buffer const & buffer_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) = 0;
|
||||
|
||||
/**
|
||||
* Read a length-prefixed message asynchronously using the given timeout. This is suitable for full duplex scenarios where it may
|
||||
* take an arbitrarily long time for the node to send messages for a given subscription.
|
||||
* Received length must be a big endian 32-bit unsigned integer.
|
||||
* @param buffer_a Receives the payload
|
||||
* @param timeout_a How long to await message data. In some scenarios, such as waiting for data on subscriptions, specifying std::chrono::seconds::max() makes sense.
|
||||
* @param callback_a If called without errors, the payload buffer is successfully populated
|
||||
*/
|
||||
virtual void async_read_message (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, std::chrono::seconds timeout_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) = 0;
|
||||
};
|
||||
|
||||
/* Boost v1.70 introduced breaking changes; the conditional compilation allows 1.6x to be supported as well. */
|
||||
|
@ -31,7 +42,7 @@ using socket_type = boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost
|
|||
|
||||
/** Domain and TCP client socket */
|
||||
template <typename SOCKET_TYPE, typename ENDPOINT_TYPE>
|
||||
class socket_client : public nano::ipc::socket_base, public channel
|
||||
class socket_client : public nano::ipc::socket_base, public channel, public std::enable_shared_from_this<socket_client<SOCKET_TYPE, ENDPOINT_TYPE>>
|
||||
{
|
||||
public:
|
||||
socket_client (boost::asio::io_context & io_ctx_a, ENDPOINT_TYPE endpoint_a) :
|
||||
|
@ -41,13 +52,14 @@ public:
|
|||
|
||||
void async_resolve (std::string const & host_a, uint16_t port_a, std::function<void(boost::system::error_code const &, boost::asio::ip::tcp::endpoint)> callback_a)
|
||||
{
|
||||
this->timer_start (io_timeout);
|
||||
resolver.async_resolve (boost::asio::ip::tcp::resolver::query (host_a, std::to_string (port_a)), [this, callback_a](boost::system::error_code const & ec, boost::asio::ip::tcp::resolver::iterator endpoint_iterator_a) {
|
||||
this->timer_cancel ();
|
||||
auto this_l (this->shared_from_this ());
|
||||
this_l->timer_start (io_timeout);
|
||||
resolver.async_resolve (boost::asio::ip::tcp::resolver::query (host_a, std::to_string (port_a)), [this_l, callback_a](boost::system::error_code const & ec, boost::asio::ip::tcp::resolver::iterator endpoint_iterator_a) {
|
||||
this_l->timer_cancel ();
|
||||
boost::asio::ip::tcp::resolver::iterator end;
|
||||
if (!ec && endpoint_iterator_a != end)
|
||||
{
|
||||
endpoint = *endpoint_iterator_a;
|
||||
this_l->endpoint = *endpoint_iterator_a;
|
||||
callback_a (ec, *endpoint_iterator_a);
|
||||
}
|
||||
else
|
||||
|
@ -59,40 +71,117 @@ public:
|
|||
|
||||
void async_connect (std::function<void(boost::system::error_code const &)> callback_a)
|
||||
{
|
||||
this->timer_start (io_timeout);
|
||||
socket.async_connect (endpoint, boost::asio::bind_executor (strand, [this, callback_a](boost::system::error_code const & ec) {
|
||||
this->timer_cancel ();
|
||||
auto this_l (this->shared_from_this ());
|
||||
this_l->timer_start (io_timeout);
|
||||
socket.async_connect (endpoint, boost::asio::bind_executor (strand, [this_l, callback_a](boost::system::error_code const & ec) {
|
||||
this_l->timer_cancel ();
|
||||
callback_a (ec);
|
||||
}));
|
||||
}
|
||||
|
||||
void async_read (std::shared_ptr<std::vector<uint8_t>> buffer_a, size_t size_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) override
|
||||
void async_read (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, size_t size_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) override
|
||||
{
|
||||
this->timer_start (io_timeout);
|
||||
auto this_l (this->shared_from_this ());
|
||||
this_l->timer_start (io_timeout);
|
||||
buffer_a->resize (size_a);
|
||||
boost::asio::async_read (socket, boost::asio::buffer (buffer_a->data (), size_a), boost::asio::bind_executor (this->strand, [this, callback_a](boost::system::error_code const & ec, size_t size_a) {
|
||||
this->timer_cancel ();
|
||||
boost::asio::async_read (socket, boost::asio::buffer (buffer_a->data (), size_a), boost::asio::bind_executor (this_l->strand, [this_l, buffer_a, callback_a](boost::system::error_code const & ec, size_t size_a) {
|
||||
this_l->timer_cancel ();
|
||||
callback_a (ec, size_a);
|
||||
}));
|
||||
}
|
||||
|
||||
void async_write (nano::shared_const_buffer const & buffer_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) override
|
||||
{
|
||||
this->timer_start (io_timeout);
|
||||
nano::async_write (socket, buffer_a, boost::asio::bind_executor (this->strand, [this, callback_a](boost::system::error_code const & ec, size_t size_a) {
|
||||
this->timer_cancel ();
|
||||
callback_a (ec, size_a);
|
||||
auto this_l (this->shared_from_this ());
|
||||
boost::asio::post (strand, boost::asio::bind_executor (strand, [buffer_a, callback_a, this_l]() {
|
||||
bool write_in_progress = !this_l->send_queue.empty ();
|
||||
auto queue_size = this_l->send_queue.size ();
|
||||
if (queue_size < this_l->queue_size_max)
|
||||
{
|
||||
this_l->send_queue.emplace_back (buffer_a, callback_a);
|
||||
}
|
||||
if (!write_in_progress)
|
||||
{
|
||||
this_l->write_queued_messages ();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void write_queued_messages ()
|
||||
{
|
||||
auto this_l (this->shared_from_this ());
|
||||
auto msg (send_queue.front ());
|
||||
this_l->timer_start (io_timeout);
|
||||
nano::async_write (socket, msg.buffer,
|
||||
boost::asio::bind_executor (strand,
|
||||
[msg, this_l](boost::system::error_code ec, std::size_t size_a) {
|
||||
this_l->timer_cancel ();
|
||||
|
||||
if (msg.callback)
|
||||
{
|
||||
msg.callback (ec, size_a);
|
||||
}
|
||||
|
||||
this_l->send_queue.pop_front ();
|
||||
if (!ec && !this_l->send_queue.empty ())
|
||||
{
|
||||
this_l->write_queued_messages ();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void async_read_message (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, std::chrono::seconds timeout_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) override
|
||||
{
|
||||
auto this_l (this->shared_from_this ());
|
||||
this_l->timer_start (timeout_a);
|
||||
buffer_a->resize (4);
|
||||
// Read 32 bit big-endian length
|
||||
boost::asio::async_read (socket, boost::asio::buffer (buffer_a->data (), 4), boost::asio::bind_executor (this_l->strand, [this_l, timeout_a, buffer_a, callback_a](boost::system::error_code const & ec, size_t size_a) {
|
||||
this_l->timer_cancel ();
|
||||
if (!ec)
|
||||
{
|
||||
uint32_t payload_size_l = boost::endian::big_to_native (*reinterpret_cast<uint32_t *> (buffer_a->data ()));
|
||||
buffer_a->resize (payload_size_l);
|
||||
// Read payload
|
||||
this_l->timer_start (timeout_a);
|
||||
this_l->async_read (buffer_a, payload_size_l, [this_l, buffer_a, callback_a](boost::system::error_code const & ec_a, size_t size_a) {
|
||||
this_l->timer_cancel ();
|
||||
callback_a (ec_a, size_a);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
callback_a (ec, size_a);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/** Shut down and close socket */
|
||||
void close () override
|
||||
{
|
||||
socket.shutdown (boost::asio::ip::tcp::socket::shutdown_both);
|
||||
socket.close ();
|
||||
auto this_l (this->shared_from_this ());
|
||||
boost::asio::post (strand, boost::asio::bind_executor (strand, [this_l]() {
|
||||
this_l->socket.shutdown (boost::asio::ip::tcp::socket::shutdown_both);
|
||||
this_l->socket.close ();
|
||||
}));
|
||||
}
|
||||
|
||||
private:
|
||||
/** Holds the buffer and callback for queued writes */
|
||||
class queue_item
|
||||
{
|
||||
public:
|
||||
queue_item (nano::shared_const_buffer const & buffer_a, std::function<void(boost::system::error_code const &, size_t)> callback_a) :
|
||||
buffer (buffer_a), callback (callback_a)
|
||||
{
|
||||
}
|
||||
nano::shared_const_buffer buffer;
|
||||
std::function<void(boost::system::error_code const &, size_t)> callback;
|
||||
};
|
||||
size_t const queue_size_max = 64 * 1024;
|
||||
/** The send queue is protected by always being accessed through the strand */
|
||||
std::deque<queue_item> send_queue;
|
||||
|
||||
ENDPOINT_TYPE endpoint;
|
||||
SOCKET_TYPE socket;
|
||||
boost::asio::ip::tcp::resolver resolver;
|
||||
|
@ -194,24 +283,50 @@ void nano::ipc::ipc_client::async_write (nano::shared_const_buffer const & buffe
|
|||
});
|
||||
}
|
||||
|
||||
void nano::ipc::ipc_client::async_read (std::shared_ptr<std::vector<uint8_t>> buffer_a, size_t size_a, std::function<void(nano::error, size_t)> callback_a)
|
||||
void nano::ipc::ipc_client::async_read (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, size_t size_a, std::function<void(nano::error, size_t)> callback_a)
|
||||
{
|
||||
auto client (boost::polymorphic_downcast<client_impl *> (impl.get ()));
|
||||
client->get_channel ().async_read (buffer_a, size_a, [callback_a](const boost::system::error_code & ec_a, size_t bytes_read_a) {
|
||||
client->get_channel ().async_read (buffer_a, size_a, [callback_a, buffer_a](const boost::system::error_code & ec_a, size_t bytes_read_a) {
|
||||
callback_a (nano::error (ec_a), bytes_read_a);
|
||||
});
|
||||
}
|
||||
|
||||
/** Read a length-prefixed message asynchronously. Received length must be a big endian 32-bit unsigned integer. */
|
||||
void nano::ipc::ipc_client::async_read_message (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, std::chrono::seconds timeout_a, std::function<void(nano::error, size_t)> callback_a)
|
||||
{
|
||||
auto client (boost::polymorphic_downcast<client_impl *> (impl.get ()));
|
||||
client->get_channel ().async_read_message (buffer_a, timeout_a, [callback_a, buffer_a](const boost::system::error_code & ec_a, size_t bytes_read_a) {
|
||||
callback_a (nano::error (ec_a), bytes_read_a);
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<uint8_t> nano::ipc::get_preamble (nano::ipc::payload_encoding encoding_a)
|
||||
{
|
||||
std::vector<uint8_t> buffer_l;
|
||||
buffer_l.push_back ('N');
|
||||
buffer_l.push_back (static_cast<uint8_t> (encoding_a));
|
||||
buffer_l.push_back (0);
|
||||
buffer_l.push_back (0);
|
||||
return buffer_l;
|
||||
}
|
||||
|
||||
nano::shared_const_buffer nano::ipc::prepare_flatbuffers_request (std::shared_ptr<flatbuffers::FlatBufferBuilder> const & flatbuffer_a)
|
||||
{
|
||||
auto buffer_l (get_preamble (nano::ipc::payload_encoding::flatbuffers));
|
||||
auto payload_length = static_cast<uint32_t> (flatbuffer_a->GetSize ());
|
||||
uint32_t be = boost::endian::native_to_big (payload_length);
|
||||
char * chars = reinterpret_cast<char *> (&be);
|
||||
buffer_l.insert (buffer_l.end (), chars, chars + sizeof (uint32_t));
|
||||
buffer_l.insert (buffer_l.end (), flatbuffer_a->GetBufferPointer (), flatbuffer_a->GetBufferPointer () + flatbuffer_a->GetSize ());
|
||||
return nano::shared_const_buffer{ std::move (buffer_l) };
|
||||
}
|
||||
|
||||
nano::shared_const_buffer nano::ipc::prepare_request (nano::ipc::payload_encoding encoding_a, std::string const & payload_a)
|
||||
{
|
||||
std::vector<uint8_t> buffer_l;
|
||||
if (encoding_a == nano::ipc::payload_encoding::json_legacy)
|
||||
if (encoding_a == nano::ipc::payload_encoding::json_v1 || encoding_a == nano::ipc::payload_encoding::flatbuffers_json)
|
||||
{
|
||||
buffer_l.push_back ('N');
|
||||
buffer_l.push_back (static_cast<uint8_t> (encoding_a));
|
||||
buffer_l.push_back (0);
|
||||
buffer_l.push_back (0);
|
||||
|
||||
buffer_l = get_preamble (encoding_a);
|
||||
auto payload_length = static_cast<uint32_t> (payload_a.size ());
|
||||
uint32_t be = boost::endian::native_to_big (payload_length);
|
||||
char * chars = reinterpret_cast<char *> (&be);
|
||||
|
@ -221,9 +336,9 @@ nano::shared_const_buffer nano::ipc::prepare_request (nano::ipc::payload_encodin
|
|||
return nano::shared_const_buffer{ std::move (buffer_l) };
|
||||
}
|
||||
|
||||
std::string nano::ipc::request (nano::ipc::ipc_client & ipc_client, std::string const & rpc_action_a)
|
||||
std::string nano::ipc::request (nano::ipc::payload_encoding encoding_a, nano::ipc::ipc_client & ipc_client, std::string const & rpc_action_a)
|
||||
{
|
||||
auto req (prepare_request (nano::ipc::payload_encoding::json_legacy, rpc_action_a));
|
||||
auto req (prepare_request (encoding_a, rpc_action_a));
|
||||
auto res (std::make_shared<std::vector<uint8_t>> ());
|
||||
|
||||
std::promise<std::string> result_l;
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/ipc_flatbuffers_lib/flatbuffer_producer.hpp>
|
||||
#include <nano/lib/asio.hpp>
|
||||
#include <nano/lib/errors.hpp>
|
||||
#include <nano/lib/ipc.hpp>
|
||||
#include <nano/lib/utility.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
namespace nano
|
||||
{
|
||||
class shared_const_buffer;
|
||||
|
@ -39,7 +44,17 @@ namespace ipc
|
|||
void async_write (nano::shared_const_buffer const & buffer_a, std::function<void(nano::error, size_t)> callback_a);
|
||||
|
||||
/** Read \p size_a bytes asynchronously */
|
||||
void async_read (std::shared_ptr<std::vector<uint8_t>> buffer_a, size_t size_a, std::function<void(nano::error, size_t)> callback_a);
|
||||
void async_read (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, size_t size_a, std::function<void(nano::error, size_t)> callback_a);
|
||||
|
||||
/**
|
||||
* Read a length-prefixed message asynchronously using the given timeout. This is suitable for full duplex scenarios where it may
|
||||
* take an arbitrarily long time for the node to send messages for a given subscription.
|
||||
* Received length must be a big endian 32-bit unsigned integer.
|
||||
* @param buffer_a Receives the payload
|
||||
* @param timeout_a How long to await message data. In some scenarios, such as waiting for data on subscriptions, specifying std::chrono::seconds::max() makes sense.
|
||||
* @param callback_a If called without errors, the payload buffer is successfully populated
|
||||
*/
|
||||
void async_read_message (std::shared_ptr<std::vector<uint8_t>> const & buffer_a, std::chrono::seconds timeout_a, std::function<void(nano::error, size_t)> callback_a);
|
||||
|
||||
private:
|
||||
boost::asio::io_context & io_ctx;
|
||||
|
@ -49,7 +64,24 @@ namespace ipc
|
|||
};
|
||||
|
||||
/** Convenience function for making synchronous IPC calls. The client must be connected */
|
||||
std::string request (nano::ipc::ipc_client & ipc_client, std::string const & rpc_action_a);
|
||||
std::string request (nano::ipc::payload_encoding encoding_a, nano::ipc::ipc_client & ipc_client, std::string const & rpc_action_a);
|
||||
|
||||
/**
|
||||
* Returns a buffer with an IPC preamble for the given \p encoding_a
|
||||
*/
|
||||
std::vector<uint8_t> get_preamble (nano::ipc::payload_encoding encoding_a);
|
||||
|
||||
/**
|
||||
* Returns a buffer with an IPC preamble, followed by 32-bit BE lenght, followed by payload
|
||||
*/
|
||||
nano::shared_const_buffer prepare_flatbuffers_request (std::shared_ptr<flatbuffers::FlatBufferBuilder> const & flatbuffer_a);
|
||||
|
||||
template <typename T>
|
||||
nano::shared_const_buffer shared_buffer_from (T & object_a, std::string const & correlation_id_a = {}, std::string const & credentials_a = {})
|
||||
{
|
||||
auto buffer_l (nano::ipc::flatbuffer_producer::make_buffer (object_a, correlation_id_a, credentials_a));
|
||||
return nano::ipc::prepare_flatbuffers_request (buffer_l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer with an IPC preamble for the given \p encoding_a followed by the payload. Depending on encoding,
|
||||
|
|
|
@ -79,4 +79,69 @@ using unique_lock = std::unique_lock<Mutex>;
|
|||
|
||||
// For consistency wrapping the less well known _any variant which can be used with any lockable type
|
||||
using condition_variable = std::condition_variable_any;
|
||||
|
||||
/** A general purpose monitor template */
|
||||
template <class T>
|
||||
class locked
|
||||
{
|
||||
public:
|
||||
template <typename... Args>
|
||||
locked (Args &&... args) :
|
||||
obj (std::forward<Args> (args)...)
|
||||
{
|
||||
}
|
||||
|
||||
struct scoped_lock final
|
||||
{
|
||||
scoped_lock (locked * owner_a) :
|
||||
owner (owner_a)
|
||||
{
|
||||
owner->mutex.lock ();
|
||||
}
|
||||
|
||||
~scoped_lock ()
|
||||
{
|
||||
owner->mutex.unlock ();
|
||||
}
|
||||
|
||||
T * operator-> ()
|
||||
{
|
||||
return &owner->obj;
|
||||
}
|
||||
|
||||
T & get () const
|
||||
{
|
||||
return owner->obj;
|
||||
}
|
||||
|
||||
locked * owner{ nullptr };
|
||||
};
|
||||
|
||||
scoped_lock operator-> ()
|
||||
{
|
||||
return scoped_lock (this);
|
||||
}
|
||||
|
||||
T & operator= (T const & other)
|
||||
{
|
||||
nano::unique_lock<std::mutex> lk (mutex);
|
||||
obj = other;
|
||||
return obj;
|
||||
}
|
||||
|
||||
operator T () const
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** Returns a scoped lock wrapper, allowing multiple calls to the underlying object under the same lock */
|
||||
scoped_lock lock ()
|
||||
{
|
||||
return scoped_lock (this);
|
||||
}
|
||||
|
||||
private:
|
||||
T obj;
|
||||
std::mutex mutex;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,17 +1,65 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace nano
|
||||
{
|
||||
class rpc;
|
||||
|
||||
/** Keeps information about http requests, and for v2+ includes path and header values of interest */
|
||||
class rpc_handler_request_params final
|
||||
{
|
||||
public:
|
||||
int rpc_version{ 1 };
|
||||
std::string path;
|
||||
std::string credentials;
|
||||
std::string correlation_id;
|
||||
|
||||
/**
|
||||
* If the path is non-empty, this wraps the body inside an IPC API compliant envelope.
|
||||
* Otherwise the input string is returned unchanged.
|
||||
* This allows HTTP clients to use a simplified request format by omitting the envelope.
|
||||
* Envelope fields may still be specified through corresponding nano- header fields.
|
||||
*/
|
||||
std::string json_envelope (std::string const & body_a) const
|
||||
{
|
||||
std::string body_l;
|
||||
if (!path.empty ())
|
||||
{
|
||||
std::ostringstream json;
|
||||
json << "{";
|
||||
if (!credentials.empty ())
|
||||
{
|
||||
json << "\"credentials\": \"" << credentials << "\", ";
|
||||
}
|
||||
if (!correlation_id.empty ())
|
||||
{
|
||||
json << "\"correlation_id\": \"" << correlation_id << "\", ";
|
||||
}
|
||||
json << "\"message_type\": \"" << path << "\", ";
|
||||
json << "\"message\": " << body_a;
|
||||
json << "}";
|
||||
body_l = json.str ();
|
||||
}
|
||||
else
|
||||
{
|
||||
body_l = body_a;
|
||||
}
|
||||
return body_l;
|
||||
}
|
||||
};
|
||||
|
||||
class rpc_handler_interface
|
||||
{
|
||||
public:
|
||||
virtual ~rpc_handler_interface () = default;
|
||||
/** Process RPC 1.0 request. */
|
||||
virtual void process_request (std::string const & action, std::string const & body, std::function<void(std::string const &)> response) = 0;
|
||||
/** Process RPC 2.0 request. This is called via the IPC API */
|
||||
virtual void process_request_v2 (rpc_handler_request_params const & params_a, std::string const & body, std::function<void(std::shared_ptr<std::string>)> response) = 0;
|
||||
virtual void stop () = 0;
|
||||
virtual void rpc_instance (nano::rpc & rpc) = 0;
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#include <nano/lib/utility.hpp>
|
||||
#include <nano/nano_node/daemon.hpp>
|
||||
#include <nano/node/daemonconfig.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/json_handler.hpp>
|
||||
#include <nano/node/node.hpp>
|
||||
#include <nano/node/openclwork.hpp>
|
||||
|
@ -105,7 +105,7 @@ void nano_daemon::daemon::run (boost::filesystem::path const & data_path, nano::
|
|||
std::cout << error.get_message () << std::endl;
|
||||
std::exit (1);
|
||||
}
|
||||
rpc_handler = std::make_unique<nano::inprocess_rpc_handler> (*node, config.rpc, [&ipc_server, &alarm, &io_ctx]() {
|
||||
rpc_handler = std::make_unique<nano::inprocess_rpc_handler> (*node, ipc_server, config.rpc, [&ipc_server, &alarm, &io_ctx]() {
|
||||
ipc_server.stop ();
|
||||
alarm.add (std::chrono::steady_clock::now () + std::chrono::seconds (3), [&io_ctx]() {
|
||||
io_ctx.stop ();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#include <nano/lib/utility.hpp>
|
||||
#include <nano/nano_node/daemon.hpp>
|
||||
#include <nano/node/cli.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/json_handler.hpp>
|
||||
#include <nano/node/node.hpp>
|
||||
#include <nano/node/testing.hpp>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#include <nano/lib/threading.hpp>
|
||||
#include <nano/lib/utility.hpp>
|
||||
#include <nano/node/cli.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/rpc/rpc.hpp>
|
||||
#include <nano/rpc/rpc_request_processor.hpp>
|
||||
#include <nano/secure/utility.hpp>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
#include <nano/nano_wallet/icon.hpp>
|
||||
#include <nano/node/cli.hpp>
|
||||
#include <nano/node/daemonconfig.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/json_handler.hpp>
|
||||
#include <nano/node/node_rpc_config.hpp>
|
||||
#include <nano/qt/qt.hpp>
|
||||
|
@ -170,7 +170,6 @@ int run_wallet (QApplication & application, int argc, char * const * argv, boost
|
|||
std::exit (1);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<nano::rpc> rpc;
|
||||
std::unique_ptr<nano::rpc_handler_interface> rpc_handler;
|
||||
if (config.rpc_enable)
|
||||
|
@ -184,7 +183,7 @@ int run_wallet (QApplication & application, int argc, char * const * argv, boost
|
|||
{
|
||||
show_error (error.get_message ());
|
||||
}
|
||||
rpc_handler = std::make_unique<nano::inprocess_rpc_handler> (*node, config.rpc);
|
||||
rpc_handler = std::make_unique<nano::inprocess_rpc_handler> (*node, ipc, config.rpc);
|
||||
rpc = nano::get_rpc (io_ctx, rpc_config, *rpc_handler);
|
||||
rpc->start ();
|
||||
}
|
||||
|
|
|
@ -58,10 +58,20 @@ add_library (node
|
|||
election.cpp
|
||||
gap_cache.hpp
|
||||
gap_cache.cpp
|
||||
ipc.hpp
|
||||
ipc.cpp
|
||||
ipcconfig.hpp
|
||||
ipcconfig.cpp
|
||||
ipc/action_handler.hpp
|
||||
ipc/action_handler.cpp
|
||||
ipc/flatbuffers_handler.hpp
|
||||
ipc/flatbuffers_handler.cpp
|
||||
ipc/flatbuffers_util.hpp
|
||||
ipc/flatbuffers_util.cpp
|
||||
ipc/ipc_access_config.hpp
|
||||
ipc/ipc_access_config.cpp
|
||||
ipc/ipc_broker.hpp
|
||||
ipc/ipc_broker.cpp
|
||||
ipc/ipc_config.hpp
|
||||
ipc/ipc_config.cpp
|
||||
ipc/ipc_server.hpp
|
||||
ipc/ipc_server.cpp
|
||||
json_handler.hpp
|
||||
json_handler.cpp
|
||||
json_payment_observer.hpp
|
||||
|
@ -158,3 +168,7 @@ target_compile_definitions(node
|
|||
PRIVATE
|
||||
-DTAG_VERSION_STRING=${TAG_VERSION_STRING}
|
||||
-DGIT_COMMIT_HASH=${GIT_COMMIT_HASH})
|
||||
|
||||
# This ensures that any changes to Flatbuffers source files will cause a
|
||||
# regeneration of any C++ header files.
|
||||
add_dependencies(node ipc_flatbuffers_lib)
|
||||
|
|
|
@ -1,326 +0,0 @@
|
|||
#include <nano/boost/asio/local/stream_protocol.hpp>
|
||||
#include <nano/boost/asio/read.hpp>
|
||||
#include <nano/lib/config.hpp>
|
||||
#include <nano/lib/ipc.hpp>
|
||||
#include <nano/lib/threading.hpp>
|
||||
#include <nano/lib/timer.hpp>
|
||||
#include <nano/node/common.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/node/json_handler.hpp>
|
||||
#include <nano/node/node.hpp>
|
||||
|
||||
#include <boost/endian/conversion.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
using namespace boost::log;
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* A session represents an inbound connection over which multiple requests/reponses are transmitted.
|
||||
*/
|
||||
template <typename SOCKET_TYPE>
|
||||
class session : public nano::ipc::socket_base, public std::enable_shared_from_this<session<SOCKET_TYPE>>
|
||||
{
|
||||
public:
|
||||
session (nano::ipc::ipc_server & server_a, boost::asio::io_context & io_ctx_a, nano::ipc::ipc_config_transport & config_transport_a) :
|
||||
socket_base (io_ctx_a),
|
||||
server (server_a), node (server_a.node), session_id (server_a.id_dispenser.fetch_add (1)), io_ctx (io_ctx_a), socket (io_ctx_a), config_transport (config_transport_a)
|
||||
{
|
||||
if (node.config.logging.log_ipc ())
|
||||
{
|
||||
node.logger.always_log ("IPC: created session with id: ", session_id);
|
||||
}
|
||||
}
|
||||
|
||||
SOCKET_TYPE & get_socket ()
|
||||
{
|
||||
return socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async read of exactly \p size_a bytes. The callback is invoked only when all the data is available and
|
||||
* no error has occurred. On error, the error is logged, the read cycle stops and the session ends. Clients
|
||||
* are expected to implement reconnect logic.
|
||||
*/
|
||||
void async_read_exactly (void * buff_a, size_t size_a, std::function<void()> const & callback_a)
|
||||
{
|
||||
async_read_exactly (buff_a, size_a, std::chrono::seconds (config_transport.io_timeout), callback_a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async read of exactly \p size_a bytes and a specific \p timeout_a.
|
||||
* @see async_read_exactly (void *, size_t, std::function<void()>)
|
||||
*/
|
||||
void async_read_exactly (void * buff_a, size_t size_a, std::chrono::seconds timeout_a, std::function<void()> const & callback_a)
|
||||
{
|
||||
timer_start (timeout_a);
|
||||
auto this_l (this->shared_from_this ());
|
||||
boost::asio::async_read (socket,
|
||||
boost::asio::buffer (buff_a, size_a),
|
||||
boost::asio::transfer_exactly (size_a),
|
||||
[this_l, callback_a](boost::system::error_code const & ec, size_t bytes_transferred_a) {
|
||||
this_l->timer_cancel ();
|
||||
if (ec == boost::asio::error::connection_aborted || ec == boost::asio::error::connection_reset)
|
||||
{
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log (boost::str (boost::format ("IPC: error reading %1% ") % ec.message ()));
|
||||
}
|
||||
}
|
||||
else if (bytes_transferred_a > 0)
|
||||
{
|
||||
callback_a ();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Handler for payload_encoding::json_legacy */
|
||||
void handle_json_query (bool allow_unsafe)
|
||||
{
|
||||
session_timer.restart ();
|
||||
auto request_id_l (std::to_string (server.id_dispenser.fetch_add (1)));
|
||||
|
||||
// This is called when nano::rpc_handler#process_request is done. We convert to
|
||||
// json and write the response to the ipc socket with a length prefix.
|
||||
auto this_l (this->shared_from_this ());
|
||||
auto response_handler_l ([this_l, request_id_l](std::string const & body) {
|
||||
auto big = boost::endian::native_to_big (static_cast<uint32_t> (body.size ()));
|
||||
std::vector<uint8_t> buffer;
|
||||
buffer.insert (buffer.end (), reinterpret_cast<std::uint8_t *> (&big), reinterpret_cast<std::uint8_t *> (&big) + sizeof (std::uint32_t));
|
||||
buffer.insert (buffer.end (), body.begin (), body.end ());
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log (boost::str (boost::format ("IPC/RPC request %1% completed in: %2% %3%") % request_id_l % this_l->session_timer.stop ().count () % this_l->session_timer.unit ()));
|
||||
}
|
||||
|
||||
this_l->timer_start (std::chrono::seconds (this_l->config_transport.io_timeout));
|
||||
nano::async_write (this_l->socket, nano::shared_const_buffer (buffer), [this_l](boost::system::error_code const & error_a, size_t size_a) {
|
||||
this_l->timer_cancel ();
|
||||
if (!error_a)
|
||||
{
|
||||
this_l->read_next_request ();
|
||||
}
|
||||
else if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Write failed: ", error_a.message ());
|
||||
}
|
||||
});
|
||||
|
||||
// Do not call any member variables here (like session_timer) as it's possible that the next request may already be underway.
|
||||
});
|
||||
|
||||
node.stats.inc (nano::stat::type::ipc, nano::stat::detail::invocations);
|
||||
auto body (std::string (reinterpret_cast<char *> (buffer.data ()), buffer.size ()));
|
||||
|
||||
// Note that if the rpc action is async, the shared_ptr<json_handler> lifetime will be extended by the action handler
|
||||
auto handler (std::make_shared<nano::json_handler> (node, server.node_rpc_config, body, response_handler_l, [& server = server]() {
|
||||
server.stop ();
|
||||
server.node.alarm.add (std::chrono::steady_clock::now () + std::chrono::seconds (3), [& io_ctx = server.node.alarm.io_ctx]() {
|
||||
io_ctx.stop ();
|
||||
});
|
||||
}));
|
||||
// For unsafe actions to be allowed, the unsafe encoding must be used AND the transport config must allow it
|
||||
handler->process_request (allow_unsafe && config_transport.allow_unsafe);
|
||||
}
|
||||
|
||||
/** Async request reader */
|
||||
void read_next_request ()
|
||||
{
|
||||
auto this_l = this->shared_from_this ();
|
||||
|
||||
// Await next request indefinitely
|
||||
buffer.resize (sizeof (buffer_size));
|
||||
async_read_exactly (buffer.data (), buffer.size (), std::chrono::seconds::max (), [this_l]() {
|
||||
if (this_l->buffer[nano::ipc::preamble_offset::lead] != 'N' || this_l->buffer[nano::ipc::preamble_offset::reserved_1] != 0 || this_l->buffer[nano::ipc::preamble_offset::reserved_2] != 0)
|
||||
{
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Invalid preamble");
|
||||
}
|
||||
}
|
||||
else if (this_l->buffer[nano::ipc::preamble_offset::encoding] == static_cast<uint8_t> (nano::ipc::payload_encoding::json_legacy) || this_l->buffer[nano::ipc::preamble_offset::encoding] == static_cast<uint8_t> (nano::ipc::payload_encoding::json_unsafe))
|
||||
{
|
||||
auto allow_unsafe (this_l->buffer[nano::ipc::preamble_offset::encoding] == static_cast<uint8_t> (nano::ipc::payload_encoding::json_unsafe));
|
||||
// Length of payload
|
||||
this_l->async_read_exactly (&this_l->buffer_size, sizeof (this_l->buffer_size), [this_l, allow_unsafe]() {
|
||||
boost::endian::big_to_native_inplace (this_l->buffer_size);
|
||||
this_l->buffer.resize (this_l->buffer_size);
|
||||
// Payload (ptree compliant JSON string)
|
||||
this_l->async_read_exactly (this_l->buffer.data (), this_l->buffer_size, [this_l, allow_unsafe]() {
|
||||
this_l->handle_json_query (allow_unsafe);
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Unsupported payload encoding");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Shut down and close socket */
|
||||
void close ()
|
||||
{
|
||||
socket.shutdown (boost::asio::ip::tcp::socket::shutdown_both);
|
||||
socket.close ();
|
||||
}
|
||||
|
||||
private:
|
||||
nano::ipc::ipc_server & server;
|
||||
nano::node & node;
|
||||
|
||||
/** Unique session id used for logging */
|
||||
uint64_t session_id;
|
||||
|
||||
/** Timer for measuring the duration of ipc calls */
|
||||
nano::timer<std::chrono::microseconds> session_timer;
|
||||
|
||||
/**
|
||||
* IO context from node, or per-transport, depending on configuration.
|
||||
* Certain transports may scale better if they use a separate context.
|
||||
*/
|
||||
boost::asio::io_context & io_ctx;
|
||||
|
||||
/** A socket of the given asio type */
|
||||
SOCKET_TYPE socket;
|
||||
|
||||
/** Buffer sizes are read into this */
|
||||
uint32_t buffer_size{ 0 };
|
||||
|
||||
/** Buffer used to store data received from the client */
|
||||
std::vector<uint8_t> buffer;
|
||||
|
||||
/** Transport configuration */
|
||||
nano::ipc::ipc_config_transport & config_transport;
|
||||
};
|
||||
|
||||
/** Domain and TCP socket transport */
|
||||
template <typename ACCEPTOR_TYPE, typename SOCKET_TYPE, typename ENDPOINT_TYPE>
|
||||
class socket_transport : public nano::ipc::transport
|
||||
{
|
||||
public:
|
||||
socket_transport (nano::ipc::ipc_server & server_a, ENDPOINT_TYPE endpoint_a, nano::ipc::ipc_config_transport & config_transport_a, int concurrency_a) :
|
||||
server (server_a), config_transport (config_transport_a)
|
||||
{
|
||||
// Using a per-transport event dispatcher?
|
||||
if (concurrency_a > 0)
|
||||
{
|
||||
io_ctx = std::make_unique<boost::asio::io_context> ();
|
||||
}
|
||||
|
||||
boost::asio::socket_base::reuse_address option (true);
|
||||
boost::asio::socket_base::keep_alive option_keepalive (true);
|
||||
acceptor = std::make_unique<ACCEPTOR_TYPE> (context (), endpoint_a);
|
||||
acceptor->set_option (option);
|
||||
acceptor->set_option (option_keepalive);
|
||||
accept ();
|
||||
|
||||
// Start serving IO requests. If concurrency_a is < 1, the node's thread pool/io_context is used instead.
|
||||
// A separate io_context for domain sockets may facilitate better performance on some systems.
|
||||
if (concurrency_a > 0)
|
||||
{
|
||||
runner = std::make_unique<nano::thread_runner> (*io_ctx, static_cast<unsigned> (concurrency_a));
|
||||
}
|
||||
}
|
||||
|
||||
boost::asio::io_context & context () const
|
||||
{
|
||||
return io_ctx ? *io_ctx : server.node.io_ctx;
|
||||
}
|
||||
|
||||
void accept ()
|
||||
{
|
||||
// Prepare the next session
|
||||
auto new_session (std::make_shared<session<SOCKET_TYPE>> (server, context (), config_transport));
|
||||
|
||||
acceptor->async_accept (new_session->get_socket (), [this, new_session](boost::system::error_code const & ec) {
|
||||
if (!ec)
|
||||
{
|
||||
new_session->read_next_request ();
|
||||
}
|
||||
else
|
||||
{
|
||||
server.node.logger.always_log ("IPC: acceptor error: ", ec.message ());
|
||||
}
|
||||
|
||||
if (ec != boost::asio::error::operation_aborted && acceptor->is_open ())
|
||||
{
|
||||
this->accept ();
|
||||
}
|
||||
else
|
||||
{
|
||||
server.node.logger.always_log ("IPC: shutting down");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void stop ()
|
||||
{
|
||||
acceptor->close ();
|
||||
if (io_ctx)
|
||||
{
|
||||
io_ctx->stop ();
|
||||
}
|
||||
|
||||
if (runner)
|
||||
{
|
||||
runner->join ();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
nano::ipc::ipc_server & server;
|
||||
nano::ipc::ipc_config_transport & config_transport;
|
||||
std::unique_ptr<nano::thread_runner> runner;
|
||||
std::unique_ptr<boost::asio::io_context> io_ctx;
|
||||
std::unique_ptr<ACCEPTOR_TYPE> acceptor;
|
||||
};
|
||||
}
|
||||
|
||||
nano::ipc::ipc_server::ipc_server (nano::node & node_a, nano::node_rpc_config const & node_rpc_config_a) :
|
||||
node (node_a),
|
||||
node_rpc_config (node_rpc_config_a)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (node_a.config.ipc_config.transport_domain.enabled)
|
||||
{
|
||||
#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
|
||||
auto threads = node_a.config.ipc_config.transport_domain.io_threads;
|
||||
file_remover = std::make_unique<dsock_file_remover> (node_a.config.ipc_config.transport_domain.path);
|
||||
boost::asio::local::stream_protocol::endpoint ep{ node_a.config.ipc_config.transport_domain.path };
|
||||
transports.push_back (std::make_shared<socket_transport<boost::asio::local::stream_protocol::acceptor, boost::asio::local::stream_protocol::socket, boost::asio::local::stream_protocol::endpoint>> (*this, ep, node_a.config.ipc_config.transport_domain, threads));
|
||||
#else
|
||||
node.logger.always_log ("IPC: Domain sockets are not supported on this platform");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (node_a.config.ipc_config.transport_tcp.enabled)
|
||||
{
|
||||
auto threads = node_a.config.ipc_config.transport_tcp.io_threads;
|
||||
transports.push_back (std::make_shared<socket_transport<boost::asio::ip::tcp::acceptor, boost::asio::ip::tcp::socket, boost::asio::ip::tcp::endpoint>> (*this, boost::asio::ip::tcp::endpoint (boost::asio::ip::tcp::v6 (), node_a.config.ipc_config.transport_tcp.port), node_a.config.ipc_config.transport_tcp, threads));
|
||||
}
|
||||
|
||||
node.logger.always_log ("IPC: server started");
|
||||
}
|
||||
catch (std::runtime_error const & ex)
|
||||
{
|
||||
node.logger.always_log ("IPC: ", ex.what ());
|
||||
}
|
||||
}
|
||||
|
||||
nano::ipc::ipc_server::~ipc_server ()
|
||||
{
|
||||
node.logger.always_log ("IPC: server stopped");
|
||||
}
|
||||
|
||||
void nano::ipc::ipc_server::stop ()
|
||||
{
|
||||
for (auto & transport : transports)
|
||||
{
|
||||
transport->stop ();
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/lib/ipc.hpp>
|
||||
#include <nano/node/node_rpc_config.hpp>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace nano
|
||||
{
|
||||
class node;
|
||||
|
||||
namespace ipc
|
||||
{
|
||||
/** The IPC server accepts connections on one or more configured transports */
|
||||
class ipc_server
|
||||
{
|
||||
public:
|
||||
ipc_server (nano::node & node_a, nano::node_rpc_config const & node_rpc_config);
|
||||
|
||||
virtual ~ipc_server ();
|
||||
void stop ();
|
||||
|
||||
nano::node & node;
|
||||
nano::node_rpc_config const & node_rpc_config;
|
||||
|
||||
/** Unique counter/id shared across sessions */
|
||||
std::atomic<uint64_t> id_dispenser{ 0 };
|
||||
|
||||
private:
|
||||
std::unique_ptr<dsock_file_remover> file_remover;
|
||||
std::vector<std::shared_ptr<nano::ipc::transport>> transports;
|
||||
};
|
||||
}
|
||||
}
|
183
nano/node/ipc/action_handler.cpp
Normal file
183
nano/node/ipc/action_handler.cpp
Normal file
|
@ -0,0 +1,183 @@
|
|||
#include <nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h>
|
||||
#include <nano/lib/errors.hpp>
|
||||
#include <nano/lib/numbers.hpp>
|
||||
#include <nano/node/ipc/action_handler.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/node.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace
|
||||
{
|
||||
nano::account parse_account (std::string const & account, bool & out_is_deprecated_format)
|
||||
{
|
||||
nano::account result (0);
|
||||
if (account.empty ())
|
||||
{
|
||||
throw nano::error (nano::error_common::bad_account_number);
|
||||
}
|
||||
if (result.decode_account (account))
|
||||
{
|
||||
throw nano::error (nano::error_common::bad_account_number);
|
||||
}
|
||||
else if (account[3] == '-' || account[4] == '-')
|
||||
{
|
||||
out_is_deprecated_format = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
/** Returns the message as a Flatbuffers ObjectAPI type, managed by a unique_ptr */
|
||||
template <typename T>
|
||||
auto get_message (nanoapi::Envelope const & envelope)
|
||||
{
|
||||
auto raw (envelope.message_as<T> ()->UnPack ());
|
||||
return std::unique_ptr<typename T::NativeTableType> (raw);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping from message type to handler function.
|
||||
* @note This must be updated whenever a new message type is added to the Flatbuffers IDL.
|
||||
*/
|
||||
auto nano::ipc::action_handler::handler_map () -> std::unordered_map<nanoapi::Message, std::function<void(nano::ipc::action_handler *, nanoapi::Envelope const &)>, nano::ipc::enum_hash>
|
||||
{
|
||||
static std::unordered_map<nanoapi::Message, std::function<void(nano::ipc::action_handler *, nanoapi::Envelope const &)>, nano::ipc::enum_hash> handlers;
|
||||
if (handlers.empty ())
|
||||
{
|
||||
handlers.emplace (nanoapi::Message::Message_IsAlive, &nano::ipc::action_handler::on_is_alive);
|
||||
handlers.emplace (nanoapi::Message::Message_TopicConfirmation, &nano::ipc::action_handler::on_topic_confirmation);
|
||||
handlers.emplace (nanoapi::Message::Message_AccountWeight, &nano::ipc::action_handler::on_account_weight);
|
||||
handlers.emplace (nanoapi::Message::Message_ServiceRegister, &nano::ipc::action_handler::on_service_register);
|
||||
handlers.emplace (nanoapi::Message::Message_ServiceStop, &nano::ipc::action_handler::on_service_stop);
|
||||
handlers.emplace (nanoapi::Message::Message_TopicServiceStop, &nano::ipc::action_handler::on_topic_service_stop);
|
||||
}
|
||||
return handlers;
|
||||
}
|
||||
|
||||
nano::ipc::action_handler::action_handler (nano::node & node_a, nano::ipc::ipc_server & server_a, std::weak_ptr<nano::ipc::subscriber> const & subscriber_a, std::shared_ptr<flatbuffers::FlatBufferBuilder> const & builder_a) :
|
||||
flatbuffer_producer (builder_a),
|
||||
node (node_a),
|
||||
ipc_server (server_a),
|
||||
subscriber (subscriber_a)
|
||||
{
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::on_topic_confirmation (nanoapi::Envelope const & envelope_a)
|
||||
{
|
||||
auto confirmationTopic (get_message<nanoapi::TopicConfirmation> (envelope_a));
|
||||
ipc_server.get_broker ().subscribe (subscriber, std::move (confirmationTopic));
|
||||
nanoapi::EventAckT ack;
|
||||
create_response (ack);
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::on_service_register (nanoapi::Envelope const & envelope_a)
|
||||
{
|
||||
require_oneof (envelope_a, { nano::ipc::access_permission::api_service_register, nano::ipc::access_permission::service });
|
||||
auto query (get_message<nanoapi::ServiceRegister> (envelope_a));
|
||||
ipc_server.get_broker ().service_register (query->service_name, this->subscriber);
|
||||
nanoapi::SuccessT success;
|
||||
create_response (success);
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::on_service_stop (nanoapi::Envelope const & envelope_a)
|
||||
{
|
||||
require_oneof (envelope_a, { nano::ipc::access_permission::api_service_stop, nano::ipc::access_permission::service });
|
||||
auto query (get_message<nanoapi::ServiceStop> (envelope_a));
|
||||
if (query->service_name == "node")
|
||||
{
|
||||
ipc_server.node.stop ();
|
||||
}
|
||||
else
|
||||
{
|
||||
ipc_server.get_broker ().service_stop (query->service_name);
|
||||
}
|
||||
nanoapi::SuccessT success;
|
||||
create_response (success);
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::on_topic_service_stop (nanoapi::Envelope const & envelope_a)
|
||||
{
|
||||
auto topic (get_message<nanoapi::TopicServiceStop> (envelope_a));
|
||||
ipc_server.get_broker ().subscribe (subscriber, std::move (topic));
|
||||
nanoapi::EventAckT ack;
|
||||
create_response (ack);
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::on_account_weight (nanoapi::Envelope const & envelope_a)
|
||||
{
|
||||
require_oneof (envelope_a, { nano::ipc::access_permission::api_account_weight, nano::ipc::access_permission::account_query });
|
||||
bool is_deprecated_format{ false };
|
||||
auto query (get_message<nanoapi::AccountWeight> (envelope_a));
|
||||
auto balance (node.weight (parse_account (query->account, is_deprecated_format)));
|
||||
|
||||
nanoapi::AccountWeightResponseT response;
|
||||
response.voting_weight = balance.str ();
|
||||
create_response (response);
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::on_is_alive (nanoapi::Envelope const & envelope)
|
||||
{
|
||||
nanoapi::IsAliveT alive;
|
||||
create_response (alive);
|
||||
}
|
||||
|
||||
bool nano::ipc::action_handler::has_access (nanoapi::Envelope const & envelope_a, nano::ipc::access_permission permission_a) const noexcept
|
||||
{
|
||||
// If credentials are missing in the envelope, the default user is used
|
||||
std::string credentials;
|
||||
if (envelope_a.credentials () != nullptr)
|
||||
{
|
||||
credentials = envelope_a.credentials ()->str ();
|
||||
}
|
||||
|
||||
return ipc_server.get_access ().has_access (credentials, permission_a);
|
||||
}
|
||||
|
||||
bool nano::ipc::action_handler::has_access_to_all (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const noexcept
|
||||
{
|
||||
// If credentials are missing in the envelope, the default user is used
|
||||
std::string credentials;
|
||||
if (envelope_a.credentials () != nullptr)
|
||||
{
|
||||
credentials = envelope_a.credentials ()->str ();
|
||||
}
|
||||
|
||||
return ipc_server.get_access ().has_access_to_all (credentials, permissions_a);
|
||||
}
|
||||
|
||||
bool nano::ipc::action_handler::has_access_to_oneof (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const noexcept
|
||||
{
|
||||
// If credentials are missing in the envelope, the default user is used
|
||||
std::string credentials;
|
||||
if (envelope_a.credentials () != nullptr)
|
||||
{
|
||||
credentials = envelope_a.credentials ()->str ();
|
||||
}
|
||||
|
||||
return ipc_server.get_access ().has_access_to_oneof (credentials, permissions_a);
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::require (nanoapi::Envelope const & envelope_a, nano::ipc::access_permission permission_a) const
|
||||
{
|
||||
if (!has_access (envelope_a, permission_a))
|
||||
{
|
||||
throw nano::error (nano::error_common::access_denied);
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::require_all (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const
|
||||
{
|
||||
if (!has_access_to_all (envelope_a, permissions_a))
|
||||
{
|
||||
throw nano::error (nano::error_common::access_denied);
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::action_handler::require_oneof (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const
|
||||
{
|
||||
if (!has_access_to_oneof (envelope_a, permissions_a))
|
||||
{
|
||||
throw nano::error (nano::error_common::access_denied);
|
||||
}
|
||||
}
|
71
nano/node/ipc/action_handler.hpp
Normal file
71
nano/node/ipc/action_handler.hpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/ipc_flatbuffers_lib/flatbuffer_producer.hpp>
|
||||
#include <nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h>
|
||||
#include <nano/node/ipc/ipc_access_config.hpp>
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
#include <flatbuffers/idl.h>
|
||||
#include <flatbuffers/minireflect.h>
|
||||
#include <flatbuffers/registry.h>
|
||||
#include <flatbuffers/util.h>
|
||||
|
||||
namespace nano
|
||||
{
|
||||
class error;
|
||||
class node;
|
||||
namespace ipc
|
||||
{
|
||||
class ipc_server;
|
||||
class subscriber;
|
||||
|
||||
/**
|
||||
* Implements handlers for the various public IPC messages. When an action handler is completed,
|
||||
* the flatbuffer contains the serialized response object.
|
||||
* @note This is a light-weight class, and an instance can be created for every request.
|
||||
*/
|
||||
class action_handler final : public flatbuffer_producer, public std::enable_shared_from_this<action_handler>
|
||||
{
|
||||
public:
|
||||
action_handler (nano::node & node, nano::ipc::ipc_server & server, std::weak_ptr<nano::ipc::subscriber> const & subscriber, std::shared_ptr<flatbuffers::FlatBufferBuilder> const & builder);
|
||||
|
||||
void on_account_weight (nanoapi::Envelope const & envelope);
|
||||
void on_is_alive (nanoapi::Envelope const & envelope);
|
||||
void on_topic_confirmation (nanoapi::Envelope const & envelope);
|
||||
|
||||
/** Request to register a service. The service name is associated with the current session. */
|
||||
void on_service_register (nanoapi::Envelope const & envelope);
|
||||
|
||||
/** Request to stop a service by name */
|
||||
void on_service_stop (nanoapi::Envelope const & envelope);
|
||||
|
||||
/** Subscribe to the ServiceStop event. The service must first have registered itself on the same session. */
|
||||
void on_topic_service_stop (nanoapi::Envelope const & envelope);
|
||||
|
||||
/** Returns a mapping from api message types to handler functions */
|
||||
static auto handler_map () -> std::unordered_map<nanoapi::Message, std::function<void(action_handler *, nanoapi::Envelope const &)>, nano::ipc::enum_hash>;
|
||||
|
||||
private:
|
||||
bool has_access (nanoapi::Envelope const & envelope_a, nano::ipc::access_permission permission_a) const noexcept;
|
||||
bool has_access_to_all (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const noexcept;
|
||||
bool has_access_to_oneof (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const noexcept;
|
||||
void require (nanoapi::Envelope const & envelope_a, nano::ipc::access_permission permission_a) const;
|
||||
void require_all (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const;
|
||||
void require_oneof (nanoapi::Envelope const & envelope_a, std::initializer_list<nano::ipc::access_permission> alternative_permissions_a) const;
|
||||
|
||||
nano::node & node;
|
||||
nano::ipc::ipc_server & ipc_server;
|
||||
std::weak_ptr<nano::ipc::subscriber> subscriber;
|
||||
};
|
||||
}
|
||||
}
|
198
nano/node/ipc/flatbuffers_handler.cpp
Normal file
198
nano/node/ipc/flatbuffers_handler.cpp
Normal file
|
@ -0,0 +1,198 @@
|
|||
#include <nano/lib/errors.hpp>
|
||||
#include <nano/node/ipc/action_handler.hpp>
|
||||
#include <nano/node/ipc/flatbuffers_handler.hpp>
|
||||
#include <nano/node/ipc/ipc_config.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
|
||||
#include <boost/dll.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
#include <flatbuffers/idl.h>
|
||||
#include <flatbuffers/minireflect.h>
|
||||
#include <flatbuffers/registry.h>
|
||||
#include <flatbuffers/util.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
auto handler_map = nano::ipc::action_handler::handler_map ();
|
||||
|
||||
/**
|
||||
* A helper for when it's necessary to create a JSON error response manually
|
||||
*/
|
||||
std::string make_error_response (std::string const & error_message)
|
||||
{
|
||||
std::ostringstream json;
|
||||
json << R"json({"message_type": "Error", "message": {"code": 1, "message": ")json"
|
||||
<< error_message
|
||||
<< R"json("}})json";
|
||||
return json.str ();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 'api/flatbuffers' directory, boost::none if not found.
|
||||
* This searches the binary path as well as the parent (which is mostly useful for development)
|
||||
*/
|
||||
boost::optional<boost::filesystem::path> get_api_path ()
|
||||
{
|
||||
auto parent_path = boost::dll::program_location ().parent_path ();
|
||||
if (!boost::filesystem::exists (parent_path / "api" / "flatbuffers"))
|
||||
{
|
||||
// See if the parent directory has the api subdirectories
|
||||
if (parent_path.has_parent_path ())
|
||||
{
|
||||
parent_path = boost::dll::program_location ().parent_path ().parent_path ();
|
||||
}
|
||||
|
||||
if (!boost::filesystem::exists (parent_path / "api" / "flatbuffers"))
|
||||
{
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
return parent_path / "api" / "flatbuffers";
|
||||
}
|
||||
}
|
||||
|
||||
nano::ipc::flatbuffers_handler::flatbuffers_handler (nano::node & node_a, nano::ipc::ipc_server & ipc_server_a, std::shared_ptr<nano::ipc::subscriber> const & subscriber_a, nano::ipc::ipc_config const & ipc_config_a) :
|
||||
node (node_a),
|
||||
ipc_server (ipc_server_a),
|
||||
subscriber (subscriber_a),
|
||||
ipc_config (ipc_config_a)
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<flatbuffers::Parser> nano::ipc::flatbuffers_handler::make_flatbuffers_parser (nano::ipc::ipc_config const & ipc_config_a)
|
||||
{
|
||||
auto parser (std::make_shared<flatbuffers::Parser> ());
|
||||
parser->opts.strict_json = true;
|
||||
parser->opts.skip_unexpected_fields_in_json = ipc_config_a.flatbuffers.skip_unexpected_fields_in_json;
|
||||
|
||||
auto api_path = get_api_path ();
|
||||
if (!api_path)
|
||||
{
|
||||
throw nano::error ("Internal IPC error: unable to find api path");
|
||||
}
|
||||
|
||||
const char * include_directories[] = { api_path->string ().c_str (), nullptr };
|
||||
std::string schemafile;
|
||||
if (!flatbuffers::LoadFile ((*api_path / "nanoapi.fbs").string ().c_str (), false, &schemafile))
|
||||
{
|
||||
throw nano::error ("Internal IPC error: unable to load schema file");
|
||||
}
|
||||
|
||||
auto parse_success = parser->Parse (schemafile.c_str (), include_directories);
|
||||
if (!parse_success)
|
||||
{
|
||||
std::string parser_error = "Internal IPC error: unable to parse schema file: ";
|
||||
parser_error += parser->error_.c_str ();
|
||||
throw nano::error (parser_error);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
|
||||
void nano::ipc::flatbuffers_handler::process_json (const uint8_t * message_buffer_a, size_t buffer_size_a,
|
||||
std::function<void(std::shared_ptr<std::string>)> response_handler)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!parser)
|
||||
{
|
||||
parser = make_flatbuffers_parser (ipc_config);
|
||||
}
|
||||
|
||||
// Convert request from JSON
|
||||
auto body (std::string (reinterpret_cast<char *> (const_cast<uint8_t *> (message_buffer_a)), buffer_size_a));
|
||||
body += '\0';
|
||||
if (parser->Parse (reinterpret_cast<const char *> (body.data ())))
|
||||
{
|
||||
process (parser->builder_.GetBufferPointer (), parser->builder_.GetSize (), [parser = parser, response_handler](std::shared_ptr<flatbuffers::FlatBufferBuilder> fbb) {
|
||||
// Convert response to JSON
|
||||
auto json (std::make_shared<std::string> ());
|
||||
if (!flatbuffers::GenerateText (*parser, fbb->GetBufferPointer (), json.get ()))
|
||||
{
|
||||
throw nano::error ("Couldn't serialize response to JSON");
|
||||
}
|
||||
|
||||
response_handler (json);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string parser_error = "Invalid message format: ";
|
||||
parser_error += parser->error_.c_str ();
|
||||
throw nano::error (parser_error);
|
||||
}
|
||||
}
|
||||
catch (nano::error const & err)
|
||||
{
|
||||
// Forces the parser construction to be retried as certain errors are
|
||||
// recoverable (such path errors getting fixed by the user without a node restart)
|
||||
parser = nullptr;
|
||||
|
||||
// Convert error response to JSON. We must construct this manually since the exception
|
||||
// may be parser related (such as not being able to load the schema)
|
||||
response_handler (std::make_shared<std::string> (make_error_response (err.get_message ())));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::cerr << "Unknown exception in " << __FUNCTION__ << std::endl;
|
||||
response_handler (std::make_shared<std::string> (make_error_response ("Unknown exception")));
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::flatbuffers_handler::process (const uint8_t * message_buffer_a, size_t buffer_size_a,
|
||||
std::function<void(std::shared_ptr<flatbuffers::FlatBufferBuilder>)> response_handler)
|
||||
{
|
||||
auto buffer_l (std::make_shared<flatbuffers::FlatBufferBuilder> ());
|
||||
auto actionhandler (std::make_shared<action_handler> (node, ipc_server, subscriber, buffer_l));
|
||||
std::string correlationId = "";
|
||||
|
||||
// Find and call the action handler
|
||||
try
|
||||
{
|
||||
// By default we verify the buffers, to make sure offsets reside inside the buffer.
|
||||
// This brings the buffer into cache, making the overall verify+parse overhead low.
|
||||
if (ipc_config.flatbuffers.verify_buffers)
|
||||
{
|
||||
auto verifier (flatbuffers::Verifier (message_buffer_a, buffer_size_a));
|
||||
if (!nanoapi::VerifyEnvelopeBuffer (verifier))
|
||||
{
|
||||
throw nano::error ("Envelope buffer did not pass verifier");
|
||||
}
|
||||
}
|
||||
|
||||
auto incoming = nanoapi::GetEnvelope (message_buffer_a);
|
||||
if (incoming == nullptr)
|
||||
{
|
||||
nano::error err ("Invalid message");
|
||||
actionhandler->make_error (err.error_code_as_int (), err.get_message ());
|
||||
response_handler (buffer_l);
|
||||
return;
|
||||
}
|
||||
|
||||
auto handler_method = handler_map.find (incoming->message_type ());
|
||||
if (handler_method != handler_map.end ())
|
||||
{
|
||||
if (incoming->correlation_id ())
|
||||
{
|
||||
actionhandler->set_correlation_id (incoming->correlation_id ()->str ());
|
||||
}
|
||||
handler_method->second (actionhandler.get (), *incoming);
|
||||
}
|
||||
else
|
||||
{
|
||||
nano::error err ("Unknown message type");
|
||||
actionhandler->make_error (err.error_code_as_int (), err.get_message ());
|
||||
}
|
||||
}
|
||||
catch (nano::error const & err)
|
||||
{
|
||||
actionhandler->make_error (err.error_code_as_int (), err.get_message ());
|
||||
}
|
||||
|
||||
response_handler (buffer_l);
|
||||
}
|
65
nano/node/ipc/flatbuffers_handler.hpp
Normal file
65
nano/node/ipc/flatbuffers_handler.hpp
Normal file
|
@ -0,0 +1,65 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace flatbuffers
|
||||
{
|
||||
class FlatBufferBuilder;
|
||||
class Parser;
|
||||
}
|
||||
namespace nano
|
||||
{
|
||||
class node;
|
||||
namespace ipc
|
||||
{
|
||||
class subscriber;
|
||||
class ipc_config;
|
||||
class ipc_server;
|
||||
/**
|
||||
* This handler sits between the IPC server and the action handler. Its job is to deserialize
|
||||
* Flatbuffers in binary and json formats into high level message objects. These messages are
|
||||
* then used to dispatch the correct action handler.
|
||||
* @throws Methods of this class throw nano::error on failure.
|
||||
* @note This class is not thread safe; use one instance per session/thread.
|
||||
*/
|
||||
class flatbuffers_handler final : public std::enable_shared_from_this<flatbuffers_handler>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Constructs the handler.
|
||||
* @param node_a Node
|
||||
* @param subscriber Subscriber instance
|
||||
* @param ipc_server_a Optional IPC server (may be nullptr, i.e when calling through the RPC gateway)
|
||||
*/
|
||||
flatbuffers_handler (nano::node & node_a, nano::ipc::ipc_server & ipc_server_a, std::shared_ptr<nano::ipc::subscriber> const & subscriber_a, nano::ipc::ipc_config const & ipc_config_a);
|
||||
|
||||
/**
|
||||
* Deserialize flatbuffer message, look up and call the action handler, then call the response handler with a
|
||||
* FlatBufferBuilder to allow for zero-copy transfers of data.
|
||||
* @param response_handler Receives a shared pointer to the flatbuffer builder, from which the buffer and size can be queried
|
||||
* @throw Throws std:runtime_error on deserialization or processing errors
|
||||
*/
|
||||
void process (const uint8_t * message_buffer_a, size_t buffer_size_a, std::function<void(std::shared_ptr<flatbuffers::FlatBufferBuilder>)> response_handler);
|
||||
|
||||
/**
|
||||
* Parses a JSON encoded requests into Flatbuffer format, calls process(), yields the result as a JSON string
|
||||
*/
|
||||
void process_json (const uint8_t * message_buffer_a, size_t buffer_size_a, std::function<void(std::shared_ptr<std::string>)> response_handler);
|
||||
|
||||
/**
|
||||
* Creates a Flatbuffers parser with the schema preparsed. This can then be used to parse and produce JSON.
|
||||
*/
|
||||
static std::shared_ptr<flatbuffers::Parser> make_flatbuffers_parser (nano::ipc::ipc_config const & ipc_config_a);
|
||||
|
||||
private:
|
||||
std::shared_ptr<flatbuffers::Parser> parser;
|
||||
nano::node & node;
|
||||
nano::ipc::ipc_server & ipc_server;
|
||||
std::weak_ptr<nano::ipc::subscriber> subscriber;
|
||||
nano::ipc::ipc_config const & ipc_config;
|
||||
};
|
||||
}
|
||||
}
|
120
nano/node/ipc/flatbuffers_util.cpp
Normal file
120
nano/node/ipc/flatbuffers_util.cpp
Normal file
|
@ -0,0 +1,120 @@
|
|||
#include <nano/lib/blocks.hpp>
|
||||
#include <nano/lib/numbers.hpp>
|
||||
#include <nano/node/ipc/flatbuffers_util.hpp>
|
||||
#include <nano/secure/common.hpp>
|
||||
|
||||
std::unique_ptr<nanoapi::BlockStateT> nano::ipc::flatbuffers_builder::from (nano::state_block const & block_a, nano::amount const & amount_a, bool is_state_send_a)
|
||||
{
|
||||
static nano::network_params params;
|
||||
auto block (std::make_unique<nanoapi::BlockStateT> ());
|
||||
block->account = block_a.account ().to_account ();
|
||||
block->hash = block_a.hash ().to_string ();
|
||||
block->previous = block_a.previous ().to_string ();
|
||||
block->representative = block_a.representative ().to_account ();
|
||||
block->balance = block_a.balance ().to_string_dec ();
|
||||
block->link = block_a.link ().to_string ();
|
||||
block->link_as_account = block_a.link ().to_account ();
|
||||
block_a.signature.encode_hex (block->signature);
|
||||
block->work = nano::to_string_hex (block_a.work);
|
||||
|
||||
if (is_state_send_a)
|
||||
{
|
||||
block->subtype = nanoapi::BlockSubType::BlockSubType_send;
|
||||
}
|
||||
else if (block_a.link ().is_zero ())
|
||||
{
|
||||
block->subtype = nanoapi::BlockSubType::BlockSubType_change;
|
||||
}
|
||||
else if (amount_a == 0 && params.ledger.epochs.is_epoch_link (block_a.link ()))
|
||||
{
|
||||
block->subtype = nanoapi::BlockSubType::BlockSubType_epoch;
|
||||
}
|
||||
else
|
||||
{
|
||||
block->subtype = nanoapi::BlockSubType::BlockSubType_receive;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
std::unique_ptr<nanoapi::BlockSendT> nano::ipc::flatbuffers_builder::from (nano::send_block const & block_a)
|
||||
{
|
||||
auto block (std::make_unique<nanoapi::BlockSendT> ());
|
||||
block->hash = block_a.hash ().to_string ();
|
||||
block->balance = block_a.balance ().to_string_dec ();
|
||||
block->destination = block_a.hashables.destination.to_account ();
|
||||
block->previous = block_a.previous ().to_string ();
|
||||
block_a.signature.encode_hex (block->signature);
|
||||
block->work = nano::to_string_hex (block_a.work);
|
||||
return block;
|
||||
}
|
||||
|
||||
std::unique_ptr<nanoapi::BlockReceiveT> nano::ipc::flatbuffers_builder::from (nano::receive_block const & block_a)
|
||||
{
|
||||
auto block (std::make_unique<nanoapi::BlockReceiveT> ());
|
||||
block->hash = block_a.hash ().to_string ();
|
||||
block->source = block_a.source ().to_string ();
|
||||
block->previous = block_a.previous ().to_string ();
|
||||
block_a.signature.encode_hex (block->signature);
|
||||
block->work = nano::to_string_hex (block_a.work);
|
||||
return block;
|
||||
}
|
||||
|
||||
std::unique_ptr<nanoapi::BlockOpenT> nano::ipc::flatbuffers_builder::from (nano::open_block const & block_a)
|
||||
{
|
||||
auto block (std::make_unique<nanoapi::BlockOpenT> ());
|
||||
block->hash = block_a.hash ().to_string ();
|
||||
block->source = block_a.source ().to_string ();
|
||||
block->account = block_a.account ().to_account ();
|
||||
block->representative = block_a.representative ().to_account ();
|
||||
block_a.signature.encode_hex (block->signature);
|
||||
block->work = nano::to_string_hex (block_a.work);
|
||||
return block;
|
||||
}
|
||||
|
||||
std::unique_ptr<nanoapi::BlockChangeT> nano::ipc::flatbuffers_builder::from (nano::change_block const & block_a)
|
||||
{
|
||||
auto block (std::make_unique<nanoapi::BlockChangeT> ());
|
||||
block->hash = block_a.hash ().to_string ();
|
||||
block->previous = block_a.previous ().to_string ();
|
||||
block->representative = block_a.representative ().to_account ();
|
||||
block_a.signature.encode_hex (block->signature);
|
||||
block->work = nano::to_string_hex (block_a.work);
|
||||
return block;
|
||||
}
|
||||
|
||||
nanoapi::BlockUnion nano::ipc::flatbuffers_builder::block_to_union (nano::block const & block_a, nano::amount const & amount_a, bool is_state_send_a)
|
||||
{
|
||||
nanoapi::BlockUnion u;
|
||||
switch (block_a.type ())
|
||||
{
|
||||
case nano::block_type::state:
|
||||
{
|
||||
u.Set (*from (dynamic_cast<nano::state_block const &> (block_a), amount_a, is_state_send_a));
|
||||
break;
|
||||
}
|
||||
case nano::block_type::send:
|
||||
{
|
||||
u.Set (*from (dynamic_cast<nano::send_block const &> (block_a)));
|
||||
break;
|
||||
}
|
||||
case nano::block_type::receive:
|
||||
{
|
||||
u.Set (*from (dynamic_cast<nano::receive_block const &> (block_a)));
|
||||
break;
|
||||
}
|
||||
case nano::block_type::open:
|
||||
{
|
||||
u.Set (*from (dynamic_cast<nano::open_block const &> (block_a)));
|
||||
break;
|
||||
}
|
||||
case nano::block_type::change:
|
||||
{
|
||||
u.Set (*from (dynamic_cast<nano::change_block const &> (block_a)));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
assert (false);
|
||||
}
|
||||
return u;
|
||||
}
|
32
nano/node/ipc/flatbuffers_util.hpp
Normal file
32
nano/node/ipc/flatbuffers_util.hpp
Normal file
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace nano
|
||||
{
|
||||
class amount;
|
||||
class block;
|
||||
class send_block;
|
||||
class receive_block;
|
||||
class change_block;
|
||||
class open_block;
|
||||
class state_block;
|
||||
namespace ipc
|
||||
{
|
||||
/**
|
||||
* Utilities to convert between blocks and Flatbuffers equivalents
|
||||
*/
|
||||
class flatbuffers_builder
|
||||
{
|
||||
public:
|
||||
static nanoapi::BlockUnion block_to_union (nano::block const & block_a, nano::amount const & amount_a, bool is_state_send_a = false);
|
||||
static std::unique_ptr<nanoapi::BlockStateT> from (nano::state_block const & block_a, nano::amount const & amount_a, bool is_state_send_a);
|
||||
static std::unique_ptr<nanoapi::BlockSendT> from (nano::send_block const & block_a);
|
||||
static std::unique_ptr<nanoapi::BlockReceiveT> from (nano::receive_block const & block_a);
|
||||
static std::unique_ptr<nanoapi::BlockOpenT> from (nano::open_block const & block_a);
|
||||
static std::unique_ptr<nanoapi::BlockChangeT> from (nano::change_block const & block_a);
|
||||
};
|
||||
}
|
||||
}
|
345
nano/node/ipc/ipc_access_config.cpp
Normal file
345
nano/node/ipc/ipc_access_config.cpp
Normal file
|
@ -0,0 +1,345 @@
|
|||
#include <nano/lib/tomlconfig.hpp>
|
||||
#include <nano/node/ipc/ipc_access_config.hpp>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Convert permission to strings. This is how permissions appears in config-access.toml */
|
||||
std::string to_string (nano::ipc::access_permission permission)
|
||||
{
|
||||
switch (permission)
|
||||
{
|
||||
case nano::ipc::access_permission::invalid:
|
||||
return "invalid";
|
||||
case nano::ipc::access_permission::unrestricted:
|
||||
return "unrestricted";
|
||||
case nano::ipc::access_permission::api_service_register:
|
||||
return "api_service_register";
|
||||
case nano::ipc::access_permission::api_service_stop:
|
||||
return "api_service_stop";
|
||||
case nano::ipc::access_permission::api_account_weight:
|
||||
return "api_account_weight";
|
||||
case nano::ipc::access_permission::api_topic_confirmation:
|
||||
return "api_topic_confirmation";
|
||||
case nano::ipc::access_permission::api_topic_service_stop:
|
||||
return "api_topic_service_stop";
|
||||
case nano::ipc::access_permission::account_query:
|
||||
return "account_query";
|
||||
case nano::ipc::access_permission::epoch_upgrade:
|
||||
return "epoch_upgrade";
|
||||
case nano::ipc::access_permission::service:
|
||||
return "service";
|
||||
case nano::ipc::access_permission::wallet:
|
||||
return "wallet";
|
||||
case nano::ipc::access_permission::wallet_read:
|
||||
return "wallet_read";
|
||||
case nano::ipc::access_permission::wallet_write:
|
||||
return "wallet_write";
|
||||
case nano::ipc::access_permission::wallet_seed_change:
|
||||
return "wallet_seed_change";
|
||||
}
|
||||
|
||||
return "invalid";
|
||||
}
|
||||
|
||||
/** Convert string to permission */
|
||||
nano::ipc::access_permission from_string (std::string permission)
|
||||
{
|
||||
if (permission == "unrestricted")
|
||||
return nano::ipc::access_permission::unrestricted;
|
||||
if (permission == "api_account_weight")
|
||||
return nano::ipc::access_permission::api_account_weight;
|
||||
if (permission == "api_service_register")
|
||||
return nano::ipc::access_permission::api_service_register;
|
||||
if (permission == "api_service_stop")
|
||||
return nano::ipc::access_permission::api_service_stop;
|
||||
if (permission == "api_topic_service_stop")
|
||||
return nano::ipc::access_permission::api_topic_service_stop;
|
||||
if (permission == "api_topic_confirmation")
|
||||
return nano::ipc::access_permission::api_topic_confirmation;
|
||||
if (permission == "account_query")
|
||||
return nano::ipc::access_permission::account_query;
|
||||
if (permission == "epoch_upgrade")
|
||||
return nano::ipc::access_permission::epoch_upgrade;
|
||||
if (permission == "service")
|
||||
return nano::ipc::access_permission::service;
|
||||
if (permission == "wallet")
|
||||
return nano::ipc::access_permission::wallet;
|
||||
if (permission == "wallet_read")
|
||||
return nano::ipc::access_permission::wallet_read;
|
||||
if (permission == "wallet_write")
|
||||
return nano::ipc::access_permission::wallet_write;
|
||||
if (permission == "wallet_seed_change")
|
||||
return nano::ipc::access_permission::wallet_seed_change;
|
||||
|
||||
return nano::ipc::access_permission::invalid;
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::access::set_effective_permissions (nano::ipc::access_subject & subject_a, std::shared_ptr<cpptoml::table> const & config_subject_a)
|
||||
{
|
||||
std::string allow_l (config_subject_a->get_as<std::string> ("allow").value_or (""));
|
||||
std::vector<std::string> allow_strings_l;
|
||||
boost::split (allow_strings_l, allow_l, boost::is_any_of (","));
|
||||
for (auto const & permission : allow_strings_l)
|
||||
{
|
||||
if (!permission.empty ())
|
||||
{
|
||||
auto permission_enum = from_string (boost::trim_copy (permission));
|
||||
if (permission_enum != nano::ipc::access_permission::invalid)
|
||||
{
|
||||
subject_a.permissions.insert (permission_enum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string deny_l (config_subject_a->get_as<std::string> ("deny").value_or (""));
|
||||
std::vector<std::string> deny_strings_l;
|
||||
boost::split (deny_strings_l, deny_l, boost::is_any_of (","));
|
||||
for (auto const & permission : deny_strings_l)
|
||||
{
|
||||
if (!permission.empty ())
|
||||
{
|
||||
auto permission_enum = from_string (boost::trim_copy (permission));
|
||||
if (permission_enum != nano::ipc::access_permission::invalid)
|
||||
{
|
||||
subject_a.permissions.erase (permission_enum);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::access::clear ()
|
||||
{
|
||||
users.clear ();
|
||||
roles.clear ();
|
||||
|
||||
// Create default user. The node operator can add additional roles
|
||||
// and permissions to the default user by adding a toml [[user]] entry
|
||||
// without an id (or set it to the empty string).
|
||||
// The default permissions can be overriden by marking the default user
|
||||
// as bare, and then set specific permissions.
|
||||
default_user.clear ();
|
||||
default_user.id = "";
|
||||
|
||||
// The default set of permissions. A new insert should be made as new safe
|
||||
// api's or resource permissions are made.
|
||||
default_user.permissions.insert (nano::ipc::access_permission::api_account_weight);
|
||||
}
|
||||
|
||||
nano::error nano::ipc::access::deserialize_toml (nano::tomlconfig & toml)
|
||||
{
|
||||
nano::unique_lock<std::mutex> lock (mutex);
|
||||
clear ();
|
||||
|
||||
nano::error error;
|
||||
if (toml.has_key ("role"))
|
||||
{
|
||||
auto get_role = [this](std::shared_ptr<cpptoml::table> const & role_a) {
|
||||
nano::ipc::access_role role;
|
||||
std::string id_l (role_a->get_as<std::string> ("id").value_or (""));
|
||||
role.id = id_l;
|
||||
set_effective_permissions (role, role_a);
|
||||
return role;
|
||||
};
|
||||
|
||||
auto role_l = toml.get_tree ()->get ("role");
|
||||
if (role_l->is_table ())
|
||||
{
|
||||
auto role = get_role (role_l->as_table ());
|
||||
if (role_l->as_table ()->contains ("deny"))
|
||||
{
|
||||
error.set ("Only users can have deny entries");
|
||||
}
|
||||
else
|
||||
{
|
||||
roles.emplace (role.id, role);
|
||||
}
|
||||
}
|
||||
else if (role_l->is_table_array ())
|
||||
{
|
||||
for (auto & table : *role_l->as_table_array ())
|
||||
{
|
||||
if (table->contains ("deny"))
|
||||
{
|
||||
error.set ("Only users can have deny entries");
|
||||
}
|
||||
|
||||
auto role = get_role (table);
|
||||
roles.emplace (role.id, role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!error && toml.has_key ("user"))
|
||||
{
|
||||
auto get_user = [this, &error](std::shared_ptr<cpptoml::table> const & user_a) {
|
||||
nano::ipc::access_user user;
|
||||
user.id = user_a->get_as<std::string> ("id").value_or ("");
|
||||
// Check bare flag. The tomlconfig parser stringifies values, so we must retrieve as string.
|
||||
bool is_bare = user_a->get_as<std::string> ("bare").value_or ("false") == "true";
|
||||
|
||||
// Adopt all permissions from the roles. This must be done before setting user permissions, since
|
||||
// the user config may add deny-entries.
|
||||
std::string roles_l (user_a->get_as<std::string> ("roles").value_or (""));
|
||||
std::vector<std::string> role_strings_l;
|
||||
boost::split (role_strings_l, roles_l, boost::is_any_of (","));
|
||||
for (auto const & role : role_strings_l)
|
||||
{
|
||||
auto role_id (boost::trim_copy (role));
|
||||
if (!role_id.empty ())
|
||||
{
|
||||
auto match = roles.find (role_id);
|
||||
if (match != roles.end ())
|
||||
{
|
||||
user.permissions.insert (match->second.permissions.begin (), match->second.permissions.end ());
|
||||
}
|
||||
else
|
||||
{
|
||||
error.set ("Unknown role: " + role_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A user with the bare flag does not inherit default permissions
|
||||
if (!is_bare)
|
||||
{
|
||||
user.permissions.insert (default_user.permissions.begin (), default_user.permissions.end ());
|
||||
}
|
||||
|
||||
set_effective_permissions (user, user_a);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
auto user_l = toml.get_tree ()->get ("user");
|
||||
if (user_l->is_table ())
|
||||
{
|
||||
auto user = get_user (user_l->as_table ());
|
||||
users.emplace (user.id, user);
|
||||
}
|
||||
else if (user_l->is_table_array ())
|
||||
{
|
||||
for (auto & table : *user_l->as_table_array ())
|
||||
{
|
||||
auto user = get_user (table);
|
||||
if (user.id.empty () && users.size () > 0)
|
||||
{
|
||||
// This is a requirement because other users inherit permissions from the default user
|
||||
error.set ("Changes to the default user must appear before other users in the access config file");
|
||||
break;
|
||||
}
|
||||
users.emplace (user.id, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add default user if it wasn't present in the config file
|
||||
if (users.find ("") == users.end ())
|
||||
{
|
||||
users.emplace (default_user.id, default_user);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
bool nano::ipc::access::has_access (std::string const & credentials_a, nano::ipc::access_permission permssion_a) const
|
||||
{
|
||||
nano::unique_lock<std::mutex> lock (mutex);
|
||||
bool permitted = false;
|
||||
auto user = users.find (credentials_a);
|
||||
if (user != users.end ())
|
||||
{
|
||||
permitted = user->second.permissions.find (permssion_a) != user->second.permissions.end ();
|
||||
if (!permitted)
|
||||
{
|
||||
permitted = user->second.permissions.find (nano::ipc::access_permission::unrestricted) != user->second.permissions.end ();
|
||||
}
|
||||
}
|
||||
return permitted;
|
||||
}
|
||||
|
||||
bool nano::ipc::access::has_access_to_all (std::string const & credentials_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const
|
||||
{
|
||||
nano::unique_lock<std::mutex> lock (mutex);
|
||||
bool permitted = false;
|
||||
auto user = users.find (credentials_a);
|
||||
if (user != users.end ())
|
||||
{
|
||||
for (auto permission : permissions_a)
|
||||
{
|
||||
permitted = user->second.permissions.find (permission) != user->second.permissions.end ();
|
||||
if (!permitted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return permitted;
|
||||
}
|
||||
|
||||
bool nano::ipc::access::has_access_to_oneof (std::string const & credentials_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const
|
||||
{
|
||||
nano::unique_lock<std::mutex> lock (mutex);
|
||||
bool permitted = false;
|
||||
auto user = users.find (credentials_a);
|
||||
if (user != users.end ())
|
||||
{
|
||||
for (auto permission : permissions_a)
|
||||
{
|
||||
permitted = user->second.permissions.find (permission) != user->second.permissions.end ();
|
||||
if (permitted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!permitted)
|
||||
{
|
||||
permitted = user->second.permissions.find (nano::ipc::access_permission::unrestricted) != user->second.permissions.end ();
|
||||
}
|
||||
}
|
||||
return permitted;
|
||||
}
|
||||
|
||||
void nano::ipc::access_subject::clear ()
|
||||
{
|
||||
permissions.clear ();
|
||||
}
|
||||
|
||||
void nano::ipc::access_user::clear ()
|
||||
{
|
||||
access_subject::clear ();
|
||||
roles.clear ();
|
||||
}
|
||||
|
||||
namespace nano
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
nano::error read_access_config_toml (boost::filesystem::path const & data_path_a, nano::ipc::access & config_a)
|
||||
{
|
||||
nano::error error;
|
||||
auto toml_config_path = nano::get_access_toml_config_path (data_path_a);
|
||||
|
||||
nano::tomlconfig toml;
|
||||
if (boost::filesystem::exists (toml_config_path))
|
||||
{
|
||||
error = toml.read (toml_config_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::stringstream config_overrides_stream;
|
||||
config_overrides_stream << std::endl;
|
||||
toml.read (config_overrides_stream);
|
||||
}
|
||||
|
||||
if (!error)
|
||||
{
|
||||
error = config_a.deserialize_toml (toml);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
133
nano/node/ipc/ipc_access_config.hpp
Normal file
133
nano/node/ipc/ipc_access_config.hpp
Normal file
|
@ -0,0 +1,133 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/lib/config.hpp>
|
||||
#include <nano/lib/errors.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace boost
|
||||
{
|
||||
namespace filesystem
|
||||
{
|
||||
class path;
|
||||
}
|
||||
}
|
||||
namespace cpptoml
|
||||
{
|
||||
class table;
|
||||
}
|
||||
|
||||
namespace nano
|
||||
{
|
||||
class tomlconfig;
|
||||
namespace ipc
|
||||
{
|
||||
struct enum_hash
|
||||
{
|
||||
template <typename T>
|
||||
constexpr typename std::enable_if<std::is_enum<T>::value, std::size_t>::type
|
||||
operator() (T s) const noexcept
|
||||
{
|
||||
return static_cast<std::size_t> (s);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Permissions come in roughly two forms: api permissions (one for every api we expose) and
|
||||
* higher level resource permissions. We define a permission per api because a common use case is to
|
||||
* allow a specific set of RPCs. The higher level resource permissions makes it easier to
|
||||
* grant access to groups of operations or resources. An API implementation will typically check
|
||||
* against the corresponding api permission (such as api_account_weight), but may also allow
|
||||
* resource permissions (such as account_query).
|
||||
*/
|
||||
enum class access_permission
|
||||
{
|
||||
invalid,
|
||||
/** Unrestricted access to the node, suitable for debugging and development */
|
||||
unrestricted,
|
||||
api_account_weight,
|
||||
api_service_register,
|
||||
api_service_stop,
|
||||
api_topic_service_stop,
|
||||
api_topic_confirmation,
|
||||
/** Query account information */
|
||||
account_query,
|
||||
/** Epoch upgrade */
|
||||
epoch_upgrade,
|
||||
/** All service operations */
|
||||
service,
|
||||
/** All wallet operations */
|
||||
wallet,
|
||||
/** Non-mutable wallet operations */
|
||||
wallet_read,
|
||||
/** Mutable wallet operations */
|
||||
wallet_write,
|
||||
/** Seed change */
|
||||
wallet_seed_change
|
||||
};
|
||||
|
||||
/** A subject is a user or role with a set of permissions */
|
||||
class access_subject
|
||||
{
|
||||
public:
|
||||
std::unordered_set<nano::ipc::access_permission, enum_hash> permissions;
|
||||
virtual ~access_subject () = default;
|
||||
virtual void clear ();
|
||||
};
|
||||
|
||||
/** Permissions can be organized into roles */
|
||||
class access_role final : public access_subject
|
||||
{
|
||||
public:
|
||||
std::string id;
|
||||
};
|
||||
|
||||
/** A user with credentials and a set of permissions (either directly or through roles) */
|
||||
class access_user final : public access_subject
|
||||
{
|
||||
public:
|
||||
/* User credentials, serving as the id */
|
||||
std::string id;
|
||||
std::vector<nano::ipc::access_role> roles;
|
||||
void clear () override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs a user/role/permission domain model from config-access.toml, and
|
||||
* allows permissions for a user to be checked.
|
||||
* @note This class is thread safe
|
||||
*/
|
||||
class access final
|
||||
{
|
||||
public:
|
||||
bool has_access (std::string const & credentials_a, nano::ipc::access_permission permission_a) const;
|
||||
bool has_access_to_all (std::string const & credentials_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const;
|
||||
bool has_access_to_oneof (std::string const & credentials_a, std::initializer_list<nano::ipc::access_permission> permissions_a) const;
|
||||
nano::error deserialize_toml (nano::tomlconfig &);
|
||||
|
||||
private:
|
||||
/** Process allow and deny entries for the given subject */
|
||||
void set_effective_permissions (nano::ipc::access_subject & subject_a, std::shared_ptr<cpptoml::table> const & config_subject_a);
|
||||
|
||||
/** Clear current users, roles and default permissions */
|
||||
void clear ();
|
||||
|
||||
std::unordered_map<std::string, nano::ipc::access_user> users;
|
||||
std::unordered_map<std::string, nano::ipc::access_role> roles;
|
||||
|
||||
/**
|
||||
* Default user with a basic set of permissions. Additional users will derive the permissions
|
||||
* from the default user (unless "bare" is true in the access config file)
|
||||
*/
|
||||
access_user default_user;
|
||||
/** The config can be externally reloaded and concurrently accessed */
|
||||
mutable std::mutex mutex;
|
||||
};
|
||||
|
||||
nano::error read_access_config_toml (boost::filesystem::path const & data_path_a, nano::ipc::access & config_a);
|
||||
}
|
||||
}
|
259
nano/node/ipc/ipc_broker.cpp
Normal file
259
nano/node/ipc/ipc_broker.cpp
Normal file
|
@ -0,0 +1,259 @@
|
|||
#include <nano/node/ipc/action_handler.hpp>
|
||||
#include <nano/node/ipc/flatbuffers_handler.hpp>
|
||||
#include <nano/node/ipc/flatbuffers_util.hpp>
|
||||
#include <nano/node/ipc/ipc_broker.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/node.hpp>
|
||||
|
||||
nano::ipc::broker::broker (nano::node & node_a) :
|
||||
node (node_a)
|
||||
{
|
||||
}
|
||||
|
||||
std::shared_ptr<flatbuffers::Parser> nano::ipc::subscriber::get_parser (nano::ipc::ipc_config const & ipc_config_a)
|
||||
{
|
||||
if (!parser)
|
||||
{
|
||||
parser = nano::ipc::flatbuffers_handler::make_flatbuffers_parser (ipc_config_a);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
|
||||
void nano::ipc::broker::start ()
|
||||
{
|
||||
node.observers.blocks.add ([this](nano::election_status const & status_a, nano::account const & account_a, nano::amount const & amount_a, bool is_state_send_a) {
|
||||
assert (status_a.type != nano::election_status_type::ongoing);
|
||||
|
||||
try
|
||||
{
|
||||
// The subscriber(s) may be gone after the count check, but the only consequence
|
||||
// is that broadcast is called only to not find any live sessions.
|
||||
if (confirmation_subscriber_count () > 0)
|
||||
{
|
||||
auto confirmation (std::make_shared<nanoapi::EventConfirmationT> ());
|
||||
|
||||
confirmation->account = account_a.to_account ();
|
||||
confirmation->amount = amount_a.to_string_dec ();
|
||||
switch (status_a.type)
|
||||
{
|
||||
case nano::election_status_type::active_confirmed_quorum:
|
||||
confirmation->confirmation_type = nanoapi::TopicConfirmationType::TopicConfirmationType_active_quorum;
|
||||
break;
|
||||
case nano::election_status_type::active_confirmation_height:
|
||||
confirmation->confirmation_type = nanoapi::TopicConfirmationType::TopicConfirmationType_active_confirmation_height;
|
||||
break;
|
||||
case nano::election_status_type::inactive_confirmation_height:
|
||||
confirmation->confirmation_type = nanoapi::TopicConfirmationType::TopicConfirmationType_inactive;
|
||||
break;
|
||||
default:
|
||||
assert (false);
|
||||
break;
|
||||
};
|
||||
confirmation->confirmation_type = nanoapi::TopicConfirmationType::TopicConfirmationType_active_quorum;
|
||||
confirmation->block = nano::ipc::flatbuffers_builder::block_to_union (*status_a.winner, amount_a, is_state_send_a);
|
||||
confirmation->election_info = std::make_unique<nanoapi::ElectionInfoT> ();
|
||||
confirmation->election_info->duration = status_a.election_duration.count ();
|
||||
confirmation->election_info->time = status_a.election_end.count ();
|
||||
confirmation->election_info->tally = status_a.tally.to_string_dec ();
|
||||
confirmation->election_info->block_count = status_a.block_count;
|
||||
confirmation->election_info->voter_count = status_a.voter_count;
|
||||
confirmation->election_info->request_count = status_a.confirmation_request_count;
|
||||
|
||||
broadcast (confirmation);
|
||||
}
|
||||
}
|
||||
catch (nano::error const & err)
|
||||
{
|
||||
this->node.logger.always_log ("IPC: could not broadcast message: ", err.get_message ());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
template <typename COLL, typename TOPIC_TYPE>
|
||||
void subscribe_or_unsubscribe (nano::logger_mt & logger, COLL & subscriber_collection, std::weak_ptr<nano::ipc::subscriber> const & subscriber_a, TOPIC_TYPE topic_a)
|
||||
{
|
||||
// Evict subscribers from dead sessions. Also remove current subscriber if unsubscribing.
|
||||
subscriber_collection.erase (std::remove_if (subscriber_collection.begin (), subscriber_collection.end (),
|
||||
[& logger = logger, topic_a, subscriber_a](auto & sub) {
|
||||
bool remove = false;
|
||||
auto subscriber_l = sub.subscriber.lock ();
|
||||
if (subscriber_l)
|
||||
{
|
||||
if (auto calling_subscriber_l = subscriber_a.lock ())
|
||||
{
|
||||
remove = topic_a->unsubscribe && subscriber_l->get_id () == calling_subscriber_l->get_id ();
|
||||
if (remove)
|
||||
{
|
||||
logger.always_log ("IPC: unsubscription from subscriber #", calling_subscriber_l->get_id ());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
remove = true;
|
||||
}
|
||||
return remove;
|
||||
}),
|
||||
subscriber_collection.end ());
|
||||
|
||||
if (!topic_a->unsubscribe)
|
||||
{
|
||||
subscriber_collection.emplace_back (subscriber_a, topic_a);
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::broker::subscribe (std::weak_ptr<nano::ipc::subscriber> const & subscriber_a, std::shared_ptr<nanoapi::TopicConfirmationT> const & confirmation_a)
|
||||
{
|
||||
auto subscribers = confirmation_subscribers.lock ();
|
||||
subscribe_or_unsubscribe (node.logger, subscribers.get (), subscriber_a, confirmation_a);
|
||||
}
|
||||
|
||||
void nano::ipc::broker::broadcast (std::shared_ptr<nanoapi::EventConfirmationT> const & confirmation_a)
|
||||
{
|
||||
using Filter = nanoapi::TopicConfirmationTypeFilter;
|
||||
decltype (confirmation_a->election_info) election_info;
|
||||
nanoapi::BlockUnion block;
|
||||
auto itr (confirmation_subscribers->begin ());
|
||||
while (itr != confirmation_subscribers->end ())
|
||||
{
|
||||
if (auto subscriber_l = itr->subscriber.lock ())
|
||||
{
|
||||
auto should_filter = [this, &itr, confirmation_a]() {
|
||||
assert (itr->topic->options != nullptr);
|
||||
auto conf_filter (itr->topic->options->confirmation_type_filter);
|
||||
|
||||
bool should_filter_conf_type_l (true);
|
||||
bool all_filter = conf_filter == Filter::TopicConfirmationTypeFilter_all;
|
||||
bool inactive_filter = conf_filter == Filter::TopicConfirmationTypeFilter_inactive;
|
||||
bool active_filter = conf_filter == Filter::TopicConfirmationTypeFilter_active || conf_filter == Filter::TopicConfirmationTypeFilter_active_quorum || conf_filter == Filter::TopicConfirmationTypeFilter_active_confirmation_height;
|
||||
|
||||
if ((confirmation_a->confirmation_type == nanoapi::TopicConfirmationType::TopicConfirmationType_active_quorum || confirmation_a->confirmation_type == nanoapi::TopicConfirmationType::TopicConfirmationType_active_confirmation_height) && (all_filter || active_filter))
|
||||
{
|
||||
should_filter_conf_type_l = false;
|
||||
}
|
||||
else if (confirmation_a->confirmation_type == nanoapi::TopicConfirmationType::TopicConfirmationType_inactive && (all_filter || inactive_filter))
|
||||
{
|
||||
should_filter_conf_type_l = false;
|
||||
}
|
||||
|
||||
bool should_filter_account_l (itr->topic->options->all_local_accounts || !itr->topic->options->accounts.empty ());
|
||||
auto state (confirmation_a->block.AsBlockState ());
|
||||
if (state && !should_filter_conf_type_l)
|
||||
{
|
||||
if (itr->topic->options->all_local_accounts)
|
||||
{
|
||||
auto transaction_l (this->node.wallets.tx_begin_read ());
|
||||
nano::account source_l (0), destination_l (0);
|
||||
auto decode_source_ok_l (!source_l.decode_account (state->account));
|
||||
auto decode_destination_ok_l (!destination_l.decode_account (state->link_as_account));
|
||||
(void)decode_source_ok_l;
|
||||
(void)decode_destination_ok_l;
|
||||
assert (decode_source_ok_l && decode_destination_ok_l);
|
||||
if (this->node.wallets.exists (transaction_l, source_l) || this->node.wallets.exists (transaction_l, destination_l))
|
||||
{
|
||||
should_filter_account_l = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (std::find (itr->topic->options->accounts.begin (), itr->topic->options->accounts.end (), state->account) != itr->topic->options->accounts.end () || std::find (itr->topic->options->accounts.begin (), itr->topic->options->accounts.end (), state->link_as_account) != itr->topic->options->accounts.end ())
|
||||
{
|
||||
should_filter_account_l = false;
|
||||
}
|
||||
}
|
||||
|
||||
return should_filter_conf_type_l || should_filter_account_l;
|
||||
};
|
||||
// Apply any filters
|
||||
auto & options (itr->topic->options);
|
||||
if (options)
|
||||
{
|
||||
if (!options->include_election_info)
|
||||
{
|
||||
election_info = std::move (confirmation_a->election_info);
|
||||
confirmation_a->election_info = nullptr;
|
||||
}
|
||||
if (!options->include_block)
|
||||
{
|
||||
block = confirmation_a->block;
|
||||
confirmation_a->block.Reset ();
|
||||
}
|
||||
}
|
||||
if (!options || !should_filter ())
|
||||
{
|
||||
auto fb (nano::ipc::flatbuffer_producer::make_buffer (*confirmation_a));
|
||||
|
||||
if (subscriber_l->get_active_encoding () == nano::ipc::payload_encoding::flatbuffers_json)
|
||||
{
|
||||
auto parser (subscriber_l->get_parser (node.config.ipc_config));
|
||||
|
||||
// Convert response to JSON
|
||||
auto json (std::make_shared<std::string> ());
|
||||
if (!flatbuffers::GenerateText (*parser, fb->GetBufferPointer (), json.get ()))
|
||||
{
|
||||
throw nano::error ("Couldn't serialize response to JSON");
|
||||
}
|
||||
|
||||
subscriber_l->async_send_message (reinterpret_cast<uint8_t const *> (json->data ()), json->size (), [json](const nano::error & err) {});
|
||||
}
|
||||
else
|
||||
{
|
||||
subscriber_l->async_send_message (fb->GetBufferPointer (), fb->GetSize (), [fb](const nano::error & err) {});
|
||||
}
|
||||
}
|
||||
|
||||
// Restore full object, the next subscriber may request it
|
||||
if (election_info)
|
||||
{
|
||||
confirmation_a->election_info = std::move (election_info);
|
||||
}
|
||||
if (block.type != nanoapi::Block::Block_NONE)
|
||||
{
|
||||
confirmation_a->block = block;
|
||||
}
|
||||
|
||||
++itr;
|
||||
}
|
||||
else
|
||||
{
|
||||
itr = confirmation_subscribers->erase (itr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t nano::ipc::broker::confirmation_subscriber_count () const
|
||||
{
|
||||
return confirmation_subscribers->size ();
|
||||
}
|
||||
|
||||
void nano::ipc::broker::service_register (std::string const & service_name_a, std::weak_ptr<nano::ipc::subscriber> const & subscriber_a)
|
||||
{
|
||||
if (auto subscriber_l = subscriber_a.lock ())
|
||||
{
|
||||
subscriber_l->set_service_name (service_name_a);
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::broker::service_stop (std::string const & service_name_a)
|
||||
{
|
||||
auto subscribers = service_stop_subscribers.lock ();
|
||||
for (auto & subcription : subscribers.get ())
|
||||
{
|
||||
if (auto subscriber_l = subcription.subscriber.lock ())
|
||||
{
|
||||
if (subscriber_l->get_service_name () == service_name_a)
|
||||
{
|
||||
nanoapi::EventServiceStopT event_stop;
|
||||
auto fb (nano::ipc::flatbuffer_producer::make_buffer (event_stop));
|
||||
subscriber_l->async_send_message (fb->GetBufferPointer (), fb->GetSize (), [fb](const nano::error & err) {});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void nano::ipc::broker::subscribe (std::weak_ptr<nano::ipc::subscriber> const & subscriber_a, std::shared_ptr<nanoapi::TopicServiceStopT> const & service_stop_a)
|
||||
{
|
||||
auto subscribers = service_stop_subscribers.lock ();
|
||||
subscribe_or_unsubscribe (node.logger, subscribers.get (), subscriber_a, service_stop_a);
|
||||
}
|
104
nano/node/ipc/ipc_broker.hpp
Normal file
104
nano/node/ipc/ipc_broker.hpp
Normal file
|
@ -0,0 +1,104 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h>
|
||||
#include <nano/lib/ipc.hpp>
|
||||
#include <nano/lib/locks.hpp>
|
||||
#include <nano/node/ipc/ipc_broker.hpp>
|
||||
#include <nano/node/node_rpc_config.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
namespace flatbuffers
|
||||
{
|
||||
class Parser;
|
||||
}
|
||||
namespace nano
|
||||
{
|
||||
class node;
|
||||
class error;
|
||||
namespace ipc
|
||||
{
|
||||
class ipc_config;
|
||||
/**
|
||||
* A subscriber represents a live session, and is weakly referenced nano::ipc::subscription whenever a subscription is made.
|
||||
* This construction helps making the session implementation opaque to clients.
|
||||
*/
|
||||
class subscriber
|
||||
{
|
||||
public:
|
||||
virtual ~subscriber () = default;
|
||||
|
||||
/**
|
||||
* Send message payload to the client. The implementation will prepend the bigendian length.
|
||||
* @param data_a The caller must ensure the lifetime is extended until the completion handler is called, such as through a lambda capture.
|
||||
* @param length_a Length of payload message in bytes
|
||||
* @param broadcast_completion_handler_a Called once sending is completed
|
||||
*/
|
||||
virtual void async_send_message (uint8_t const * data_a, size_t length_a, std::function<void(nano::error const &)> broadcast_completion_handler_a) = 0;
|
||||
/** Returns the unique id of the associated session */
|
||||
virtual uint64_t get_id () const = 0;
|
||||
/** Returns the service name associated with the session */
|
||||
virtual std::string get_service_name () const = 0;
|
||||
/** Sets the service name associated with the session */
|
||||
virtual void set_service_name (std::string const & service_name_a) = 0;
|
||||
/** Returns the session's active payload encoding */
|
||||
virtual nano::ipc::payload_encoding get_active_encoding () const = 0;
|
||||
|
||||
/** Get flatbuffer parser instance for this subscriber; create it if necessary */
|
||||
std::shared_ptr<flatbuffers::Parser> get_parser (nano::ipc::ipc_config const & ipc_config_a);
|
||||
|
||||
private:
|
||||
std::shared_ptr<flatbuffers::Parser> parser;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscriptions are added to the broker whenever a topic message is sent from a client.
|
||||
* The subscription is removed when the client unsubscribes, or lazily removed after the
|
||||
* session is closed.
|
||||
*/
|
||||
template <typename TopicType>
|
||||
class subscription final
|
||||
{
|
||||
public:
|
||||
subscription (std::weak_ptr<nano::ipc::subscriber> const & subscriber_a, std::shared_ptr<TopicType> const & topic_a) :
|
||||
subscriber (subscriber_a), topic (topic_a)
|
||||
{
|
||||
}
|
||||
|
||||
std::weak_ptr<nano::ipc::subscriber> subscriber;
|
||||
std::shared_ptr<TopicType> topic;
|
||||
};
|
||||
|
||||
/**
|
||||
* The broker manages subscribers and performs message broadcasting
|
||||
* @note Add subscribe overloads for new topics
|
||||
*/
|
||||
class broker final
|
||||
{
|
||||
public:
|
||||
broker (nano::node & node_a);
|
||||
/** Starts the broker by setting up observers */
|
||||
void start ();
|
||||
/** Subscribe to block confirmations */
|
||||
void subscribe (std::weak_ptr<nano::ipc::subscriber> const & subscriber_a, std::shared_ptr<nanoapi::TopicConfirmationT> const & confirmation_a);
|
||||
/** Subscribe to EventServiceStop notifications for \p subscriber_a. The subscriber must first have called ServiceRegister. */
|
||||
void subscribe (std::weak_ptr<nano::ipc::subscriber> const & subscriber_a, std::shared_ptr<nanoapi::TopicServiceStopT> const & service_stop_a);
|
||||
|
||||
/** Returns the number of confirmation subscribers */
|
||||
size_t confirmation_subscriber_count () const;
|
||||
/** Associate the service name with the subscriber */
|
||||
void service_register (std::string const & service_name_a, std::weak_ptr<nano::ipc::subscriber> const & subscriber_a);
|
||||
/** Sends a notification to the session associated with the given service (if the session has subscribed to TopicServiceStop) */
|
||||
void service_stop (std::string const & service_name_a);
|
||||
|
||||
private:
|
||||
/** Broadcast block confirmations */
|
||||
void broadcast (std::shared_ptr<nanoapi::EventConfirmationT> const & confirmation_a);
|
||||
|
||||
nano::node & node;
|
||||
mutable nano::locked<std::vector<subscription<nanoapi::TopicConfirmationT>>> confirmation_subscribers;
|
||||
mutable nano::locked<std::vector<subscription<nanoapi::TopicServiceStopT>>> service_stop_subscribers;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
#include <nano/lib/jsonconfig.hpp>
|
||||
#include <nano/lib/tomlconfig.hpp>
|
||||
#include <nano/node/ipcconfig.hpp>
|
||||
#include <nano/node/ipc/ipc_config.hpp>
|
||||
|
||||
nano::error nano::ipc::ipc_config::serialize_toml (nano::tomlconfig & toml) const
|
||||
{
|
||||
|
@ -25,6 +25,12 @@ nano::error nano::ipc::ipc_config::serialize_toml (nano::tomlconfig & toml) cons
|
|||
domain_l.put ("path", transport_domain.path, "Path to the local domain socket.\ntype:string");
|
||||
domain_l.put ("io_timeout", transport_domain.io_timeout, "Timeout for requests.\ntype:seconds");
|
||||
toml.put_child ("local", domain_l);
|
||||
|
||||
nano::tomlconfig flatbuffers_l;
|
||||
flatbuffers_l.put ("skip_unexpected_fields_in_json", flatbuffers.skip_unexpected_fields_in_json, "Allow client to send unknown fields in json messages. These will be ignored.\ntype:bool");
|
||||
flatbuffers_l.put ("verify_buffers", flatbuffers.verify_buffers, "Verify that the buffer is valid before parsing. This is recommended when receiving data from untrusted sources.\ntype:bool");
|
||||
toml.put_child ("flatbuffers", flatbuffers_l);
|
||||
|
||||
return toml.get_error ();
|
||||
}
|
||||
|
||||
|
@ -50,6 +56,13 @@ nano::error nano::ipc::ipc_config::deserialize_toml (nano::tomlconfig & toml)
|
|||
domain_l->get<size_t> ("io_timeout", transport_domain.io_timeout);
|
||||
}
|
||||
|
||||
auto flatbuffers_l (toml.get_optional_child ("flatbuffers"));
|
||||
if (flatbuffers_l)
|
||||
{
|
||||
flatbuffers_l->get<bool> ("skip_unexpected_fields_in_json", flatbuffers.skip_unexpected_fields_in_json);
|
||||
flatbuffers_l->get<bool> ("verify_buffers", flatbuffers.verify_buffers);
|
||||
}
|
||||
|
||||
return toml.get_error ();
|
||||
}
|
||||
|
|
@ -22,6 +22,16 @@ namespace ipc
|
|||
long io_threads{ -1 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Flatbuffers encoding config. See TOML serialization calls for details about each field.
|
||||
*/
|
||||
class ipc_config_flatbuffers final
|
||||
{
|
||||
public:
|
||||
bool skip_unexpected_fields_in_json{ true };
|
||||
bool verify_buffers{ true };
|
||||
};
|
||||
|
||||
/** Domain socket specific transport config */
|
||||
class ipc_config_domain_socket : public ipc_config_transport
|
||||
{
|
||||
|
@ -61,6 +71,7 @@ namespace ipc
|
|||
nano::error serialize_toml (nano::tomlconfig & toml) const;
|
||||
ipc_config_domain_socket transport_domain;
|
||||
ipc_config_tcp_socket transport_tcp;
|
||||
ipc_config_flatbuffers flatbuffers;
|
||||
};
|
||||
}
|
||||
}
|
659
nano/node/ipc/ipc_server.cpp
Normal file
659
nano/node/ipc/ipc_server.cpp
Normal file
|
@ -0,0 +1,659 @@
|
|||
#include <nano/boost/asio/bind_executor.hpp>
|
||||
#include <nano/boost/asio/local/stream_protocol.hpp>
|
||||
#include <nano/boost/asio/read.hpp>
|
||||
#include <nano/boost/asio/strand.hpp>
|
||||
#include <nano/lib/config.hpp>
|
||||
#include <nano/lib/ipc.hpp>
|
||||
#include <nano/lib/locks.hpp>
|
||||
#include <nano/lib/threading.hpp>
|
||||
#include <nano/lib/timer.hpp>
|
||||
#include <nano/node/common.hpp>
|
||||
#include <nano/node/ipc/action_handler.hpp>
|
||||
#include <nano/node/ipc/flatbuffers_handler.hpp>
|
||||
#include <nano/node/ipc/flatbuffers_util.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/json_handler.hpp>
|
||||
#include <nano/node/node.hpp>
|
||||
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/endian/conversion.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <list>
|
||||
|
||||
#include <flatbuffers/flatbuffers.h>
|
||||
|
||||
using namespace boost::log;
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* A session manages an inbound connection over which messages are exchanged.
|
||||
*/
|
||||
template <typename SOCKET_TYPE>
|
||||
class session final : public nano::ipc::socket_base, public std::enable_shared_from_this<session<SOCKET_TYPE>>
|
||||
{
|
||||
public:
|
||||
session (nano::ipc::ipc_server & server_a, boost::asio::io_context & io_ctx_a, nano::ipc::ipc_config_transport & config_transport_a) :
|
||||
socket_base (io_ctx_a),
|
||||
server (server_a), node (server_a.node), session_id (server_a.id_dispenser.fetch_add (1)),
|
||||
io_ctx (io_ctx_a), strand (io_ctx_a.get_executor ()), socket (io_ctx_a), config_transport (config_transport_a)
|
||||
{
|
||||
if (node.config.logging.log_ipc ())
|
||||
{
|
||||
node.logger.always_log ("IPC: created session with id: ", session_id.load ());
|
||||
}
|
||||
}
|
||||
|
||||
~session ()
|
||||
{
|
||||
close ();
|
||||
}
|
||||
|
||||
SOCKET_TYPE & get_socket ()
|
||||
{
|
||||
return socket;
|
||||
}
|
||||
|
||||
std::shared_ptr<nano::ipc::subscriber> get_subscriber ()
|
||||
{
|
||||
class subscriber_impl final : public nano::ipc::subscriber, public std::enable_shared_from_this<subscriber_impl>
|
||||
{
|
||||
public:
|
||||
subscriber_impl (std::shared_ptr<session> const & session_a) :
|
||||
session_m (session_a)
|
||||
{
|
||||
}
|
||||
virtual void async_send_message (uint8_t const * data_a, size_t length_a, std::function<void(nano::error const &)> broadcast_completion_handler_a) override
|
||||
{
|
||||
if (auto session_l = session_m.lock ())
|
||||
{
|
||||
auto big_endian_length = std::make_shared<uint32_t> (boost::endian::native_to_big (static_cast<uint32_t> (length_a)));
|
||||
boost::array<boost::asio::const_buffer, 2> buffers = {
|
||||
boost::asio::buffer (big_endian_length.get (), sizeof (std::uint32_t)),
|
||||
boost::asio::buffer (data_a, length_a)
|
||||
};
|
||||
|
||||
session_l->queued_write (buffers, [broadcast_completion_handler_a, big_endian_length](boost::system::error_code const & ec_a, size_t size_a) {
|
||||
if (broadcast_completion_handler_a)
|
||||
{
|
||||
nano::error error_l (ec_a);
|
||||
broadcast_completion_handler_a (error_l);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t get_id () const override
|
||||
{
|
||||
uint64_t id{ 0 };
|
||||
if (auto session_l = session_m.lock ())
|
||||
{
|
||||
id = session_l->session_id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
std::string get_service_name () const override
|
||||
{
|
||||
std::string name{ 0 };
|
||||
if (auto session_l = session_m.lock ())
|
||||
{
|
||||
name = session_l->service_name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
void set_service_name (std::string const & service_name_a) override
|
||||
{
|
||||
if (auto session_l = session_m.lock ())
|
||||
{
|
||||
session_l->service_name = service_name_a;
|
||||
}
|
||||
}
|
||||
|
||||
nano::ipc::payload_encoding get_active_encoding () const override
|
||||
{
|
||||
nano::ipc::payload_encoding encoding{ nano::ipc::payload_encoding::flatbuffers };
|
||||
if (auto session_l = session_m.lock ())
|
||||
{
|
||||
encoding = session_l->active_encoding;
|
||||
}
|
||||
return encoding;
|
||||
}
|
||||
|
||||
private:
|
||||
std::weak_ptr<session> session_m;
|
||||
};
|
||||
|
||||
static std::mutex subscriber_mutex;
|
||||
nano::unique_lock<std::mutex> lock (subscriber_mutex);
|
||||
|
||||
if (!subscriber)
|
||||
{
|
||||
subscriber = std::make_shared<subscriber_impl> (this->shared_from_this ());
|
||||
}
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
/** Write a fixed array of buffers through the queue. Once the last item is completed, the callback is invoked */
|
||||
template <std::size_t N>
|
||||
void queued_write (boost::array<boost::asio::const_buffer, N> & buffers, std::function<void(boost::system::error_code const &, size_t)> callback_a)
|
||||
{
|
||||
auto this_l (this->shared_from_this ());
|
||||
boost::asio::post (strand, boost::asio::bind_executor (strand, [buffers, callback_a, this_l]() {
|
||||
bool write_in_progress = !this_l->send_queue.empty ();
|
||||
auto queue_size = this_l->send_queue.size ();
|
||||
if (queue_size < this_l->queue_size_max)
|
||||
{
|
||||
for (size_t i = 0; i < N - 1; i++)
|
||||
{
|
||||
this_l->send_queue.emplace_back (queue_item{ buffers[i], nullptr });
|
||||
}
|
||||
this_l->send_queue.emplace_back (queue_item{ buffers[N - 1], callback_a });
|
||||
}
|
||||
if (!write_in_progress)
|
||||
{
|
||||
this_l->write_queued_messages ();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to underlying socket. Writes goes through a queue protected by the strand. Thus, this function
|
||||
* can be called concurrently with other writes.
|
||||
* @note This function explicitely doesn't use nano::shared_const_buffer, as buffers usually originate from Flatbuffers
|
||||
* and copying into the shared_const_buffer vector would impose a significant overhead for large requests and responses.
|
||||
*/
|
||||
void queued_write (boost::asio::const_buffer const & buffer_a, std::function<void(boost::system::error_code const &, size_t)> callback_a)
|
||||
{
|
||||
auto this_l (this->shared_from_this ());
|
||||
boost::asio::post (strand, boost::asio::bind_executor (strand, [buffer_a, callback_a, this_l]() {
|
||||
bool write_in_progress = !this_l->send_queue.empty ();
|
||||
auto queue_size = this_l->send_queue.size ();
|
||||
if (queue_size < this_l->queue_size_max)
|
||||
{
|
||||
this_l->send_queue.emplace_back (queue_item{ buffer_a, callback_a });
|
||||
}
|
||||
if (!write_in_progress)
|
||||
{
|
||||
this_l->write_queued_messages ();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void write_queued_messages ()
|
||||
{
|
||||
std::weak_ptr<session> this_w (this->shared_from_this ());
|
||||
auto msg (send_queue.front ());
|
||||
timer_start (std::chrono::seconds (config_transport.io_timeout));
|
||||
nano::unsafe_async_write (socket, msg.buffer,
|
||||
boost::asio::bind_executor (strand,
|
||||
[msg, this_w](boost::system::error_code ec, std::size_t size_a) {
|
||||
if (auto this_l = this_w.lock ())
|
||||
{
|
||||
this_l->timer_cancel ();
|
||||
|
||||
if (msg.callback)
|
||||
{
|
||||
msg.callback (ec, size_a);
|
||||
}
|
||||
|
||||
this_l->send_queue.pop_front ();
|
||||
if (!ec && !this_l->send_queue.empty ())
|
||||
{
|
||||
this_l->write_queued_messages ();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Async read of exactly \p size_a bytes. The callback is invoked only when all the data is available and
|
||||
* no error has occurred. On error, the error is logged, the read cycle stops and the session ends. Clients
|
||||
* are expected to implement reconnect logic.
|
||||
*/
|
||||
void async_read_exactly (void * buff_a, size_t size_a, std::function<void()> const & callback_a)
|
||||
{
|
||||
async_read_exactly (buff_a, size_a, std::chrono::seconds (config_transport.io_timeout), callback_a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async read of exactly \p size_a bytes and a specific \p timeout_a.
|
||||
* @see async_read_exactly (void *, size_t, std::function<void()>)
|
||||
*/
|
||||
void async_read_exactly (void * buff_a, size_t size_a, std::chrono::seconds timeout_a, std::function<void()> const & callback_a)
|
||||
{
|
||||
timer_start (timeout_a);
|
||||
auto this_l (this->shared_from_this ());
|
||||
boost::asio::async_read (socket,
|
||||
boost::asio::buffer (buff_a, size_a),
|
||||
boost::asio::transfer_exactly (size_a),
|
||||
boost::asio::bind_executor (strand,
|
||||
[this_l, callback_a](boost::system::error_code const & ec, size_t bytes_transferred_a) {
|
||||
this_l->timer_cancel ();
|
||||
if (ec == boost::asio::error::broken_pipe || ec == boost::asio::error::connection_aborted || ec == boost::asio::error::connection_reset || ec == boost::asio::error::connection_refused)
|
||||
{
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log (boost::str (boost::format ("IPC: error reading %1% ") % ec.message ()));
|
||||
}
|
||||
}
|
||||
else if (bytes_transferred_a > 0)
|
||||
{
|
||||
callback_a ();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/** Handler for payload_encoding::json_legacy */
|
||||
void handle_json_query (bool allow_unsafe)
|
||||
{
|
||||
session_timer.restart ();
|
||||
auto request_id_l (std::to_string (server.id_dispenser.fetch_add (1)));
|
||||
|
||||
// This is called when nano::rpc_handler#process_request is done. We convert to
|
||||
// json and write the response to the ipc socket with a length prefix.
|
||||
auto this_l (this->shared_from_this ());
|
||||
auto response_handler_l ([this_l, request_id_l](std::string const & body) {
|
||||
auto big = boost::endian::native_to_big (static_cast<uint32_t> (body.size ()));
|
||||
auto buffer (std::make_shared<std::vector<uint8_t>> ());
|
||||
buffer->insert (buffer->end (), reinterpret_cast<std::uint8_t *> (&big), reinterpret_cast<std::uint8_t *> (&big) + sizeof (std::uint32_t));
|
||||
buffer->insert (buffer->end (), body.begin (), body.end ());
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log (boost::str (boost::format ("IPC/RPC request %1% completed in: %2% %3%") % request_id_l % this_l->session_timer.stop ().count () % this_l->session_timer.unit ()));
|
||||
}
|
||||
|
||||
this_l->timer_start (std::chrono::seconds (this_l->config_transport.io_timeout));
|
||||
this_l->queued_write (boost::asio::buffer (buffer->data (), buffer->size ()), [this_l, buffer](boost::system::error_code const & error_a, size_t size_a) {
|
||||
this_l->timer_cancel ();
|
||||
if (!error_a)
|
||||
{
|
||||
this_l->read_next_request ();
|
||||
}
|
||||
else if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Write failed: ", error_a.message ());
|
||||
}
|
||||
});
|
||||
|
||||
// Do not call any member variables here (like session_timer) as it's possible that the next request may already be underway.
|
||||
});
|
||||
|
||||
node.stats.inc (nano::stat::type::ipc, nano::stat::detail::invocations);
|
||||
auto body (std::string (reinterpret_cast<char *> (buffer.data ()), buffer.size ()));
|
||||
|
||||
// Note that if the rpc action is async, the shared_ptr<json_handler> lifetime will be extended by the action handler
|
||||
auto handler (std::make_shared<nano::json_handler> (node, server.node_rpc_config, body, response_handler_l, [& server = server]() {
|
||||
server.stop ();
|
||||
server.node.alarm.add (std::chrono::steady_clock::now () + std::chrono::seconds (3), [& io_ctx = server.node.alarm.io_ctx]() {
|
||||
io_ctx.stop ();
|
||||
});
|
||||
}));
|
||||
// For unsafe actions to be allowed, the unsafe encoding must be used AND the transport config must allow it
|
||||
handler->process_request (allow_unsafe && config_transport.allow_unsafe);
|
||||
}
|
||||
|
||||
/** Async request reader */
|
||||
void read_next_request ()
|
||||
{
|
||||
auto this_l = this->shared_from_this ();
|
||||
|
||||
// Await next request indefinitely
|
||||
buffer.resize (sizeof (buffer_size));
|
||||
async_read_exactly (buffer.data (), buffer.size (), std::chrono::seconds::max (), [this_l]() {
|
||||
auto encoding (this_l->buffer[nano::ipc::preamble_offset::encoding]);
|
||||
this_l->active_encoding = static_cast<nano::ipc::payload_encoding> (encoding);
|
||||
if (this_l->buffer[nano::ipc::preamble_offset::lead] != 'N' || this_l->buffer[nano::ipc::preamble_offset::reserved_1] != 0 || this_l->buffer[nano::ipc::preamble_offset::reserved_2] != 0)
|
||||
{
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Invalid preamble");
|
||||
}
|
||||
}
|
||||
else if (encoding == static_cast<uint8_t> (nano::ipc::payload_encoding::json_v1) || encoding == static_cast<uint8_t> (nano::ipc::payload_encoding::json_v1_unsafe))
|
||||
{
|
||||
auto allow_unsafe (encoding == static_cast<uint8_t> (nano::ipc::payload_encoding::json_v1_unsafe));
|
||||
// Length of payload
|
||||
this_l->async_read_exactly (&this_l->buffer_size, sizeof (this_l->buffer_size), [this_l, allow_unsafe]() {
|
||||
boost::endian::big_to_native_inplace (this_l->buffer_size);
|
||||
this_l->buffer.resize (this_l->buffer_size);
|
||||
// Payload (ptree compliant JSON string)
|
||||
this_l->async_read_exactly (this_l->buffer.data (), this_l->buffer_size, [this_l, allow_unsafe]() {
|
||||
this_l->handle_json_query (allow_unsafe);
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (encoding == static_cast<uint8_t> (nano::ipc::payload_encoding::flatbuffers) || encoding == static_cast<uint8_t> (nano::ipc::payload_encoding::flatbuffers_json))
|
||||
{
|
||||
// Length of payload
|
||||
this_l->async_read_exactly (&this_l->buffer_size, sizeof (this_l->buffer_size), [this_l, encoding]() {
|
||||
boost::endian::big_to_native_inplace (this_l->buffer_size);
|
||||
this_l->buffer.resize (this_l->buffer_size);
|
||||
// Payload (flatbuffers or flatbuffers mappable json)
|
||||
this_l->async_read_exactly (this_l->buffer.data (), this_l->buffer_size, [this_l, encoding]() {
|
||||
this_l->session_timer.restart ();
|
||||
|
||||
// Lazily create one Flatbuffers handler instance per session
|
||||
if (!this_l->flatbuffers_handler)
|
||||
{
|
||||
this_l->flatbuffers_handler = std::make_shared<nano::ipc::flatbuffers_handler> (this_l->node, this_l->server, this_l->get_subscriber (), this_l->node.config.ipc_config);
|
||||
}
|
||||
|
||||
if (encoding == static_cast<uint8_t> (nano::ipc::payload_encoding::flatbuffers_json))
|
||||
{
|
||||
this_l->flatbuffers_handler->process_json (this_l->buffer.data (), this_l->buffer_size, [this_l](std::shared_ptr<std::string> body) {
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log (boost::str (boost::format ("IPC/Flatbuffer request completed in: %1% %2%") % this_l->session_timer.stop ().count () % this_l->session_timer.unit ()));
|
||||
}
|
||||
|
||||
auto big_endian_length = std::make_shared<uint32_t> (boost::endian::native_to_big (static_cast<uint32_t> (body->size ())));
|
||||
boost::array<boost::asio::const_buffer, 2> buffers = {
|
||||
boost::asio::buffer (big_endian_length.get (), sizeof (std::uint32_t)),
|
||||
boost::asio::buffer (body->data (), body->size ())
|
||||
};
|
||||
|
||||
this_l->queued_write (buffers, [this_l, body, big_endian_length](boost::system::error_code const & error_a, size_t size_a) {
|
||||
if (!error_a)
|
||||
{
|
||||
this_l->read_next_request ();
|
||||
}
|
||||
else if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Write failed: ", error_a.message ());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this_l->flatbuffers_handler->process (this_l->buffer.data (), this_l->buffer_size, [this_l](std::shared_ptr<flatbuffers::FlatBufferBuilder> fbb) {
|
||||
if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log (boost::str (boost::format ("IPC/Flatbuffer request completed in: %1% %2%") % this_l->session_timer.stop ().count () % this_l->session_timer.unit ()));
|
||||
}
|
||||
|
||||
auto big_endian_length = std::make_shared<uint32_t> (boost::endian::native_to_big (static_cast<uint32_t> (fbb->GetSize ())));
|
||||
boost::array<boost::asio::const_buffer, 2> buffers = {
|
||||
boost::asio::buffer (big_endian_length.get (), sizeof (std::uint32_t)),
|
||||
boost::asio::buffer (fbb->GetBufferPointer (), fbb->GetSize ())
|
||||
};
|
||||
|
||||
this_l->queued_write (buffers, [this_l, fbb, big_endian_length](boost::system::error_code const & error_a, size_t size_a) {
|
||||
if (!error_a)
|
||||
{
|
||||
this_l->read_next_request ();
|
||||
}
|
||||
else if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Write failed: ", error_a.message ());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (this_l->node.config.logging.log_ipc ())
|
||||
{
|
||||
this_l->node.logger.always_log ("IPC: Unsupported payload encoding");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Shut down and close socket. This is also called if the timer expires. */
|
||||
void close ()
|
||||
{
|
||||
boost::system::error_code ec_ignored;
|
||||
socket.shutdown (boost::asio::ip::tcp::socket::shutdown_both, ec_ignored);
|
||||
socket.close (ec_ignored);
|
||||
}
|
||||
|
||||
private:
|
||||
/** Holds the buffer and callback for queued writes */
|
||||
class queue_item
|
||||
{
|
||||
public:
|
||||
boost::asio::const_buffer buffer;
|
||||
std::function<void(boost::system::error_code const &, size_t)> callback;
|
||||
};
|
||||
size_t const queue_size_max = 64 * 1024;
|
||||
|
||||
nano::ipc::ipc_server & server;
|
||||
nano::node & node;
|
||||
|
||||
/** Unique session id */
|
||||
std::atomic<uint64_t> session_id;
|
||||
|
||||
/** Service name associated with this session. This is set through the ServiceRegister API */
|
||||
nano::locked<std::string> service_name;
|
||||
|
||||
/**
|
||||
* The payload encoding currently in use by this session. This is set as requests are
|
||||
* received and usually never changes (although a client technically can)
|
||||
*/
|
||||
std::atomic<nano::ipc::payload_encoding> active_encoding;
|
||||
|
||||
/** Timer for measuring the duration of ipc calls */
|
||||
nano::timer<std::chrono::microseconds> session_timer;
|
||||
|
||||
/**
|
||||
* IO context from node, or per-transport, depending on configuration.
|
||||
* Certain transports may scale better if they use a separate context.
|
||||
*/
|
||||
boost::asio::io_context & io_ctx;
|
||||
|
||||
/** IO strand for synchronizing */
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand;
|
||||
|
||||
/** The send queue is protected by always being accessed through the strand */
|
||||
std::deque<queue_item> send_queue;
|
||||
|
||||
/** A socket of the given asio type */
|
||||
SOCKET_TYPE socket;
|
||||
|
||||
/** Buffer sizes are read into this */
|
||||
uint32_t buffer_size{ 0 };
|
||||
|
||||
/** Buffer used to store data received from the client */
|
||||
std::vector<uint8_t> buffer;
|
||||
|
||||
/** Transport configuration */
|
||||
nano::ipc::ipc_config_transport & config_transport;
|
||||
|
||||
/** Handler for Flatbuffers requests. This is created lazily on the first request. */
|
||||
std::shared_ptr<nano::ipc::flatbuffers_handler> flatbuffers_handler;
|
||||
|
||||
/** Session subscriber */
|
||||
std::shared_ptr<nano::ipc::subscriber> subscriber;
|
||||
};
|
||||
|
||||
/** Domain and TCP socket transport */
|
||||
template <typename ACCEPTOR_TYPE, typename SOCKET_TYPE, typename ENDPOINT_TYPE>
|
||||
class socket_transport : public nano::ipc::transport
|
||||
{
|
||||
public:
|
||||
socket_transport (nano::ipc::ipc_server & server_a, ENDPOINT_TYPE endpoint_a, nano::ipc::ipc_config_transport & config_transport_a, int concurrency_a) :
|
||||
server (server_a), config_transport (config_transport_a)
|
||||
{
|
||||
// Using a per-transport event dispatcher?
|
||||
if (concurrency_a > 0)
|
||||
{
|
||||
io_ctx = std::make_unique<boost::asio::io_context> ();
|
||||
}
|
||||
|
||||
boost::asio::socket_base::reuse_address option (true);
|
||||
boost::asio::socket_base::keep_alive option_keepalive (true);
|
||||
acceptor = std::make_unique<ACCEPTOR_TYPE> (context (), endpoint_a);
|
||||
acceptor->set_option (option);
|
||||
acceptor->set_option (option_keepalive);
|
||||
accept ();
|
||||
|
||||
// Start serving IO requests. If concurrency_a is < 1, the node's thread pool/io_context is used instead.
|
||||
// A separate io_context for domain sockets may facilitate better performance on some systems.
|
||||
if (concurrency_a > 0)
|
||||
{
|
||||
runner = std::make_unique<nano::thread_runner> (*io_ctx, static_cast<unsigned> (concurrency_a));
|
||||
}
|
||||
}
|
||||
|
||||
boost::asio::io_context & context () const
|
||||
{
|
||||
return io_ctx ? *io_ctx : server.node.io_ctx;
|
||||
}
|
||||
|
||||
void accept ()
|
||||
{
|
||||
// Prepare the next session
|
||||
auto new_session (std::make_shared<session<SOCKET_TYPE>> (server, context (), config_transport));
|
||||
|
||||
acceptor->async_accept (new_session->get_socket (), [this, new_session](boost::system::error_code const & ec) {
|
||||
if (!ec)
|
||||
{
|
||||
new_session->read_next_request ();
|
||||
}
|
||||
else
|
||||
{
|
||||
server.node.logger.always_log ("IPC: acceptor error: ", ec.message ());
|
||||
}
|
||||
|
||||
if (ec != boost::asio::error::operation_aborted && acceptor->is_open ())
|
||||
{
|
||||
this->accept ();
|
||||
}
|
||||
else
|
||||
{
|
||||
server.node.logger.always_log ("IPC: shutting down");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void stop ()
|
||||
{
|
||||
acceptor->close ();
|
||||
if (io_ctx)
|
||||
{
|
||||
io_ctx->stop ();
|
||||
}
|
||||
|
||||
if (runner)
|
||||
{
|
||||
runner->join ();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
nano::ipc::ipc_server & server;
|
||||
nano::ipc::ipc_config_transport & config_transport;
|
||||
std::unique_ptr<nano::thread_runner> runner;
|
||||
std::unique_ptr<boost::asio::io_context> io_ctx;
|
||||
std::unique_ptr<ACCEPTOR_TYPE> acceptor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits SIGHUP via signal_set instead of std::signal, as this allows the handler to escape the
|
||||
* Posix signal handler restrictions
|
||||
*/
|
||||
void await_hup_signal (std::shared_ptr<boost::asio::signal_set> const & signals, nano::ipc::ipc_server & server_a)
|
||||
{
|
||||
signals->async_wait ([signals, &server_a](const boost::system::error_code & ec, int signal_number) {
|
||||
if (ec != boost::asio::error::operation_aborted)
|
||||
{
|
||||
std::cout << "Reloading access configuration..." << std::endl;
|
||||
auto error (server_a.reload_access_config ());
|
||||
if (!error)
|
||||
{
|
||||
std::cout << "Reloaded access configuration successfully" << std::endl;
|
||||
}
|
||||
await_hup_signal (signals, server_a);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nano::ipc::ipc_server::ipc_server (nano::node & node_a, nano::node_rpc_config const & node_rpc_config_a) :
|
||||
node (node_a),
|
||||
node_rpc_config (node_rpc_config_a),
|
||||
broker (node_a)
|
||||
{
|
||||
try
|
||||
{
|
||||
nano::error access_config_error (reload_access_config ());
|
||||
if (access_config_error)
|
||||
{
|
||||
std::exit (1);
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// Hook up config reloading through the HUP signal
|
||||
auto signals (std::make_shared<boost::asio::signal_set> (node.io_ctx, SIGHUP));
|
||||
await_hup_signal (signals, *this);
|
||||
#endif
|
||||
if (node_a.config.ipc_config.transport_domain.enabled)
|
||||
{
|
||||
#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
|
||||
auto threads = node_a.config.ipc_config.transport_domain.io_threads;
|
||||
file_remover = std::make_unique<dsock_file_remover> (node_a.config.ipc_config.transport_domain.path);
|
||||
boost::asio::local::stream_protocol::endpoint ep{ node_a.config.ipc_config.transport_domain.path };
|
||||
transports.push_back (std::make_shared<socket_transport<boost::asio::local::stream_protocol::acceptor, boost::asio::local::stream_protocol::socket, boost::asio::local::stream_protocol::endpoint>> (*this, ep, node_a.config.ipc_config.transport_domain, threads));
|
||||
#else
|
||||
node.logger.always_log ("IPC: Domain sockets are not supported on this platform");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (node_a.config.ipc_config.transport_tcp.enabled)
|
||||
{
|
||||
auto threads = node_a.config.ipc_config.transport_tcp.io_threads;
|
||||
transports.push_back (std::make_shared<socket_transport<boost::asio::ip::tcp::acceptor, boost::asio::ip::tcp::socket, boost::asio::ip::tcp::endpoint>> (*this, boost::asio::ip::tcp::endpoint (boost::asio::ip::tcp::v6 (), node_a.config.ipc_config.transport_tcp.port), node_a.config.ipc_config.transport_tcp, threads));
|
||||
}
|
||||
|
||||
node.logger.always_log ("IPC: server started");
|
||||
|
||||
if (!transports.empty ())
|
||||
{
|
||||
broker.start ();
|
||||
}
|
||||
}
|
||||
catch (std::runtime_error const & ex)
|
||||
{
|
||||
node.logger.always_log ("IPC: ", ex.what ());
|
||||
}
|
||||
}
|
||||
|
||||
nano::ipc::ipc_server::~ipc_server ()
|
||||
{
|
||||
node.logger.always_log ("IPC: server stopped");
|
||||
}
|
||||
|
||||
void nano::ipc::ipc_server::stop ()
|
||||
{
|
||||
for (auto & transport : transports)
|
||||
{
|
||||
transport->stop ();
|
||||
}
|
||||
}
|
||||
|
||||
nano::ipc::broker & nano::ipc::ipc_server::get_broker ()
|
||||
{
|
||||
return broker;
|
||||
}
|
||||
|
||||
nano::ipc::access & nano::ipc::ipc_server::get_access ()
|
||||
{
|
||||
return access;
|
||||
}
|
||||
|
||||
nano::error nano::ipc::ipc_server::reload_access_config ()
|
||||
{
|
||||
nano::error access_config_error (nano::ipc::read_access_config_toml (node.application_path, access));
|
||||
if (access_config_error)
|
||||
{
|
||||
auto error (boost::str (boost::format ("IPC: invalid access configuration file: %1%") % access_config_error.get_message ()));
|
||||
std::cerr << error << std::endl;
|
||||
node.logger.always_log (error);
|
||||
}
|
||||
return access_config_error;
|
||||
}
|
49
nano/node/ipc/ipc_server.hpp
Normal file
49
nano/node/ipc/ipc_server.hpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/ipc_flatbuffers_lib/generated/flatbuffers/nanoapi_generated.h>
|
||||
#include <nano/lib/errors.hpp>
|
||||
#include <nano/lib/ipc.hpp>
|
||||
#include <nano/node/ipc/ipc_access_config.hpp>
|
||||
#include <nano/node/ipc/ipc_broker.hpp>
|
||||
#include <nano/node/node_rpc_config.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
namespace flatbuffers
|
||||
{
|
||||
class Parser;
|
||||
}
|
||||
namespace nano
|
||||
{
|
||||
class node;
|
||||
class error;
|
||||
namespace ipc
|
||||
{
|
||||
class access;
|
||||
/** The IPC server accepts connections on one or more configured transports */
|
||||
class ipc_server final
|
||||
{
|
||||
public:
|
||||
ipc_server (nano::node & node, nano::node_rpc_config const & node_rpc_config);
|
||||
~ipc_server ();
|
||||
void stop ();
|
||||
|
||||
nano::node & node;
|
||||
nano::node_rpc_config const & node_rpc_config;
|
||||
|
||||
/** Unique counter/id shared across sessions */
|
||||
std::atomic<uint64_t> id_dispenser{ 1 };
|
||||
nano::ipc::broker & get_broker ();
|
||||
nano::ipc::access & get_access ();
|
||||
nano::error reload_access_config ();
|
||||
|
||||
private:
|
||||
void setup_callbacks ();
|
||||
nano::ipc::broker broker;
|
||||
nano::ipc::access access;
|
||||
std::unique_ptr<dsock_file_remover> file_remover;
|
||||
std::vector<std::shared_ptr<nano::ipc::transport>> transports;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -5104,6 +5104,23 @@ void nano::json_handler::work_peers_clear ()
|
|||
response_errors ();
|
||||
}
|
||||
|
||||
void nano::inprocess_rpc_handler::process_request (std::string const &, std::string const & body_a, std::function<void(std::string const &)> response_a)
|
||||
{
|
||||
// Note that if the rpc action is async, the shared_ptr<json_handler> lifetime will be extended by the action handler
|
||||
auto handler (std::make_shared<nano::json_handler> (node, node_rpc_config, body_a, response_a, [this]() {
|
||||
this->stop_callback ();
|
||||
this->stop ();
|
||||
}));
|
||||
handler->process_request ();
|
||||
}
|
||||
|
||||
void nano::inprocess_rpc_handler::process_request_v2 (rpc_handler_request_params const & params_a, std::string const & body_a, std::function<void(std::shared_ptr<std::string>)> response_a)
|
||||
{
|
||||
std::string body_l = params_a.json_envelope (body_a);
|
||||
auto handler (std::make_shared<nano::ipc::flatbuffers_handler> (node, ipc_server, nullptr, node.config.ipc_config));
|
||||
handler->process_json (reinterpret_cast<const uint8_t *> (body_l.data ()), body_l.size (), response_a);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
void construct_json (nano::container_info_component * component, boost::property_tree::ptree & parent)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/lib/numbers.hpp>
|
||||
#include <nano/node/ipc/flatbuffers_handler.hpp>
|
||||
#include <nano/node/wallet.hpp>
|
||||
#include <nano/rpc/rpc.hpp>
|
||||
|
||||
|
@ -11,6 +12,10 @@
|
|||
|
||||
namespace nano
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
class ipc_server;
|
||||
}
|
||||
class node;
|
||||
class node_rpc_config;
|
||||
|
||||
|
@ -168,22 +173,16 @@ class inprocess_rpc_handler final : public nano::rpc_handler_interface
|
|||
{
|
||||
public:
|
||||
inprocess_rpc_handler (
|
||||
nano::node & node_a, nano::node_rpc_config const & node_rpc_config_a, std::function<void()> stop_callback_a = []() {}) :
|
||||
nano::node & node_a, nano::ipc::ipc_server & ipc_server_a, nano::node_rpc_config const & node_rpc_config_a, std::function<void()> stop_callback_a = []() {}) :
|
||||
node (node_a),
|
||||
ipc_server (ipc_server_a),
|
||||
stop_callback (stop_callback_a),
|
||||
node_rpc_config (node_rpc_config_a)
|
||||
{
|
||||
}
|
||||
|
||||
void process_request (std::string const &, std::string const & body_a, std::function<void(std::string const &)> response_a) override
|
||||
{
|
||||
// Note that if the rpc action is async, the shared_ptr<json_handler> lifetime will be extended by the action handler
|
||||
auto handler (std::make_shared<nano::json_handler> (node, node_rpc_config, body_a, response_a, [this]() {
|
||||
this->stop_callback ();
|
||||
this->stop ();
|
||||
}));
|
||||
handler->process_request ();
|
||||
}
|
||||
void process_request (std::string const &, std::string const & body_a, std::function<void(std::string const &)> response_a) override;
|
||||
void process_request_v2 (rpc_handler_request_params const & params_a, std::string const & body_a, std::function<void(std::shared_ptr<std::string>)> response_a) override;
|
||||
|
||||
void stop () override
|
||||
{
|
||||
|
@ -200,6 +199,7 @@ public:
|
|||
|
||||
private:
|
||||
nano::node & node;
|
||||
nano::ipc::ipc_server & ipc_server;
|
||||
boost::optional<nano::rpc &> rpc;
|
||||
std::function<void()> stop_callback;
|
||||
nano::node_rpc_config const & node_rpc_config;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#include <nano/lib/json_error_response.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/json_handler.hpp>
|
||||
#include <nano/node/json_payment_observer.hpp>
|
||||
#include <nano/node/node.hpp>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
#include <nano/lib/numbers.hpp>
|
||||
#include <nano/lib/rocksdbconfig.hpp>
|
||||
#include <nano/lib/stats.hpp>
|
||||
#include <nano/node/ipcconfig.hpp>
|
||||
#include <nano/node/ipc/ipc_config.hpp>
|
||||
#include <nano/node/logging.hpp>
|
||||
#include <nano/node/websocketconfig.hpp>
|
||||
#include <nano/secure/common.hpp>
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include <nano/rpc/rpc_connection.hpp>
|
||||
#include <nano/rpc/rpc_handler.hpp>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#ifdef NANO_SECURE_RPC
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#endif
|
||||
|
@ -99,11 +101,14 @@ template <typename STREAM_TYPE>
|
|||
void nano::rpc_connection::parse_request (STREAM_TYPE & stream, std::shared_ptr<boost::beast::http::request_parser<boost::beast::http::empty_body>> header_parser)
|
||||
{
|
||||
auto this_l (shared_from_this ());
|
||||
auto header_field_credentials_l (header_parser->get ()["nano-api-key"]);
|
||||
auto header_corr_id_l (header_parser->get ()["nano-correlation-id"]);
|
||||
auto body_parser (std::make_shared<boost::beast::http::request_parser<boost::beast::http::string_body>> (std::move (*header_parser)));
|
||||
boost::beast::http::async_read (stream, buffer, *body_parser, boost::asio::bind_executor (strand, [this_l, body_parser, &stream](boost::system::error_code const & ec, size_t bytes_transferred) {
|
||||
auto path_l (body_parser->get ().target ().to_string ());
|
||||
boost::beast::http::async_read (stream, buffer, *body_parser, boost::asio::bind_executor (strand, [this_l, body_parser, header_field_credentials_l, header_corr_id_l, path_l, &stream](boost::system::error_code const & ec, size_t bytes_transferred) {
|
||||
if (!ec)
|
||||
{
|
||||
this_l->io_ctx.post ([this_l, body_parser, &stream]() {
|
||||
this_l->io_ctx.post ([this_l, body_parser, header_field_credentials_l, header_corr_id_l, path_l, &stream]() {
|
||||
auto & req (body_parser->get ());
|
||||
auto start (std::chrono::steady_clock::now ());
|
||||
auto version (req.version ());
|
||||
|
@ -121,13 +126,23 @@ void nano::rpc_connection::parse_request (STREAM_TYPE & stream, std::shared_ptr<
|
|||
ss << "RPC request " << request_id << " completed in: " << std::chrono::duration_cast<std::chrono::microseconds> (std::chrono::steady_clock::now () - start).count () << " microseconds";
|
||||
this_l->logger.always_log (ss.str ().c_str ());
|
||||
});
|
||||
|
||||
std::string api_path_l = "/api/v2";
|
||||
int rpc_version_l = boost::starts_with (path_l, api_path_l) ? 2 : 1;
|
||||
|
||||
auto method = req.method ();
|
||||
switch (method)
|
||||
{
|
||||
case boost::beast::http::verb::post:
|
||||
{
|
||||
auto handler (std::make_shared<nano::rpc_handler> (this_l->rpc_config, req.body (), request_id, response_handler, this_l->rpc_handler_interface, this_l->logger));
|
||||
handler->process_request ();
|
||||
nano::rpc_handler_request_params request_params;
|
||||
request_params.rpc_version = rpc_version_l;
|
||||
request_params.credentials = header_field_credentials_l.to_string ();
|
||||
request_params.correlation_id = header_corr_id_l.to_string ();
|
||||
request_params.path = boost::algorithm::erase_first_copy (path_l, api_path_l);
|
||||
request_params.path = boost::algorithm::erase_first_copy (request_params.path, "/");
|
||||
handler->process_request (request_params);
|
||||
break;
|
||||
}
|
||||
case boost::beast::http::verb::options:
|
||||
|
|
|
@ -26,7 +26,7 @@ logger (logger)
|
|||
{
|
||||
}
|
||||
|
||||
void nano::rpc_handler::process_request ()
|
||||
void nano::rpc_handler::process_request (nano::rpc_handler_request_params const & request_params)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -50,55 +50,70 @@ void nano::rpc_handler::process_request ()
|
|||
}
|
||||
else
|
||||
{
|
||||
boost::property_tree::ptree request;
|
||||
if (request_params.rpc_version == 1)
|
||||
{
|
||||
boost::property_tree::ptree request;
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << body;
|
||||
boost::property_tree::read_json (ss, request);
|
||||
}
|
||||
|
||||
auto action = request.get<std::string> ("action");
|
||||
// Creating same string via stringstream as using it directly is generating a TSAN warning
|
||||
std::stringstream ss;
|
||||
ss << body;
|
||||
boost::property_tree::read_json (ss, request);
|
||||
ss << request_id;
|
||||
logger.always_log (ss.str (), " ", filter_request (request));
|
||||
|
||||
// Check if this is a RPC command which requires RPC enabled control
|
||||
std::error_code rpc_control_disabled_ec = nano::error_rpc::rpc_control_disabled;
|
||||
|
||||
bool error = false;
|
||||
auto found = rpc_control_impl_set.find (action);
|
||||
if (found != rpc_control_impl_set.cend () && !rpc_config.enable_control)
|
||||
{
|
||||
json_error_response (response, rpc_control_disabled_ec.message ());
|
||||
error = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Special case with stats, type -> objects
|
||||
if (action == "stats" && !rpc_config.enable_control)
|
||||
{
|
||||
if (request.get<std::string> ("type") == "objects")
|
||||
{
|
||||
json_error_response (response, rpc_control_disabled_ec.message ());
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
else if (action == "process")
|
||||
{
|
||||
auto force = request.get_optional<bool> ("force").value_or (false);
|
||||
auto watch_work = request.get_optional<bool> ("watch_work").value_or (true);
|
||||
if ((force || watch_work) && !rpc_config.enable_control)
|
||||
{
|
||||
json_error_response (response, rpc_control_disabled_ec.message ());
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!error)
|
||||
{
|
||||
rpc_handler_interface.process_request (action, body, this->response);
|
||||
}
|
||||
}
|
||||
|
||||
auto action = request.get<std::string> ("action");
|
||||
// Creating same string via stringstream as using it directly is generating a TSAN warning
|
||||
std::stringstream ss;
|
||||
ss << request_id;
|
||||
logger.always_log (ss.str (), " ", filter_request (request));
|
||||
|
||||
// Check if this is a RPC command which requires RPC enabled control
|
||||
std::error_code rpc_control_disabled_ec = nano::error_rpc::rpc_control_disabled;
|
||||
|
||||
bool error = false;
|
||||
auto found = rpc_control_impl_set.find (action);
|
||||
if (found != rpc_control_impl_set.cend () && !rpc_config.enable_control)
|
||||
else if (request_params.rpc_version == 2)
|
||||
{
|
||||
json_error_response (response, rpc_control_disabled_ec.message ());
|
||||
error = true;
|
||||
rpc_handler_interface.process_request_v2 (request_params, body, [response = response](std::shared_ptr<std::string> body) {
|
||||
std::string body_l = *body;
|
||||
response (body_l);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Special case with stats, type -> objects
|
||||
if (action == "stats" && !rpc_config.enable_control)
|
||||
{
|
||||
if (request.get<std::string> ("type") == "objects")
|
||||
{
|
||||
json_error_response (response, rpc_control_disabled_ec.message ());
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
else if (action == "process")
|
||||
{
|
||||
auto force = request.get_optional<bool> ("force").value_or (false);
|
||||
auto watch_work = request.get_optional<bool> ("watch_work").value_or (true);
|
||||
if ((force || watch_work) && !rpc_config.enable_control)
|
||||
{
|
||||
json_error_response (response, rpc_control_disabled_ec.message ());
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!error)
|
||||
{
|
||||
rpc_handler_interface.process_request (action, body, this->response);
|
||||
assert (false);
|
||||
json_error_response (response, "Invalid RPC version");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,13 @@ namespace nano
|
|||
class rpc_config;
|
||||
class rpc_handler_interface;
|
||||
class logger_mt;
|
||||
class rpc_handler_request_params;
|
||||
|
||||
class rpc_handler : public std::enable_shared_from_this<nano::rpc_handler>
|
||||
{
|
||||
public:
|
||||
rpc_handler (nano::rpc_config const & rpc_config, std::string const & body_a, std::string const & request_id_a, std::function<void(std::string const &)> const & response_a, nano::rpc_handler_interface & rpc_handler_interface_a, nano::logger_mt & logger);
|
||||
void process_request ();
|
||||
void process_request (nano::rpc_handler_request_params const & request_params);
|
||||
|
||||
private:
|
||||
std::string body;
|
||||
|
|
|
@ -146,7 +146,8 @@ void nano::rpc_request_processor::run ()
|
|||
auto connection = *it;
|
||||
connection->is_available = false; // Make sure no one else can take it
|
||||
conditions_lk.unlock ();
|
||||
auto req (nano::ipc::prepare_request (nano::ipc::payload_encoding::json_legacy, rpc_request->body));
|
||||
auto encoding (rpc_request->rpc_api_version == 1 ? nano::ipc::payload_encoding::json_v1 : nano::ipc::payload_encoding::flatbuffers_json);
|
||||
auto req (nano::ipc::prepare_request (encoding, rpc_request->body));
|
||||
auto res (std::make_shared<std::vector<uint8_t>> ());
|
||||
|
||||
// Have we tried to connect yet?
|
||||
|
|
|
@ -27,6 +27,17 @@ struct rpc_request
|
|||
{
|
||||
}
|
||||
|
||||
rpc_request (int rpc_api_version_a, const std::string & body_a, std::function<void(std::string const &)> response_a) :
|
||||
rpc_api_version (rpc_api_version_a), body (body_a), response (response_a)
|
||||
{
|
||||
}
|
||||
|
||||
rpc_request (int rpc_api_version_a, const std::string & action_a, const std::string & body_a, std::function<void(std::string const &)> response_a) :
|
||||
rpc_api_version (rpc_api_version_a), action (action_a), body (body_a), response (response_a)
|
||||
{
|
||||
}
|
||||
|
||||
int rpc_api_version{ 1 };
|
||||
std::string action;
|
||||
std::string body;
|
||||
std::function<void(std::string const &)> response;
|
||||
|
@ -71,6 +82,15 @@ public:
|
|||
rpc_request_processor.add (std::make_shared<nano::rpc_request> (action_a, body_a, response_a));
|
||||
}
|
||||
|
||||
void process_request_v2 (rpc_handler_request_params const & params_a, std::string const & body_a, std::function<void(std::shared_ptr<std::string>)> response_a) override
|
||||
{
|
||||
std::string body_l = params_a.json_envelope (body_a);
|
||||
rpc_request_processor.add (std::make_shared<nano::rpc_request> (2 /* rpc version */, body_l, [response_a](std::string const & resp) {
|
||||
auto resp_l (std::make_shared<std::string> (resp));
|
||||
response_a (resp_l);
|
||||
}));
|
||||
}
|
||||
|
||||
void stop () override
|
||||
{
|
||||
rpc_request_processor.stop ();
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#include <nano/core_test/testutil.hpp>
|
||||
#include <nano/lib/rpcconfig.hpp>
|
||||
#include <nano/lib/threading.hpp>
|
||||
#include <nano/node/ipc.hpp>
|
||||
#include <nano/node/ipc/ipc_server.hpp>
|
||||
#include <nano/node/json_handler.hpp>
|
||||
#include <nano/node/node_rpc_config.hpp>
|
||||
#include <nano/node/testing.hpp>
|
||||
|
@ -7362,7 +7362,8 @@ TEST (rpc, in_process)
|
|||
nano::rpc_config rpc_config (nano::get_available_port (), true);
|
||||
rpc_config.rpc_process.ipc_port = node->config.ipc_config.transport_tcp.port;
|
||||
nano::node_rpc_config node_rpc_config;
|
||||
nano::inprocess_rpc_handler inprocess_rpc_handler (*node, node_rpc_config);
|
||||
nano::ipc::ipc_server ipc_server (*node, node_rpc_config);
|
||||
nano::inprocess_rpc_handler inprocess_rpc_handler (*node, ipc_server, node_rpc_config);
|
||||
nano::rpc rpc (system.io_ctx, rpc_config, inprocess_rpc_handler);
|
||||
rpc.start ();
|
||||
boost::property_tree::ptree request;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue