mirror of https://github.com/oxen-io/lokinet
Updated RpcServer Initialization and Logic
-- Moved all RPCServer initialization logic to rpcserver constructor -- Fixed config logic, fxn binding to rpc address, fxn adding rpc cats -- router hive failed CI/CD resulting from outdated reference to rpcBindAddr -- ipc socket as default hidden from windows (for now) refactored config endpoint - added rpc call script (contrib/omq-rpc.py) - added new fxns to .ini config stuff - added delete .ini file functionality to config endpoint - added edge case control for config endpoint add commented out line in clang-form for header reorg laterpull/2121/head
parent
0edfe8ff83
commit
13b01c86a6
@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import nacl.bindings as sodium
|
||||
from nacl.public import PrivateKey
|
||||
from nacl.signing import SigningKey, VerifyKey
|
||||
import nacl.encoding
|
||||
import requests
|
||||
import zmq
|
||||
import zmq.utils.z85
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import random
|
||||
import shutil
|
||||
|
||||
|
||||
context = zmq.Context()
|
||||
socket = context.socket(zmq.DEALER)
|
||||
socket.setsockopt(zmq.CONNECT_TIMEOUT, 5000)
|
||||
socket.setsockopt(zmq.HANDSHAKE_IVL, 5000)
|
||||
#socket.setsockopt(zmq.IMMEDIATE, 1)
|
||||
|
||||
if len(sys.argv) > 1 and any(sys.argv[1].startswith(x) for x in ("ipc://", "tcp://", "curve://")):
|
||||
remote = sys.argv[1]
|
||||
del sys.argv[1]
|
||||
else:
|
||||
remote = "ipc://./rpc.sock"
|
||||
|
||||
curve_pubkey = b''
|
||||
my_privkey, my_pubkey = b'', b''
|
||||
|
||||
# If given a curve://whatever/pubkey argument then transform it into 'tcp://whatever' and put the
|
||||
# 'pubkey' back into argv to be handled below.
|
||||
if remote.startswith("curve://"):
|
||||
pos = remote.rfind('/')
|
||||
pkhex = remote[pos+1:]
|
||||
remote = "tcp://" + remote[8:pos]
|
||||
if len(pkhex) != 64 or not all(x in "0123456789abcdefABCDEF" for x in pkhex):
|
||||
print("curve:// addresses must be in the form curve://HOST:PORT/REMOTE_PUBKEY_HEX", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
sys.argv[1:0] = [pkhex]
|
||||
|
||||
if len(sys.argv) > 1 and len(sys.argv[1]) == 64 and all(x in "0123456789abcdefABCDEF" for x in sys.argv[1]):
|
||||
curve_pubkey = bytes.fromhex(sys.argv[1])
|
||||
del sys.argv[1]
|
||||
socket.curve_serverkey = curve_pubkey
|
||||
if len(sys.argv) > 1 and len(sys.argv[1]) == 64 and all(x in "0123456789abcdefABCDEF" for x in sys.argv[1]):
|
||||
my_privkey = bytes.fromhex(sys.argv[1])
|
||||
del sys.argv[1]
|
||||
my_pubkey = zmq.utils.z85.decode(zmq.curve_public(zmq.utils.z85.encode(my_privkey)))
|
||||
else:
|
||||
my_privkey = PrivateKey.generate()
|
||||
my_pubkey = my_privkey.public_key.encode()
|
||||
my_privkey = my_privkey.encode()
|
||||
|
||||
print("No curve client privkey given; generated a random one (pubkey: {}, privkey: {})".format(
|
||||
my_pubkey.hex(), my_privkey.hex()), file=sys.stderr)
|
||||
socket.curve_secretkey = my_privkey
|
||||
socket.curve_publickey = my_pubkey
|
||||
|
||||
if not 2 <= len(sys.argv) <= 3 or any(x in y for x in ("--help", "-h") for y in sys.argv[1:]):
|
||||
print("Usage: {} [ipc:///path/to/sock|tcp://1.2.3.4:5678] [SERVER_CURVE_PUBKEY [LOCAL_CURVE_PRIVKEY]] COMMAND ['JSON']".format(
|
||||
sys.argv[0]), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
beginning_of_time = time.clock_gettime(time.CLOCK_MONOTONIC)
|
||||
|
||||
print("Connecting to {}".format(remote), file=sys.stderr)
|
||||
socket.connect(remote)
|
||||
to_send = [sys.argv[1].encode(), b'tagxyz123']
|
||||
to_send += (x.encode() for x in sys.argv[2:])
|
||||
print("Sending {}".format(to_send[0]), file=sys.stderr)
|
||||
socket.send_multipart(to_send)
|
||||
if socket.poll(timeout=5000):
|
||||
m = socket.recv_multipart()
|
||||
recv_time = time.clock_gettime(time.CLOCK_MONOTONIC)
|
||||
if len(m) < 3 or m[0:2] != [b'REPLY', b'tagxyz123']:
|
||||
print("Received unexpected {}-part reply:".format(len(m)), file=sys.stderr)
|
||||
for x in m:
|
||||
print("- {}".format(x))
|
||||
else: # m[2] is numeric value, m[3] is data part, and will become m[2] <- changed
|
||||
print("Received reply in {:.6f}s:".format(recv_time - beginning_of_time), file=sys.stderr)
|
||||
if len(m) < 3:
|
||||
print("(empty reply data)", file=sys.stderr)
|
||||
else:
|
||||
for x in m[2:]:
|
||||
print("{} bytes data part:".format(len(x)), file=sys.stderr)
|
||||
if any(x.startswith(y) for y in (b'd', b'l', b'i')) and x.endswith(b'e'):
|
||||
sys.stdout.buffer.write(x)
|
||||
else:
|
||||
print(x.decode(), end="\n\n")
|
||||
|
||||
else:
|
||||
print("Request timed out", file=sys.stderr)
|
||||
socket.close(linger=0)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ./lmq-rpc.py ipc://$HOME/.oxen/testnet/oxend.sock 'llarp.get_service_nodes' | jq
|
@ -0,0 +1,97 @@
|
||||
# High Level Iterative Approach
|
||||
|
||||
the desired outcome of this refactor will be splitting the existing code up into a stack of new components.
|
||||
a layer hides all functionality of the layer below it to reduce the complexity like the OSI stack intends to.
|
||||
the refactor starts at the top layer, wiring up the old implementation piecewise to the top layer.
|
||||
once the top layer is wired up to the old implementation we will move down to the next layer.
|
||||
this will repeat until we reach the bottom layer.
|
||||
once the old implementation is wired up into these new clearly defined layers, we can fixup or replace different parts of each layer one at a time as needed.
|
||||
|
||||
working down from each layer will let us pick apart the old implementation (if needed) that we would wire up to the new base classes for that layer we are defining now without worrying about what is below it (yet).
|
||||
|
||||
this refactor is very able to be split up into small work units that (ideally) do not confict with each other.
|
||||
|
||||
|
||||
PDU: https://en.wikipedia.org/wiki/Protocol_data_unit
|
||||
|
||||
# The New Layers
|
||||
|
||||
from top to bottom the new layers are:
|
||||
|
||||
* Platform Layer
|
||||
* Flow Layer
|
||||
* Routing Layer
|
||||
* Onion Layer
|
||||
* Link Layer
|
||||
* Wire Layer
|
||||
|
||||
|
||||
## Platform Layer
|
||||
|
||||
this is the top layer, it is responsibile ONLY to act as a handler of reading data from the "user" (via tun interface or whatever) to forward to the flow layer as desired, and to take data from the flow layer and send it to the "user".
|
||||
any kind of IP/dns mapping or traffic isolation details are done here. embedded lokinet would be implemented in this layer as well, as it is without a full tun interface.
|
||||
|
||||
Platform layer PDU are what the OS gives us and we internally convert them into flow layer PDU and hand them off to the flow layer.
|
||||
|
||||
|
||||
## Flow Layer
|
||||
|
||||
this layer is tl;dr mean to multiplex data from the platform layer across the routing layer and propagating PDU from the routing to the platform layer if needed.
|
||||
|
||||
the flow layer is responsible for sending platform layer PDU across path we have already established.
|
||||
this layer is informed by the routing layer below it of state changes in what paths are available for use.
|
||||
the flow layer requests from the layer below to make new paths if it wishes to get new ones on demand.
|
||||
this layer will recieve routing layer PDU from the routing layer and apply any congestion control needed to buffer things to the os if it is needed at all.
|
||||
|
||||
flow layer PDU are (data, ethertype, src-pubkey, dst-pubkey, isolation-metric) tuples.
|
||||
data is the datum we are tunneling over lokinet. ethertype tells us what kind of datum this is, e.g. plainquic/ipv4/ipv6/auth/etc.
|
||||
src-pubkey and dst-pubkey are public the ed25519 public keys of each end of the flow in use.
|
||||
the isolation metric is a piece of metadata we use to distinguish unique flows (convotag). in this new seperation convotags explicitly do not hand over across paths.
|
||||
|
||||
|
||||
## Routing Layer
|
||||
|
||||
this layer is tl;dr meant for path management but not path building.
|
||||
|
||||
the routing layer is responsible for sending/recieving flow layer PDU, DHT requests/responses, latency testing PDU and any other kind of PDU we send/recieve over the onion layer.
|
||||
this layer will be responsible for managing paths we have already built across lokinet.
|
||||
the routing layer will periodically measure path status/latency, and do any other kinds of perioidic path related tasks post build.
|
||||
this layer when asked for a new path from the flow layer will use one that has been prebuilt already and if the number of prebuilt paths is below a threshold we will tell the onion layer to build more paths.
|
||||
the routing layer will recieve path build results be their success/fail/timeout from the onion layer that were requested and apply any congestion control needed at the pivot router.
|
||||
|
||||
routing layer PDU are (data, src-path, dst-path) tuples.
|
||||
data is the datum we are transferring between paths.
|
||||
src-path and dst-path are (pathid, router id) tuples, the source being which path this routing layer PDU originated from, destination being which path it is going to.
|
||||
in the old model, router id is always the router that recieves it as the pivot router, this remains the same unless we explicitly provide router-id.
|
||||
this lets us propagate hints to DHT related PDU held inside the datum.
|
||||
|
||||
|
||||
## Onion Layer
|
||||
|
||||
the onion layer is repsonsible for path builds, path selection logic and low level details of encrypted/decrypting PDU that are onion routed over paths.
|
||||
this layer is requested by the routing layer to build a path to a pivot router with an optional additional constraints (e.g. unique cidr/operator/geoip/etc, latency constaints, hop length, path lifetime).
|
||||
the onion layer will encrypt PDU and send them to link layer as (frame/edge router id) tuples, and recieve link layer frames from edge routers, decrypt them and propagate them as needed to the routing layer.
|
||||
this layer also handles transit onion traffic and transit path build responsibilities as a snode and apply congestion control as needed per transit path.
|
||||
|
||||
the onion layer PDU are (data, src-path, dst-path) tuples.
|
||||
src-path and dst-path are (router-id, path-id) tuples which contain the ed25519 pubkey of the node and the 128 bit path-id it was associated with.
|
||||
data is some datum we are onion routing that we would apply symettric encryption as needed before propagating to upper or lower layers.
|
||||
|
||||
|
||||
## Link Layer
|
||||
|
||||
the link layer is responsbile for transmission of frames between nodes.
|
||||
this layer will handle queuing and congestion control between wire proto sessions between nodes.
|
||||
the link layer is will initate and recieve wire session to/from remote nodes.
|
||||
|
||||
the link layer PDU is (data, src-router-id, dst-router-id) tuples.
|
||||
data is a datum of a link layer frame.
|
||||
src-router-id and dst-router-id are (ed25519-pubkey, net-addr, wire-proto-info) tuples.
|
||||
the ed25519 pubkey is a .snode address, (clients have these too but they are ephemeral).
|
||||
net-addr is an (ip, port) tuple the node is reachable via the wire protocol.
|
||||
wire-proto-info is dialect specific wire protocol specific info.
|
||||
|
||||
## Wire Layer
|
||||
|
||||
the wire layer is responsible for transmitting link layer frames between nodes.
|
||||
all details here are specific to each wire proto dialect.
|
@ -1 +1 @@
|
||||
Subproject commit 12c17d6eab754908cd88f05d09b9388381e47515
|
||||
Subproject commit 9f2323a2db5fc54fe8394892769eff859967f735
|
@ -0,0 +1,63 @@
|
||||
#include "json_binary_proxy.hpp"
|
||||
#include <oxenc/hex.h>
|
||||
#include <oxenc/base64.h>
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
|
||||
void
|
||||
load_binary_parameter_impl(
|
||||
std::string_view bytes, size_t raw_size, bool allow_raw, uint8_t* val_data)
|
||||
{
|
||||
if (allow_raw && bytes.size() == raw_size)
|
||||
{
|
||||
std::memcpy(val_data, bytes.data(), bytes.size());
|
||||
return;
|
||||
}
|
||||
else if (bytes.size() == raw_size * 2)
|
||||
{
|
||||
if (oxenc::is_hex(bytes))
|
||||
{
|
||||
oxenc::from_hex(bytes.begin(), bytes.end(), val_data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const size_t b64_padded = (raw_size + 2) / 3 * 4;
|
||||
const size_t b64_padding = raw_size % 3 == 1 ? 2 : raw_size % 3 == 2 ? 1 : 0;
|
||||
const size_t b64_unpadded = b64_padded - b64_padding;
|
||||
const std::string_view b64_padding_string = b64_padding == 2 ? "=="sv
|
||||
: b64_padding == 1 ? "="sv
|
||||
: ""sv;
|
||||
if (bytes.size() == b64_unpadded
|
||||
|| (b64_padding > 0 && bytes.size() == b64_padded
|
||||
&& bytes.substr(b64_unpadded) == b64_padding_string))
|
||||
{
|
||||
if (oxenc::is_base64(bytes))
|
||||
{
|
||||
oxenc::from_base64(bytes.begin(), bytes.end(), val_data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error{"Invalid binary value: unexpected size and/or encoding"};
|
||||
}
|
||||
|
||||
nlohmann::json&
|
||||
json_binary_proxy::operator=(std::string_view binary_data)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case fmt::bt:
|
||||
return e = binary_data;
|
||||
case fmt::hex:
|
||||
return e = oxenc::to_hex(binary_data);
|
||||
case fmt::base64:
|
||||
return e = oxenc::to_base64(binary_data);
|
||||
}
|
||||
throw std::runtime_error{"Internal error: invalid binary encoding"};
|
||||
}
|
||||
|
||||
} // namespace llarp::rpc
|
@ -0,0 +1,181 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
|
||||
// Binary types that we support for rpc input/output. For json, these must be specified as hex or
|
||||
// base64; for bt-encoded requests these can be accepted as binary, hex, or base64.
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary = false;
|
||||
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary_container = false;
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary_container<std::vector<T>> = json_is_binary<T>;
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary_container<std::unordered_set<T>> = json_is_binary<T>;
|
||||
|
||||
// De-referencing wrappers around the above:
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary<const T&> = json_is_binary<T>;
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary<T&&> = json_is_binary<T>;
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary_container<const T&> = json_is_binary_container<T>;
|
||||
template <typename T>
|
||||
inline constexpr bool json_is_binary_container<T&&> = json_is_binary_container<T>;
|
||||
|
||||
void
|
||||
load_binary_parameter_impl(
|
||||
std::string_view bytes, size_t raw_size, bool allow_raw, uint8_t* val_data);
|
||||
|
||||
// Loads a binary value from a string_view which may contain hex, base64, and (optionally) raw
|
||||
// bytes.
|
||||
template <typename T, typename = std::enable_if_t<json_is_binary<T>>>
|
||||
void
|
||||
load_binary_parameter(std::string_view bytes, bool allow_raw, T& val)
|
||||
{
|
||||
load_binary_parameter_impl(bytes, sizeof(T), allow_raw, reinterpret_cast<uint8_t*>(&val));
|
||||
}
|
||||
|
||||
// Wrapper around a nlohmann::json that assigns a binary value either as binary (for bt-encoding);
|
||||
// or as hex or base64 (for json-encoding).
|
||||
class json_binary_proxy
|
||||
{
|
||||
public:
|
||||
nlohmann::json& e;
|
||||
enum class fmt
|
||||
{
|
||||
bt,
|
||||
hex,
|
||||
base64
|
||||
} format;
|
||||
explicit json_binary_proxy(nlohmann::json& elem, fmt format) : e{elem}, format{format}
|
||||
{}
|
||||
json_binary_proxy() = delete;
|
||||
|
||||
json_binary_proxy(const json_binary_proxy&) = default;
|
||||
json_binary_proxy(json_binary_proxy&&) = default;
|
||||
|
||||
/// Dereferencing a proxy element accesses the underlying nlohmann::json
|
||||
nlohmann::json&
|
||||
operator*()
|
||||
{
|
||||
return e;
|
||||
}
|
||||
nlohmann::json*
|
||||
operator->()
|
||||
{
|
||||
return &e;
|
||||
}
|
||||
|
||||
/// Descends into the json object, returning a new binary value proxy around the child element.
|
||||
template <typename T>
|
||||
json_binary_proxy
|
||||
operator[](T&& key)
|
||||
{
|
||||
return json_binary_proxy{e[std::forward<T>(key)], format};
|
||||
}
|
||||
|
||||
/// Returns a binary value proxy around the first/last element (requires an underlying list)
|
||||
json_binary_proxy
|
||||
front()
|
||||
{
|
||||
return json_binary_proxy{e.front(), format};
|
||||
}
|
||||
json_binary_proxy
|
||||
back()
|
||||
{
|
||||
return json_binary_proxy{e.back(), format};
|
||||
}
|
||||
|
||||
/// Assigns binary data from a string_view/string/etc.
|
||||
nlohmann::json&
|
||||
operator=(std::string_view binary_data);
|
||||
|
||||
/// Assigns binary data from a string_view over a 1-byte, non-char type (e.g. unsigned char or
|
||||
/// uint8_t).
|
||||
template <
|
||||
typename Char,
|
||||
std::enable_if_t<sizeof(Char) == 1 && !std::is_same_v<Char, char>, int> = 0>
|
||||
nlohmann::json&
|
||||
operator=(std::basic_string_view<Char> binary_data)
|
||||
{
|
||||
return *this = std::string_view{
|
||||
reinterpret_cast<const char*>(binary_data.data()), binary_data.size()};
|
||||
}
|
||||
|
||||
/// Takes a trivial, no-padding data structure (e.g. a crypto::hash) as the value and dumps its
|
||||
/// contents as the binary value.
|
||||
template <typename T, std::enable_if_t<json_is_binary<T>, int> = 0>
|
||||
nlohmann::json&
|
||||
operator=(const T& val)
|
||||
{
|
||||
return *this = std::string_view{reinterpret_cast<const char*>(&val), sizeof(val)};
|
||||
}
|
||||
|
||||
/// Takes a vector of some json_binary_proxy-assignable type and builds an array by assigning
|
||||
/// each one into a new array of binary values.
|
||||
template <typename T, std::enable_if_t<json_is_binary_container<T>, int> = 0>
|
||||
nlohmann::json&
|
||||
operator=(const T& vals)
|
||||
{
|
||||
auto a = nlohmann::json::array();
|
||||
for (auto& val : vals)
|
||||
json_binary_proxy{a.emplace_back(), format} = val;
|
||||
return e = std::move(a);
|
||||
}
|
||||
/// Emplaces a new nlohman::json to the end of an underlying list and returns a
|
||||
/// json_binary_proxy wrapping it.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// auto child = wrappedelem.emplace_back({"key1": 1}, {"key2": 2});
|
||||
/// child["binary-key"] = some_binary_thing;
|
||||
template <typename... Args>
|
||||
json_binary_proxy
|
||||
emplace_back(Args&&... args)
|
||||
{
|
||||
return json_binary_proxy{e.emplace_back(std::forward<Args>(args)...), format};
|
||||
}
|
||||
|
||||
/// Adds an element to an underlying list, then copies or moves the given argument onto it via
|
||||
/// json_binary_proxy assignment.
|
||||
template <typename T>
|
||||
void
|
||||
push_back(T&& val)
|
||||
{
|
||||
emplace_back() = std::forward<T>(val);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace llarp::rpc
|
||||
|
||||
// Specializations of binary types for deserialization; when receiving these from json we expect
|
||||
// them encoded in hex or base64. These may *not* be used for serialization, and will throw if so
|
||||
// invoked; for serialization you need to use RPC_COMMAND::response_hex (or _b64) instead.
|
||||
namespace nlohmann
|
||||
{
|
||||
template <typename T>
|
||||
struct adl_serializer<T, std::enable_if_t<llarp::rpc::json_is_binary<T>>>
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T> && std::has_unique_object_representations_v<T>);
|
||||
|
||||
static void
|
||||
to_json(const T&)
|
||||
{
|
||||
throw std::logic_error{"Internal error: binary types are not directly serializable"};
|
||||
}
|
||||
static void
|
||||
from_json(const json& j, T& val)
|
||||
{
|
||||
llarp::rpc::load_binary_parameter(j.get<std::string_view>(), false /*no raw*/, val);
|
||||
}
|
||||
};
|
||||
} // namespace nlohmann
|
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <oxenc/bt_value.h>
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
|
||||
inline oxenc::bt_value
|
||||
json_to_bt(json&& j)
|
||||
{
|
||||
if (j.is_object())
|
||||
{
|
||||
oxenc::bt_dict res;
|
||||
for (auto& [k, v] : j.items())
|
||||
{
|
||||
if (v.is_null())
|
||||
continue; // skip k-v pairs with a null v (for other nulls we fail).
|
||||
res[k] = json_to_bt(std::move(v));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
if (j.is_array())
|
||||
{
|
||||
oxenc::bt_list res;
|
||||
for (auto& v : j)
|
||||
res.push_back(json_to_bt(std::move(v)));
|
||||
return res;
|
||||
}
|
||||
if (j.is_string())
|
||||
{
|
||||
return std::move(j.get_ref<std::string&>());
|
||||
}
|
||||
if (j.is_boolean())
|
||||
return j.get<bool>() ? 1 : 0;
|
||||
if (j.is_number_unsigned())
|
||||
return j.get<uint64_t>();
|
||||
if (j.is_number_integer())
|
||||
return j.get<int64_t>();
|
||||
throw std::domain_error{
|
||||
"internal error: encountered some unhandled/invalid type in json-to-bt translation"};
|
||||
}
|
||||
|
||||
} // namespace llarp::rpc
|
@ -0,0 +1,359 @@
|
||||
#pragma once
|
||||
|
||||
#include "json_binary_proxy.hpp"
|
||||
#include <oxenc/bt_serialize.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
|
||||
using json_range = std::pair<nlohmann::json::const_iterator, nlohmann::json::const_iterator>;
|
||||
using rpc_input = std::variant<std::monostate, nlohmann::json, oxenc::bt_dict_consumer>;
|
||||
|
||||
// Checks that key names are given in ascending order
|
||||
template <typename... Ignore>
|
||||
void
|
||||
check_ascending_names(std::string_view name1, std::string_view name2, const Ignore&...)
|
||||
{
|
||||
if (!(name2 > name1))
|
||||
throw std::runtime_error{
|
||||
"Internal error: request values must be retrieved in ascending order"};
|
||||
}
|
||||
|
||||
// Wrapper around a reference for get_values that is used to indicate that the value is
|
||||
// required, in which case an exception will be raised if the value is not found. Usage:
|
||||
//
|
||||
// int a_optional = 0, b_required;
|
||||
// get_values(input,
|
||||
// "a", a_optional,
|
||||
// "b", required{b_required},
|
||||
// // ...
|
||||
// );
|
||||
template <typename T>
|
||||
struct required
|
||||
{
|
||||
T& value;
|
||||
required(T& ref) : value{ref}
|
||||
{}
|
||||
};
|
||||
template <typename T>
|
||||
constexpr bool is_required_wrapper = false;
|
||||
template <typename T>
|
||||
constexpr bool is_required_wrapper<required<T>> = true;
|
||||
|
||||
template <typename T>
|
||||
constexpr bool is_std_optional = false;
|
||||
template <typename T>
|
||||
constexpr bool is_std_optional<std::optional<T>> = true;
|
||||
|
||||
// Wrapper around a reference for get_values that adds special handling to act as if the value was
|
||||
// not given at all if the value is given as an empty string. This sucks, but is necessary for
|
||||
// backwards compatibility (especially with wallet2 clients).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// std::string x;
|
||||
// get_values(input,
|
||||
// "x", ignore_empty_string{x},
|
||||
// // ...
|
||||
// );
|
||||
template <typename T>
|
||||
struct ignore_empty_string
|
||||
{
|
||||
T& value;
|
||||
ignore_empty_string(T& ref) : value{ref}
|
||||
{}
|
||||
|
||||
bool
|
||||
should_ignore(oxenc::bt_dict_consumer& d)
|
||||
{
|
||||
if (d.is_string())
|
||||
{
|
||||
auto d2{d}; // Copy because we want to leave d intact
|
||||
if (d2.consume_string_view().empty())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
should_ignore(json_range& it_range)
|
||||
{
|
||||
auto& e = *it_range.first;
|
||||
return (e.is_string() && e.get<std::string_view>().empty());
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
constexpr bool is_ignore_empty_string_wrapper = false;
|
||||
template <typename T>
|
||||
constexpr bool is_ignore_empty_string_wrapper<ignore_empty_string<T>> = true;
|
||||
|
||||
// Advances the dict consumer to the first element >= the given name. Returns true if found,
|
||||
// false if it advanced beyond the requested name. This is exactly the same as
|
||||
// `d.skip_until(name)`, but is here so we can also overload an equivalent function for json
|
||||
// iteration.
|
||||
inline bool
|
||||
skip_until(oxenc::bt_dict_consumer& d, std::string_view name)
|
||||
{
|
||||
return d.skip_until(name);
|
||||
}
|
||||
// Equivalent to the above but for a json object iterator.
|
||||
inline bool
|
||||
skip_until(json_range& it_range, std::string_view name)
|
||||
{
|
||||
auto& [it, end] = it_range;
|
||||
while (it != end && it.key() < name)
|
||||
++it;
|
||||
return it != end && it.key() == name;
|
||||
}
|
||||
|
||||
// List types that are expandable; for these we emplace_back for each element of the input
|
||||
template <typename T>
|
||||
constexpr bool is_expandable_list = false;
|
||||
template <typename T>
|
||||
constexpr bool is_expandable_list<std::vector<T>> = true;
|
||||
|
||||
// Fixed size elements: tuples, pairs, and std::array's; we accept list input as long as the
|
||||
// list length matches exactly.
|
||||
template <typename T>
|
||||
constexpr bool is_tuple_like = false;
|
||||
template <typename T, size_t N>
|
||||
constexpr bool is_tuple_like<std::array<T, N>> = true;
|
||||
template <typename S, typename T>
|
||||
constexpr bool is_tuple_like<std::pair<S, T>> = true;
|
||||
template <typename... T>
|
||||
constexpr bool is_tuple_like<std::tuple<T...>> = true;
|
||||
|
||||
// True if T is a `std::unordered_map<std::string, ANYTHING...>`
|
||||
template <typename T>
|
||||
constexpr bool is_unordered_string_map = false;
|
||||
template <typename... ValueEtc>
|
||||
constexpr bool is_unordered_string_map<std::unordered_map<std::string, ValueEtc...>> = true;
|
||||
|
||||
template <typename TupleLike, size_t... Is>
|
||||
void
|
||||
load_tuple_values(oxenc::bt_list_consumer&, TupleLike&, std::index_sequence<Is...>);
|
||||
|
||||
// Consumes the next value from the dict consumer into `val`
|
||||
template <
|
||||
typename BTConsumer,
|
||||
typename T,
|
||||
std::enable_if_t<
|
||||
std::is_same_v<
|
||||
BTConsumer,
|
||||
oxenc::bt_dict_consumer> || std::is_same_v<BTConsumer, oxenc::bt_list_consumer>,
|
||||
int> = 0>
|
||||
void
|
||||
load_value(BTConsumer& c, T& val)
|
||||
{
|
||||
if constexpr (std::is_integral_v<T>)
|
||||
val = c.template consume_integer<T>();
|
||||
else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>)
|
||||
val = c.consume_string_view();
|
||||
else if constexpr (llarp::rpc::json_is_binary<T>)
|
||||
llarp::rpc::load_binary_parameter(c.consume_string_view(), true /*allow raw*/, val);
|
||||
else if constexpr (is_expandable_list<T>)
|
||||
{
|
||||
auto lc = c.consume_list_consumer();
|
||||
val.clear();
|
||||
while (!lc.is_finished())
|
||||
load_value(lc, val.emplace_back());
|
||||
}
|
||||
else if constexpr (is_tuple_like<T>)
|
||||
{
|
||||
auto lc = c.consume_list_consumer();
|
||||
load_tuple_values(lc, val, std::make_index_sequence<std::tuple_size_v<T>>{});
|
||||
}
|
||||
else if constexpr (is_unordered_string_map<T>)
|
||||
{
|
||||
auto dc = c.consume_dict_consumer();
|
||||
val.clear();
|
||||
while (!dc.is_finished())
|
||||
load_value(dc, val[std::string{dc.key()}]);
|
||||
}
|
||||
else
|
||||
static_assert(std::is_same_v<T, void>, "Unsupported load_value type");
|
||||
}
|
||||
|
||||
// Copies the next value from the json range into `val`, and advances the iterator. Throws
|
||||
// on unconvertible values.
|
||||
template <typename T>
|
||||
void
|
||||
load_value(json_range& r, T& val)
|
||||
{
|
||||
auto& key = r.first.key();
|
||||
auto& e = *r.first;
|
||||
if constexpr (std::is_same_v<T, bool>)
|
||||
{
|
||||
if (e.is_boolean())
|
||||
val = e.get<bool>();
|
||||
else if (e.is_number_unsigned())
|
||||
{
|
||||
// Also accept 0 or 1 for bools (mainly to be compatible with bt-encoding which doesn't
|
||||
// have a distinct bool type).
|
||||
auto b = e.get<uint64_t>();
|
||||
if (b <= 1)
|
||||
val = b;
|
||||
else
|
||||
throw std::domain_error{"Invalid value for '" + key + "': expected boolean"};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::domain_error{"Invalid value for '" + key + "': expected boolean"};
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_unsigned_v<T>)
|
||||
{
|
||||
if (!e.is_number_unsigned())
|
||||
throw std::domain_error{"Invalid value for '" + key + "': non-negative value required"};
|
||||
auto i = e.get<uint64_t>();
|
||||
if (sizeof(T) < sizeof(uint64_t) && i > std::numeric_limits<T>::max())
|
||||
throw std::domain_error{"Invalid value for '" + key + "': value too large"};
|
||||
val = i;
|
||||
}
|
||||
else if constexpr (std::is_integral_v<T>)
|
||||
{
|
||||
if (!e.is_number_integer())
|
||||
throw std::domain_error{"Invalid value for '" + key + "': value is not an integer"};
|
||||
auto i = e.get<int64_t>();
|
||||
if (sizeof(T) < sizeof(int64_t))
|
||||
{
|
||||
if (i < std::numeric_limits<T>::lowest())
|
||||
throw std::domain_error{
|
||||
"Invalid value for '" + key + "': negative value magnitude is too large"};
|
||||
if (i > std::numeric_limits<T>::max())
|
||||
throw std::domain_error{"Invalid value for '" + key + "': value is too large"};
|
||||
}
|
||||
val = i;
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>)
|
||||
{
|
||||
val = e.get<std::string_view>();
|
||||
}
|
||||
else if constexpr (
|
||||
llarp::rpc::json_is_binary<
|
||||
T> || is_expandable_list<T> || is_tuple_like<T> || is_unordered_string_map<T>)
|
||||
{
|
||||
try
|
||||
{
|
||||
e.get_to(val);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
throw std::domain_error{"Invalid values in '" + key + "'"};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(std::is_same_v<T, void>, "Unsupported load type");
|
||||
}
|
||||
++r.first;
|
||||
}
|
||||
|
||||
template <typename TupleLike, size_t... Is>
|
||||
void
|
||||
load_tuple_values(oxenc::bt_list_consumer& c, TupleLike& val, std::index_sequence<Is...>)
|
||||
{
|
||||
(load_value(c, std::get<Is>(val)), ...);
|
||||
}
|
||||
|
||||
// Takes a json object iterator or bt_dict_consumer and loads the current value at the iterator.
|
||||
// This calls itself recursively, if needed, to unwrap optional/required/ignore_empty_string
|
||||
// wrappers.
|
||||
template <typename In, typename T>
|
||||
void
|
||||
load_curr_value(In& in, T& val)
|
||||
{
|
||||
if constexpr (is_required_wrapper<T>)
|
||||
{
|
||||
load_curr_value(in, val.value);
|
||||
}
|
||||
else if constexpr (is_ignore_empty_string_wrapper<T>)
|
||||
{
|
||||
if (!val.should_ignore(in))
|
||||
load_curr_value(in, val.value);
|
||||
}
|
||||
else if constexpr (is_std_optional<T>)
|
||||
{
|
||||
load_curr_value(in, val.emplace());
|
||||
}
|
||||
else
|
||||
{
|
||||
load_value(in, val);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the next value from a json object iterator or bt_dict_consumer. Leaves the iterator at
|
||||
// the next value, i.e. found + 1 if found, or the next greater value if not found. (NB:
|
||||
// nlohmann::json objects are backed by an *ordered* map and so both nlohmann iterators and
|
||||
// bt_dict_consumer behave analogously here).
|
||||
template <typename In, typename T>
|
||||
void
|
||||
get_next_value(In& in, [[maybe_unused]] std::string_view name, T& val)
|
||||
{
|
||||
if constexpr (std::is_same_v<std::monostate, In>)
|
||||
;
|
||||
else if (skip_until(in, name))
|
||||
load_curr_value(in, val);
|
||||
else if constexpr (is_required_wrapper<T>)
|
||||
throw std::runtime_error{"Required key '" + std::string{name} + "' not found"};
|
||||
}
|
||||
|
||||
// Accessor for simple, flat value retrieval from a json or bt_dict_consumer. In the later
|
||||
// case note that the given bt_dict_consumer will be advanced, so you *must* take care to
|
||||
// process keys in order, both for the keys passed in here *and* for use before and after this
|
||||
// call.
|
||||
template <typename Input, typename T, typename... More>
|
||||
void
|
||||
get_values(Input& in, std::string_view name, T&& val, More&&... more)
|
||||
{
|
||||
if constexpr (std::is_same_v<rpc_input, Input>)
|
||||
{
|
||||
if (auto* json_in = std::get_if<nlohmann::json>(&in))
|
||||
{
|
||||
json_range r{json_in->cbegin(), json_in->cend()};
|
||||
get_values(r, name, val, std::forward<More>(more)...);
|
||||
}
|
||||
else if (auto* dict = std::get_if<oxenc::bt_dict_consumer>(&in))
|
||||
{
|
||||
get_values(*dict, name, val, std::forward<More>(more)...);
|
||||
}
|
||||
else
|
||||
{
|
||||
// A monostate indicates that no parameters field was provided at all
|
||||
get_values(var::get<std::monostate>(in), name, val, std::forward<More>(more)...);
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<std::string_view, Input>)
|
||||
{
|
||||
if (in.front() == 'd')
|
||||
{
|
||||
oxenc::bt_dict_consumer d{in};
|
||||
get_values(d, name, val, std::forward<More>(more)...);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto json_in = nlohmann::json::parse(in);
|
||||
json_range r{json_in.cbegin(), json_in.cend()};
|
||||
get_values(r, name, val, std::forward<More>(more)...);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(
|
||||
std::is_same_v<
|
||||
json_range,
|
||||
Input> || std::is_same_v<oxenc::bt_dict_consumer, Input> || std::is_same_v<std::monostate, Input>);
|
||||
get_next_value(in, name, val);
|
||||
if constexpr (sizeof...(More) > 0)
|
||||
{
|
||||
check_ascending_names(name, more...);
|
||||
get_values(in, std::forward<More>(more)...);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace llarp::rpc
|
@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc_server.hpp"
|
||||
#include "rpc_request_parser.hpp"
|
||||
#include "rpc_request_decorators.hpp"
|
||||
#include "rpc_request_definitions.hpp"
|
||||
#include "json_bt.hpp"
|
||||
#include <string_view>
|
||||
#include <llarp/config/config.hpp>
|
||||
#include <llarp/router/abstractrouter.hpp>
|
||||
#include <oxenmq/oxenmq.h>
|
||||
#include <oxenmq/address.h>
|
||||
#include <oxen/log/omq_logger.hpp>
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
template <typename RPC>
|
||||
auto
|
||||
make_invoke()
|
||||
{
|
||||
return [](oxenmq::Message& m, RPCServer& server) {
|
||||
EndpointHandler<RPC> handler{server, m.send_later()};
|
||||
auto& rpc = handler.rpc;
|
||||
|
||||
if (m.data.size() > 1)
|
||||
m.send_reply(CreateJSONError(
|
||||
"Bad Request: RPC requests must have at most one data part (received {})"_format(
|
||||
m.data.size())));
|
||||
|
||||
// parsing input as bt or json
|
||||
// hand off to parse_request (overloaded versions)
|
||||
try
|
||||
{
|
||||
if (m.data.empty() or m.data[0].empty())
|
||||
{
|
||||
parse_request(rpc, nlohmann::json::object());
|
||||
}
|
||||
else if (m.data[0].front() == 'd')
|
||||
{
|
||||
rpc.set_bt();
|
||||
parse_request(rpc, oxenc::bt_dict_consumer{m.data[0]});
|
||||
}
|
||||
else
|
||||
{
|
||||
parse_request(rpc, nlohmann::json::parse(m.data[0]));
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
m.send_reply(CreateJSONError("Failed to parse request parameters: "s + e.what()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (not std::is_base_of_v<Immediate, RPC>)
|
||||
{
|
||||
server.m_Router.loop()->call_soon(std::move(handler));
|
||||
}
|
||||
else
|
||||
{
|
||||
handler();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace llarp::rpc
|
@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include "json_binary_proxy.hpp"
|
||||
#include "json_bt.hpp"
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <string_view>
|
||||
#include <llarp/config/config.hpp>
|
||||
#include <oxenmq/oxenmq.h>
|
||||
#include <oxenmq/address.h>
|
||||
#include <oxen/log/omq_logger.hpp>
|
||||
|
||||
namespace tools
|
||||
{
|
||||
// Type wrapper that contains an arbitrary list of types.
|
||||
template <typename...>
|
||||
struct type_list
|
||||
{};
|
||||
} // namespace tools
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
// Base class that all RPC requests will expand for each endpoint type
|
||||
struct RPCRequest
|
||||
{
|
||||
private:
|
||||
bool bt = false;
|
||||
|
||||
public:
|
||||
// Returns true if response is bt-encoded, and false for json
|
||||
// Note: do not set value
|
||||
bool
|
||||
is_bt() const
|
||||
{
|
||||
return bt;
|
||||
}
|
||||
|
||||
// Callable method to indicate request is bt-encoded
|
||||
void
|
||||
set_bt()
|
||||
{
|
||||
bt = true;
|
||||
response_b64.format = llarp::rpc::json_binary_proxy::fmt::bt;
|
||||
response_hex.format = llarp::rpc::json_binary_proxy::fmt::bt;
|
||||
}
|
||||
|
||||
// Invoked if this.replier is still present. If it is "stolen" by endpoint (moved from
|
||||
// RPC struct), then endpoint handles sending reply
|
||||
void
|
||||
send_response()
|
||||
{
|
||||
replier->reply(is_bt() ? oxenc::bt_serialize(json_to_bt(std::move(response)))
|
||||
: response.dump());
|
||||
}
|
||||
|
||||
void
|
||||
send_response(nlohmann::json _response)
|
||||
{
|
||||
response = std::move(_response);
|
||||
send_response();
|
||||
}
|
||||
|
||||
// Response Data:
|
||||
// bt-encoded are converted in real-time
|
||||
// - bool becomes 0 or 1
|
||||
// - key:value where value == null are omitted
|
||||
// - other nulls will raise an exception if found in json
|
||||
// - no doubles
|
||||
// - to store doubles: encode bt in endpoint-specific way
|
||||
// - binary strings will fail json serialization; caller must
|
||||
//
|
||||
// std::string binary = some_binary_data();
|
||||
// request.response["binary_value"] = is_bt ? binary : oxenmq::to_hex(binary)
|
||||
//
|
||||
nlohmann::json response;
|
||||
|
||||
// Proxy Object:
|
||||
// Sets binary data in "response"
|
||||
// - if return type is json, encodes as hex
|
||||
// - if return type is bt, then binary is untouched
|
||||
//
|
||||
// Usage:
|
||||
// std::string data = "abc";
|
||||
// request.response_hex["foo"]["bar"] = data; // json: "616263", bt: "abc"
|
||||
//
|
||||
llarp::rpc::json_binary_proxy response_hex{response, llarp::rpc::json_binary_proxy::fmt::hex};
|
||||
|
||||
// Proxy Object:
|
||||
// Encodes binary data as base_64 for json-encoded responses, leaves as binary for bt-encoded
|
||||
// responses
|
||||
//
|
||||
// Usage:
|
||||
// std::string data = "abc"
|
||||
// request.response_b64["foo"]["bar"] = data; json: "YWJj", bt: "abc"
|
||||
//
|
||||
llarp::rpc::json_binary_proxy response_b64{
|
||||
response, llarp::rpc::json_binary_proxy::fmt::base64};
|
||||
|
||||
// The oxenmq deferred send object into which the response will be set. If this optional is
|
||||
// still set when the `invoke` call returns then the response is sent at that point; if it has
|
||||
// been moved out (i.e. either just this instance or the whole request struct is stolen/moved by
|
||||
// the invoke function) then it is the invoke function's job to send a reply. Typically this is
|
||||
// done when a response cannot be sent immediately
|
||||
std::optional<oxenmq::Message::DeferredSend> replier;
|
||||
};
|
||||
|
||||
// Tag types that are inherited to set RPC endpoint properties
|
||||
|
||||
// RPC call wil take no input arguments
|
||||
// Parameter dict can be passed, but will be ignored
|
||||
struct NoArgs : virtual RPCRequest
|
||||
{};
|
||||
|
||||
// RPC call will be executed immediately
|
||||
struct Immediate : virtual RPCRequest
|
||||
{};
|
||||
|
||||
} // namespace llarp::rpc
|
@ -0,0 +1,303 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc_request_decorators.hpp"
|
||||
#include "net/ip_range.hpp"
|
||||
#include "router/abstractrouter.hpp"
|
||||
#include "router/route_poker.hpp"
|
||||
#include "service/address.hpp"
|
||||
#include "service/endpoint.hpp"
|
||||
#include "service/outbound_context.hpp"
|
||||
#include <string_view>
|
||||
#include <llarp/config/config.hpp>
|
||||
#include <oxenmq/oxenmq.h>
|
||||
#include <oxenmq/address.h>
|
||||
#include <oxen/log/omq_logger.hpp>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
// RPC: halt
|
||||
// Stops lokinet router
|
||||
//
|
||||
// Inputs: none
|
||||
//
|
||||
struct Halt : NoArgs, Immediate
|
||||
{
|
||||
static constexpr auto name = "halt"sv;
|
||||
};
|
||||
|
||||
// RPC: version
|
||||
// Returns version and uptime information
|
||||
//
|
||||
// Inputs: none
|
||||
//
|
||||
// Returns: "OK"
|
||||
// "uptime"
|
||||
// "version"
|
||||
//
|
||||
struct Version : NoArgs, Immediate
|
||||
{
|
||||
static constexpr auto name = "version"sv;
|
||||
};
|
||||
|
||||
// RPC: status
|
||||
// Returns that current activity status of lokinet router
|
||||
// Calls router::extractstatus
|
||||
//
|
||||
// Inputs: none
|
||||
//
|
||||
// Returns: massive dump of status info including
|
||||
// "running"
|
||||
// "numNodesKnown"
|
||||
// "dht"
|
||||
// "services"
|
||||
// "exit"
|
||||
// "links"
|
||||
// "outboundMessages"
|
||||
// etc
|
||||
//
|
||||
struct Status : NoArgs
|
||||
{
|
||||
static constexpr auto name = "status"sv;
|
||||
};
|
||||
|
||||
// RPC: get_status
|
||||
// Returns current summary status
|
||||
//
|
||||
// Inputs: none
|
||||
//
|
||||
// Returns: slightly smaller dump of status info including
|
||||
// "authcodes"
|
||||
// "exitMap"
|
||||
// "lokiAddress"
|
||||
// "networkReady"
|
||||
// "numPathsBuilt"
|
||||
// "numPeersConnected"
|
||||
// etc
|
||||
//
|
||||
struct GetStatus : NoArgs
|
||||
{
|
||||
static constexpr auto name = "get_status"sv;
|
||||
};
|
||||
|
||||
// RPC: quic_connect
|
||||
// Initializes QUIC connection tunnel
|
||||
// Passes request parameters in nlohmann::json format
|
||||
//
|
||||
// Inputs:
|
||||
// "endpoint" : endpoint id (string)
|
||||
// "bindAddr" : bind address (string, ex: "127.0.0.1:1142")
|
||||
// "host" : remote host ID (string)
|
||||
// "port" : port to bind to (int)
|
||||
// "close" : close connection to port or host ID
|
||||
//
|
||||
// Returns:
|
||||
// "id" : connection ID
|
||||
// "addr" : connection local address
|
||||
//
|
||||
struct QuicConnect : RPCRequest
|
||||
{
|
||||
static constexpr auto name = "quic_connect"sv;
|
||||
|
||||
struct request_parameters
|
||||
{
|
||||
std::string bindAddr;
|
||||
int closeID;
|
||||
std::string endpoint;
|
||||
uint16_t port;
|
||||
std::string remoteHost;
|
||||
} request;
|
||||
};
|
||||
|
||||
// RPC: quick_listener
|
||||
// Connects to QUIC interface on local endpoint
|
||||
// Passes request parameters in nlohmann::json format
|
||||
//
|
||||
// Inputs:
|
||||
// "endpoint" : endpoint id (string)
|
||||
// "host" : remote host ID (string)
|
||||
// "port" : port to bind to (int)
|
||||
// "close" : close connection to port or host ID
|
||||
// "srv-proto" :
|
||||
//
|
||||
// Returns:
|
||||
// "id" : connection ID
|
||||
// "addr" : connection local address
|
||||
//
|
||||
struct QuicListener : RPCRequest
|
||||
{
|
||||
static constexpr auto name = "quic_listener"sv;
|
||||
|
||||
struct request_parameters
|
||||
{
|
||||
int closeID;
|
||||
std::string endpoint;
|
||||
uint16_t port;
|
||||
std::string remoteHost;
|
||||
std::string srvProto;
|
||||
} request;
|
||||
};
|
||||
|
||||
// RPC: lookup_snode
|
||||
// Look up service node
|
||||
// Passes request parameters in nlohmann::json format
|
||||
//
|
||||
// Inputs:
|
||||
// "routerID" : router ID to query (string)
|
||||
//
|
||||
// Returns:
|
||||
// "ip" : snode IP address
|
||||
//
|
||||
struct LookupSnode : RPCRequest
|
||||
{
|
||||
static constexpr auto name = "lookup_snode"sv;
|
||||
|
||||
struct request_parameters
|
||||
{
|
||||
std::string routerID;
|
||||
} request;
|
||||
};
|
||||
|
||||
// RPC: exit
|
||||
// Seems like this adds an exit node?
|
||||
//
|
||||
// Note: ask Jason about the internals of this
|
||||
//
|
||||
// Inputs:
|
||||
// "endpoint" :
|
||||
// "unmap" : if true, unmaps connection to exit node (bool)
|
||||
// "range" : IP range to map to exit node
|
||||
// "token" :
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
struct Exit : RPCRequest
|
||||
{
|
||||
static constexpr auto name = "exit"sv;
|
||||
|
||||
struct request_parameters
|
||||
{
|
||||
std::string address;
|
||||
std::string ip_range;
|
||||
std::string token;
|
||||
bool unmap;
|
||||
} request;
|
||||
|
||||
void
|
||||
onGoodResult(std::string reason, bool hasClient)
|
||||
{
|
||||
response = (hasClient) ?
|
||||
nlohmann::json{{"result", reason}}.dump() :
|
||||
nlohmann::json{{"error", "We don't have an exit?"}}.dump();
|
||||
}
|
||||
|
||||
void
|
||||
onBadResult(std::string reason, AbstractRouter& abs, llarp::service::Endpoint_ptr eptr, IPRange range)
|
||||
{
|
||||
abs.routePoker()->Down();
|
||||
eptr->UnmapExitRange(range);
|
||||
response = nlohmann::json{{"result", reason}}.dump();
|
||||
}
|
||||
|
||||
void
|
||||
mapExit(service::Address addr, AbstractRouter& router, llarp::service::Endpoint_ptr eptr, IPRange range, service::Address exitAddr)
|
||||
{
|
||||
eptr->MapExitRange(range, addr);
|
||||
|
||||
bool sendAuth = (request.token.empty()) ? false : true;
|
||||
if (sendAuth)
|
||||
eptr->SetAuthInfoForEndpoint(exitAddr, service::AuthInfo{request.token});
|
||||
|
||||
if (addr.IsZero())
|
||||
{
|
||||
onGoodResult("Null exit added", router.HasClientExit());
|
||||
return;
|
||||
}
|
||||
|
||||
eptr->MarkAddressOutbound(addr);
|
||||
|
||||
eptr->EnsurePathToService(addr, [&](auto, service::OutboundContext* ctx) {
|
||||
if (ctx == nullptr)
|
||||
{
|
||||
onBadResult("Could not find exit", router, eptr, range);
|
||||
return;
|
||||
}
|
||||
if (not sendAuth)
|
||||
{
|
||||
onGoodResult("OK: connected to " + addr.ToString(), router.HasClientExit());
|
||||
return;
|
||||
}
|
||||
// only lambda that we will keep
|
||||
ctx->AsyncSendAuth([&](service::AuthResult result) {
|
||||
if (result.code != service::AuthResultCode::eAuthAccepted)
|
||||
{
|
||||
onBadResult(result.reason, router, eptr, range);
|
||||
return;
|
||||
}
|
||||
onGoodResult(result.reason, router.HasClientExit());
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// RPC: dns_query
|
||||
// Attempts to query endpoint by domain name
|
||||
//
|
||||
// Note: ask Jason about the internals of this
|
||||
//
|
||||
// Inputs:
|
||||
// "endpoint" : endpoint ID to query (string)
|
||||
// "qname" : query name (string)
|
||||
// "qtype" : query type (int)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
struct DNSQuery : Immediate
|
||||
{
|
||||
static constexpr auto name = "dns_query"sv;
|
||||
|
||||
struct request_parameters
|
||||
{
|
||||
std::string endpoint;
|
||||
uint16_t qtype;
|
||||
std::string qname;
|
||||
} request;
|
||||
};
|
||||
|
||||
// RPC: config
|
||||
// Runs lokinet router using .ini config file passed as path
|
||||
//
|
||||
// Inputs:
|
||||
// "filename" : name of .ini file to either save or delete
|
||||
// "ini" : .ini chunk to save in new file
|
||||
// "del" : boolean specifying whether to delete file "filename" or save it
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
struct Config : Immediate
|
||||
{
|
||||
static constexpr auto name = "config"sv;
|
||||
|
||||
struct request_parameters
|
||||
{
|
||||
bool del;
|
||||
std::string filename;
|
||||
std::string ini;
|
||||
} request;
|
||||
};
|
||||
|
||||
// List of all RPC request structs to allow compile-time enumeration of all supported types
|
||||
using rpc_request_types = tools::type_list<
|
||||
Halt,
|
||||
Version,
|
||||
Status,
|
||||
GetStatus,
|
||||
QuicConnect,
|
||||
QuicListener,
|
||||
LookupSnode,
|
||||
Exit,
|
||||
DNSQuery,
|
||||
Config>;
|
||||
|
||||
} // namespace llarp::rpc
|
@ -0,0 +1,94 @@
|
||||
#include "rpc_request_parser.hpp"
|
||||
#include "param_parser.hpp"
|
||||
#include <string_view>
|
||||
#include <llarp/config/config.hpp>
|
||||
#include <oxenmq/oxenmq.h>
|
||||
#include <oxenmq/address.h>
|
||||
#include <oxen/log/omq_logger.hpp>
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
using nlohmann::json;
|
||||
|
||||
void
|
||||
parse_request(QuicConnect& quicconnect, rpc_input input)
|
||||
{
|
||||
get_values(
|
||||
input,
|
||||
"bindAddr",
|
||||
quicconnect.request.bindAddr,
|
||||
"closeID",
|
||||
quicconnect.request.closeID,
|
||||
"endpoint",
|
||||
quicconnect.request.endpoint,
|
||||
"port",
|
||||
quicconnect.request.port,
|
||||
"remoteHost",
|
||||
quicconnect.request.remoteHost);
|
||||
}
|
||||
|
||||
void
|
||||
parse_request(QuicListener& quiclistener, rpc_input input)
|
||||
{
|
||||
get_values(
|
||||
input,
|
||||
"closeID",
|
||||
quiclistener.request.closeID,
|
||||
"endpoint",
|
||||
quiclistener.request.endpoint,
|
||||
"port",
|
||||
quiclistener.request.port,
|
||||
"remoteHost",
|
||||
quiclistener.request.remoteHost,
|
||||
"srvProto",
|
||||
quiclistener.request.srvProto);
|
||||
}
|
||||
|
||||
void
|
||||
parse_request(LookupSnode& lookupsnode, rpc_input input)
|
||||
{
|
||||
get_values(input, "routerID", lookupsnode.request.routerID);
|
||||
}
|
||||
|
||||
void
|
||||
parse_request(Exit& exit, rpc_input input)
|
||||
{
|
||||
get_values(
|
||||
input,
|
||||
"address",
|
||||
exit.request.address,
|
||||
"IP_range",
|
||||
exit.request.ip_range,
|
||||
"token",
|
||||
exit.request.token,
|
||||
"unmap",
|
||||
exit.request.unmap);
|
||||
}
|
||||
|
||||
void
|
||||
parse_request(DNSQuery& dnsquery, rpc_input input)
|
||||
{
|
||||
get_values(
|
||||
input,
|
||||
"endpoint",
|
||||
dnsquery.request.endpoint,
|
||||
"qname",
|
||||
dnsquery.request.qname,
|
||||
"qtype",
|
||||
dnsquery.request.qtype);
|
||||
}
|
||||
|
||||
void
|
||||
parse_request(Config& config, rpc_input input)
|
||||
{
|
||||
get_values(
|
||||
input,
|
||||
"delete",
|
||||
config.request.del,
|
||||
"filename",
|
||||
config.request.filename,
|
||||
"ini",
|
||||
config.request.ini);
|
||||
}
|
||||
|
||||
} // namespace llarp::rpc
|
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc_request_definitions.hpp"
|
||||
#include <string_view>
|
||||
#include <llarp/config/config.hpp>
|
||||
#include <oxenmq/oxenmq.h>
|
||||
#include <oxenmq/address.h>
|
||||
#include <oxen/log/omq_logger.hpp>
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
using rpc_input = std::variant<std::monostate, nlohmann::json, oxenc::bt_dict_consumer>;
|
||||
|
||||
inline void
|
||||
parse_request(NoArgs&, rpc_input)
|
||||
{}
|
||||
|
||||
void
|
||||
parse_request(QuicConnect& quicconnect, rpc_input input);
|
||||
void
|
||||
parse_request(QuicListener& quiclistener, rpc_input input);
|
||||
void
|
||||
parse_request(LookupSnode& lookupsnode, rpc_input input);
|
||||
void
|
||||
parse_request(Exit& exit, rpc_input input);
|
||||
void
|
||||
parse_request(DNSQuery& dnsquery, rpc_input input);
|
||||
void
|
||||
parse_request(Config& config, rpc_input input);
|
||||
|
||||
} // namespace llarp::rpc
|
File diff suppressed because it is too large
Load Diff
@ -1,35 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include "rpc_request_definitions.hpp"
|
||||
#include "json_bt.hpp"
|
||||
#include <string_view>
|
||||
#include <llarp/config/config.hpp>
|
||||
#include <oxenmq/oxenmq.h>
|
||||
#include <oxenmq/message.h>
|
||||
#include <oxenmq/address.h>
|
||||
#include <oxen/log/omq_logger.hpp>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
struct AbstractRouter;
|
||||
}
|
||||
} // namespace llarp
|
||||
|
||||
namespace
|
||||
{
|
||||
static auto logcat = llarp::log::Cat("lokinet.rpc");
|
||||
} // namespace
|
||||
|
||||
namespace llarp::rpc
|
||||
{
|
||||
using LMQ_ptr = std::shared_ptr<oxenmq::OxenMQ>;
|
||||
using DeferredSend = oxenmq::Message::DeferredSend;
|
||||
|
||||
class RPCServer;
|
||||
|
||||
struct RpcServer
|
||||
// Stores RPC request callback
|
||||
struct rpc_callback
|
||||
{
|
||||
explicit RpcServer(LMQ_ptr, AbstractRouter*);
|
||||
~RpcServer() = default;
|
||||
using result_type = std::variant<oxenc::bt_value, nlohmann::json, std::string>;
|
||||
// calls with incoming request data; returns response body or throws exception
|
||||
void (*invoke)(oxenmq::Message&, RPCServer&);
|
||||
};
|
||||
|
||||
void
|
||||
AddRPCCategories();
|
||||
// RPC request registration
|
||||
// Stores references to RPC requests in a unordered map for ease of reference
|
||||
// when adding to server. To add endpoints, define in rpc_request_definitions.hpp
|
||||
// and register in rpc_server.cpp
|
||||
extern const std::unordered_map<std::string, std::shared_ptr<const rpc_callback>> rpc_request_map;
|
||||
|
||||
// Exception used to signal various types of errors with a request back to the caller. This
|
||||
// exception indicates that the caller did something wrong: bad data, invalid value, etc., but
|
||||
// don't indicate a local problem (and so we'll log them only at debug). For more serious,
|
||||
// internal errors a command should throw some other stl error (e.g. std::runtime_error or
|
||||
// perhaps std::logic_error), which will result in a local daemon warning (and a generic internal
|
||||
// error response to the user).
|
||||
//
|
||||
// For JSON RPC these become an error response with the code as the error.code value and the
|
||||
// string as the error.message.
|
||||
// For HTTP JSON these become a 500 Internal Server Error response with the message as the body.
|
||||
// For OxenMQ the code becomes the first part of the response and the message becomes the
|
||||
// second part of the response.
|
||||
struct rpc_error : std::runtime_error
|
||||
{
|
||||
/// \param message - a message to send along with the error code (see general description
|
||||
/// above).
|
||||
rpc_error(std::string message)
|
||||
: std::runtime_error{"RPC error: " + message}, message{std::move(message)}
|
||||
{}
|
||||
|
||||
std::string message;
|
||||
};
|
||||
|
||||
template <typename Result_t>
|
||||
std::string
|
||||
CreateJSONResponse(Result_t result)
|
||||
{
|
||||
return nlohmann::json{{"result", result}}.dump();
|
||||
}
|
||||
|
||||
inline std::string
|
||||
CreateJSONError(std::string_view msg)
|
||||
{
|
||||
return nlohmann::json{{"error", msg}}.dump();
|
||||
}
|
||||
|
||||
class RPCServer
|
||||
{
|
||||
public:
|
||||
explicit RPCServer(LMQ_ptr, AbstractRouter&);
|
||||
~RPCServer() = default;
|
||||
|
||||
private:
|
||||
void
|
||||
HandleLogsSubRequest(oxenmq::Message& m);
|
||||
|
||||
LMQ_ptr m_LMQ;
|
||||
AbstractRouter* const m_Router;
|
||||
void
|
||||
AddCategories();
|
||||
|
||||
void
|
||||
invoke(Halt& halt);
|
||||
void
|
||||
invoke(Version& version);
|
||||
void
|
||||
invoke(Status& status);
|
||||
void
|
||||
invoke(GetStatus& getstatus);
|
||||
void
|
||||
invoke(QuicConnect& quicconnect);
|
||||
void
|
||||
invoke(QuicListener& quiclistener);
|
||||
void
|
||||
invoke(LookupSnode& lookupsnode);
|
||||
void
|
||||
invoke(Exit& exit);
|
||||
void
|
||||
invoke(DNSQuery& dnsquery);
|
||||
void
|
||||
invoke(Config& config);
|
||||
|
||||
LMQ_ptr m_LMQ;
|
||||
AbstractRouter& m_Router;
|
||||
oxen::log::PubsubLogger log_subs;
|
||||
};
|
||||
|
||||
template <typename RPC>
|
||||
class EndpointHandler
|
||||
{
|
||||
public:
|
||||
RPCServer& server;
|
||||
RPC rpc{};
|
||||
|
||||
EndpointHandler(RPCServer& _server, DeferredSend _replier)
|
||||
: server{_server}
|
||||
{
|
||||
rpc.replier.emplace(std::move(_replier));
|
||||
}
|
||||
|
||||
void
|
||||
operator()()
|
||||
{
|
||||
try
|
||||
{
|
||||
server.invoke(rpc);
|
||||
}
|
||||
catch (const rpc_error& e)
|
||||
{
|
||||
log::info(logcat, "RPC request 'rpc.{}' failed with: {}", rpc.name, e.what());
|
||||
rpc.response = CreateJSONError(
|
||||
fmt::format("RPC request 'rpc.{}' failed with: {}", rpc.name, e.what()));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
log::info(logcat, "RPC request 'rpc.{}' raised an exception: {}", rpc.name, e.what());
|
||||
rpc.response = CreateJSONError(
|
||||
fmt::format("RPC request 'rpc.{}' raised an exception: {}", rpc.name, e.what()));
|
||||
};
|
||||
|
||||
// check if std::optional in rpc is present
|
||||
// then rpc.send_response
|
||||
// else
|
||||
// do nothing because invoke stole RPC
|
||||
if (rpc.replier.has_value())
|
||||
rpc.send_response();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace llarp::rpc
|
||||
|
Loading…
Reference in New Issue