Merge pull request #2121 from drouhana/rpc-refactor

RPC refactor
This commit is contained in:
Jason Rhinelander 2023-02-01 19:55:18 -04:00 committed by GitHub
commit 7fb36782dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2429 additions and 710 deletions

99
contrib/omq-rpc.py Executable file
View File

@ -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)
# sample usage:
# ./omq-rpc.py ipc://$HOME/.oxen/testnet/oxend.sock 'llarp.get_service_nodes' | jq

View File

@ -5,10 +5,12 @@
#include <vector>
#include <array>
#include <llarp/net/net.hpp>
#include <string_view>
#include <CLI/App.hpp>
#include <CLI/Formatter.hpp>
#include <CLI/Config.hpp>
#include "oxenmq/address.h"
#ifdef _WIN32
// add the unholy windows headers for iphlpapi
@ -56,7 +58,6 @@ OMQ_Request(
namespace
{
struct command_line_options
{
// bool options
@ -64,6 +65,7 @@ namespace
bool help = false;
bool vpnUp = false;
bool vpnDown = false;
bool swap = false;
bool printStatus = false;
bool killDaemon = false;
@ -73,9 +75,10 @@ namespace
std::string endpoint = "default";
std::string token;
std::optional<std::string> range;
std::vector<std::string> swapExits;
// oxenmq
oxenmq::address rpcURL{"tcp://127.0.0.1:1190"};
oxenmq::address rpcURL{};
oxenmq::LogLevel logLevel = oxenmq::LogLevel::warn;
};
@ -109,15 +112,23 @@ main(int argc, char* argv[])
// flags: boolean values in command_line_options struct
cli.add_flag("-v,--verbose", options.verbose, "Verbose");
cli.add_flag("--up", options.vpnUp, "Put VPN up");
cli.add_flag("--down", options.vpnDown, "Put VPN down");
cli.add_flag("--add,--up", options.vpnUp, "Map VPN connection to exit node [--up is deprecated]");
cli.add_flag(
"--remove,--down",
options.vpnDown,
"Unmap VPN connection to exit node [--down is deprecated]");
cli.add_flag("--status", options.printStatus, "Print VPN status and exit");
cli.add_flag("-k,--kill", options.killDaemon, "Kill lokinet daemon");
// options: string values in command_line_options struct
cli.add_option("--exit", options.exitAddress, "Specify exit node address")->capture_default_str();
cli.add_option("--endpoint", options.endpoint, "Endpoint to use")->capture_default_str();
cli.add_option("--token", options.token, "Exit auth token to use")->capture_default_str();
cli.add_option("--token,--auth", options.token, "Exit auth token to use")->capture_default_str();
cli.add_option("--range", options.range, "IP range to map exit to")->capture_default_str();
cli.add_option(
"--swap", options.swapExits, "Exit addresses to swap mapped connection to [old] [new]")
->expected(2)
->capture_default_str();
// options: oxenmq values in command_line_options struct
cli.add_option("--rpc", options.rpc, "Specify RPC URL for lokinet")->capture_default_str();
@ -149,16 +160,17 @@ main(int argc, char* argv[])
cli.exit(e);
};
int numCommands = options.vpnUp + options.vpnDown + options.printStatus + options.killDaemon;
int numCommands = options.vpnUp + options.vpnDown + options.printStatus + options.killDaemon
+ (not options.swapExits.empty());
switch (numCommands)
{
case 0:
return exit_error(3, "One of --up/--down/--status/--kill must be specified");
return exit_error(3, "One of --add/--remove/--swap/--status/--kill must be specified");
case 1:
break;
default:
return exit_error(3, "Only one of --up/--down/--status/--kill may be specified");
return exit_error(3, "Only one of --add/--remove/--swap/--status/--kill may be specified");
}
if (options.vpnUp and options.exitAddress.empty())
@ -170,12 +182,14 @@ main(int argc, char* argv[])
},
options.logLevel};
options.rpcURL = oxenmq::address{(options.rpc.empty()) ? "tcp://127.0.0.1:1190" : options.rpc};
omq.start();
std::promise<bool> connectPromise;
const auto connectionID = omq.connect_remote(
options.rpc,
options.rpcURL,
[&connectPromise](auto) { connectPromise.set_value(true); },
[&connectPromise](auto, std::string_view msg) {
std::cout << "Failed to connect to lokinet RPC: " << msg << std::endl;
@ -188,28 +202,36 @@ main(int argc, char* argv[])
if (options.killDaemon)
{
if (not OMQ_Request(omq, connectionID, "llarp.halt"))
auto maybe_halt = OMQ_Request(omq, connectionID, "llarp.halt");
if (not maybe_halt)
return exit_error("Call to llarp.halt failed");
return 0;
if (auto err_it = maybe_halt->find("error");
err_it != maybe_halt->end() and not err_it.value().is_null())
{
return exit_error("{}", err_it.value());
}
}
if (options.printStatus)
{
const auto maybe_status = OMQ_Request(omq, connectionID, "llarp.status");
if (not maybe_status)
return exit_error("call to llarp.status failed");
return exit_error("Call to llarp.status failed");
try
{
const auto& ep = maybe_status->at("result").at("services").at(options.endpoint);
const auto exitMap = ep.at("exitMap");
if (exitMap.empty())
const auto& ep = maybe_status->at("result").at("services").at(options.endpoint).at("exitMap");
if (ep.empty())
{
std::cout << "no exits" << std::endl;
std::cout << "No exits found" << std::endl;
}
else
{
for (const auto& [range, exit] : exitMap.items())
for (const auto& [range, exit] : ep.items())
{
std::cout << range << " via " << exit.get<std::string>() << std::endl;
}
@ -217,20 +239,37 @@ main(int argc, char* argv[])
}
catch (std::exception& ex)
{
return exit_error("failed to parse result: {}", ex.what());
return exit_error("Failed to parse result: {}", ex.what());
}
return 0;
}
if (not options.swapExits.empty())
{
nlohmann::json opts{{"exit_addresses", std::move(options.swapExits)}};
auto maybe_swap = OMQ_Request(omq, connectionID, "llarp.swap_exits", std::move(opts));
if (not maybe_swap)
return exit_error("Failed to swap exit node connections");
if (auto err_it = maybe_swap->find("error");
err_it != maybe_swap->end() and not err_it.value().is_null())
{
return exit_error("{}", err_it.value());
}
}
if (options.vpnUp)
{
nlohmann::json opts{{"exit", options.exitAddress}, {"token", options.token}};
nlohmann::json opts{{"address", options.exitAddress}, {"token", options.token}};
if (options.range)
opts["range"] = *options.range;
opts["ip_range"] = *options.range;
auto maybe_result = OMQ_Request(omq, connectionID, "llarp.exit", std::move(opts));
auto maybe_result = OMQ_Request(omq, connectionID, "llarp.map_exit", std::move(opts));
if (not maybe_result)
return exit_error("could not add exit");
return exit_error("Could not add exit");
if (auto err_it = maybe_result->find("error");
err_it != maybe_result->end() and not err_it.value().is_null())
@ -240,11 +279,20 @@ main(int argc, char* argv[])
}
if (options.vpnDown)
{
nlohmann::json opts{{"unmap", true}};
nlohmann::json opts{{"unmap_exit", true}};
if (options.range)
opts["range"] = *options.range;
if (not OMQ_Request(omq, connectionID, "llarp.exit", std::move(opts)))
return exit_error("failed to unmap exit");
opts["ip_range"] = *options.range;
auto maybe_down = OMQ_Request(omq, connectionID, "llarp.unmap_exit", std::move(opts));
if (not maybe_down)
return exit_error("Failed to unmap exit node connection");
if (auto err_it = maybe_down->find("error");
err_it != maybe_down->end() and not err_it.value().is_null())
{
return exit_error("{}", err_it.value());
}
}
return 0;

@ -1 +1 @@
Subproject commit 12c17d6eab754908cd88f05d09b9388381e47515
Subproject commit 9f2323a2db5fc54fe8394892769eff859967f735

View File

@ -246,7 +246,10 @@ add_library(lokinet-context
# lokinet-rpc holds all rpc related compilation units
add_library(lokinet-rpc
STATIC
rpc/json_binary_proxy.cpp
rpc/json_conversions.cpp
rpc/lokid_rpc_client.cpp
rpc/rpc_request_parser.cpp
rpc/rpc_server.cpp
rpc/endpoint_rpc.cpp
)

View File

@ -9,6 +9,7 @@
#include <list>
#include <iostream>
#include <cassert>
#include <stdexcept>
namespace llarp
{
@ -30,6 +31,14 @@ namespace llarp
return Parse();
}
bool
ConfigParser::LoadNewFromStr(std::string_view str)
{
m_Data.resize(str.size());
std::copy(str.begin(), str.end(), m_Data.begin());
return ParseAll();
}
bool
ConfigParser::LoadFromStr(std::string_view str)
{
@ -52,6 +61,78 @@ namespace llarp
return std::isspace(static_cast<unsigned char>(ch)) != 0;
}
/// Differs from Parse() as ParseAll() does NOT skip comments
/// ParseAll() is only used by RPC endpoint 'config' for
/// reading new .ini files from string and writing them
bool
ConfigParser::ParseAll()
{
std::list<std::string_view> lines;
{
auto itr = m_Data.begin();
// split into lines
while (itr != m_Data.end())
{
auto beg = itr;
while (itr != m_Data.end() && *itr != '\n' && *itr != '\r')
++itr;
lines.emplace_back(std::addressof(*beg), std::distance(beg, itr));
if (itr == m_Data.end())
break;
++itr;
}
}
std::string_view sectName;
size_t lineno = 0;
for (auto line : lines)
{
lineno++;
// Trim whitespace
while (!line.empty() && whitespace(line.front()))
line.remove_prefix(1);
while (!line.empty() && whitespace(line.back()))
line.remove_suffix(1);
// Skip blank lines but NOT comments
if (line.empty())
continue;
if (line.front() == '[' && line.back() == ']')
{
// section header
line.remove_prefix(1);
line.remove_suffix(1);
sectName = line;
}
else if (auto kvDelim = line.find('='); kvDelim != std::string_view::npos)
{
// key value pair
std::string_view k = line.substr(0, kvDelim);
std::string_view v = line.substr(kvDelim + 1);
// Trim inner whitespace
while (!k.empty() && whitespace(k.back()))
k.remove_suffix(1);
while (!v.empty() && whitespace(v.front()))
v.remove_prefix(1);
if (k.empty())
{
throw std::runtime_error(
fmt::format("{} invalid line ({}): '{}'", m_FileName, lineno, line));
}
LogDebug(m_FileName, ": [", sectName, "]:", k, "=", v);
m_Config[std::string{sectName}].emplace(k, v);
}
else // malformed?
{
throw std::runtime_error(
fmt::format("{} invalid line ({}): '{}'", m_FileName, lineno, line));
}
}
return true;
}
bool
ConfigParser::Parse()
{
@ -82,7 +163,7 @@ namespace llarp
while (!line.empty() && whitespace(line.back()))
line.remove_suffix(1);
// Skip blank lines and comments
// Skip blank lines
if (line.empty() or line.front() == ';' or line.front() == '#')
continue;
@ -106,16 +187,16 @@ namespace llarp
if (k.empty())
{
LogError(m_FileName, " invalid line (", lineno, "): '", line, "'");
return false;
throw std::runtime_error(
fmt::format("{} invalid line ({}): '{}'", m_FileName, lineno, line));
}
LogDebug(m_FileName, ": [", sectName, "]:", k, "=", v);
m_Config[std::string{sectName}].emplace(k, v);
}
else // malformed?
{
LogError(m_FileName, " invalid line (", lineno, "): '", line, "'");
return false;
throw std::runtime_error(
fmt::format("{} invalid line ({}): '{}'", m_FileName, lineno, line));
}
}
return true;
@ -168,4 +249,31 @@ namespace llarp
m_Overrides.clear();
}
void
ConfigParser::SaveNew() const
{
if (not m_Overrides.empty())
{
throw std::invalid_argument("Override specified when attempting new .ini save");
}
if (m_Config.empty())
{
throw std::invalid_argument("New config not loaded when attempting new .ini save");
}
if (m_FileName.empty())
{
throw std::invalid_argument("New config cannot be saved with filepath specified");
}
std::ofstream ofs(m_FileName);
for (const auto& [section, values] : m_Config)
{
ofs << std::endl << "[" << section << "]" << std::endl;
for (const auto& [key, value] : values)
{
ofs << key << "=" << value << std::endl;
}
}
}
} // namespace llarp

View File

@ -24,6 +24,12 @@ namespace llarp
bool
LoadFile(const fs::path& fname);
/// load new .ini file from string (calls ParseAll() rather than Parse())
/// return true on success
/// return false on error
bool
LoadNewFromStr(std::string_view str);
/// load from string
/// return true on success
/// return false on error
@ -47,6 +53,10 @@ namespace llarp
void
Save();
/// save new .ini config file to path
void
SaveNew() const;
inline void
Filename(fs::path f)
{
@ -54,6 +64,9 @@ namespace llarp
};
private:
bool
ParseAll();
bool
Parse();

View File

@ -25,10 +25,10 @@
#include <pthread_np.h>
#endif
static auto logcat = llarp::log::Cat("llarp-context");
namespace llarp
{
static auto logcat = llarp::log::Cat("llarp-context");
bool
Context::CallSafe(std::function<void(void)> f)
{

View File

@ -8,6 +8,7 @@
#include <list>
#include <optional>
#include <stdexcept>
#include <string>
namespace llarp
@ -24,6 +25,12 @@ namespace llarp
: addr{std::move(address)}, netmask_bits{std::move(netmask)}
{}
explicit IPRange(std::string _range)
{
if (not FromString(_range))
throw std::invalid_argument{"IP string '{}' cannot be parsed as IP range"_format(_range)};
}
static constexpr IPRange
V4MappedRange()
{
@ -40,7 +47,8 @@ namespace llarp
FromIPv4(net::ipv4addr_t addr, net::ipv4addr_t netmask)
{
return IPRange{
net::ExpandV4(ToHost(addr)), netmask_ipv6_bits(bits::count_bits(netmask) + 96)};
net::ExpandV4(llarp::net::ToHost(addr)),
netmask_ipv6_bits(bits::count_bits(netmask) + 96)};
}
/// return true if this iprange is in the IPv4 mapping range for containing ipv4 addresses
@ -103,7 +111,7 @@ namespace llarp
inline bool
Contains(const net::ipaddr_t& ip) const
{
return var::visit([this](auto&& ip) { return Contains(ToHost(ip)); }, ip);
return var::visit([this](auto&& ip) { return Contains(llarp::net::ToHost(ip)); }, ip);
}
/// get the highest address on this range

View File

@ -2,6 +2,7 @@
#include "ip_range.hpp"
#include <llarp/util/status.hpp>
#include <set>
#include <vector>
namespace llarp

View File

@ -623,8 +623,7 @@ namespace llarp
}
if (IsServiceNode())
return SaveRC();
else
return true;
return true;
}
bool
@ -1256,7 +1255,7 @@ namespace llarp
Router::StartRpcServer()
{
if (m_Config->api.m_enableRPCServer)
m_RPCServer = std::make_unique<rpc::RpcServer>(m_lmq, this);
m_RPCServer = std::make_unique<rpc::RPCServer>(m_lmq, *this);
return true;
}

View File

@ -298,7 +298,7 @@ namespace llarp
void
PumpLL();
std::unique_ptr<rpc::RpcServer> m_RPCServer;
std::unique_ptr<rpc::RPCServer> m_RPCServer;
const llarp_time_t _randomStartDelay;

View File

@ -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

View File

@ -0,0 +1,158 @@
#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

46
llarp/rpc/json_bt.hpp Normal file
View File

@ -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

View File

@ -0,0 +1,18 @@
#include "json_conversions.hpp"
#include <nlohmann/json.hpp>
namespace llarp
{
void
to_json(nlohmann::json& j, const IPRange& ipr)
{
j = ipr.ToString();
}
void
from_json(const nlohmann::json& j, IPRange& ipr)
{
ipr = IPRange{j.get<std::string>()};
}
} // namespace llarp

View File

@ -0,0 +1,37 @@
#pragma once
#include <llarp/net/ip_range.hpp>
#include <nlohmann/json_fwd.hpp>
#include "json_binary_proxy.hpp"
namespace llarp
{
void
to_json(nlohmann::json& j, const IPRange& ipr);
void
from_json(const nlohmann::json& j, IPRange& ipr);
} // namespace llarp
namespace nlohmann
{
// 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.
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(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

369
llarp/rpc/param_parser.hpp Normal file
View File

@ -0,0 +1,369 @@
#pragma once
#include "json_binary_proxy.hpp"
#include "json_bt.hpp"
#include "json_conversions.hpp"
#include <oxenc/bt_serialize.h>
#include <nlohmann/json.hpp>
#include <stdexcept>
#include <string>
#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;
// Types that are constructible from string
template <typename T>
constexpr bool is_string_constructible = false;
template <>
inline constexpr bool is_string_constructible<IPRange> = 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& target)
{
if constexpr (std::is_integral_v<T>)
target = c.template consume_integer<T>();
else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>)
target = c.consume_string_view();
else if constexpr (is_string_constructible<T>)
target = T{c.consume_string()};
else if constexpr (llarp::rpc::json_is_binary<T>)
llarp::rpc::load_binary_parameter(c.consume_string_view(), true /*allow raw*/, target);
else if constexpr (is_expandable_list<T>)
{
auto lc = c.consume_list_consumer();
target.clear();
while (!lc.is_finished())
load_value(lc, target.emplace_back());
}
else if constexpr (is_tuple_like<T>)
{
auto lc = c.consume_list_consumer();
load_tuple_values(lc, target, std::make_index_sequence<std::tuple_size_v<T>>{});
}
else if constexpr (is_unordered_string_map<T>)
{
auto dc = c.consume_dict_consumer();
target.clear();
while (!dc.is_finished())
load_value(dc, target[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& range_itr, T& target)
{
auto& key = range_itr.first.key();
auto& current = *range_itr.first; // value currently pointed to by range_itr.first
if constexpr (std::is_same_v<T, bool>)
{
if (current.is_boolean())
target = current.get<bool>();
else if (current.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 = current.get<uint64_t>();
if (b <= 1)
target = 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 (!current.is_number_unsigned())
throw std::domain_error{"Invalid value for '" + key + "': non-negative value required"};
auto i = current.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"};
target = i;
}
else if constexpr (std::is_integral_v<T>)
{
if (!current.is_number_integer())
throw std::domain_error{"Invalid value for '" + key + "': value is not an integer"};
auto i = current.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"};
}
target = i;
}
else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>)
{
target = current.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
{
current.get_to(target);
}
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");
}
++range_itr.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

71
llarp/rpc/rpc_request.hpp Normal file
View File

@ -0,0 +1,71 @@
#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(nlohmann::json{
{"error",
"Bad Request: RPC requests must have at most one data part (received {})"_format(
m.data.size())}}
.dump());
return;
}
// 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(nlohmann::json{{"Failed to parse request parameters: "s + e.what()}}.dump());
return;
}
if (not std::is_base_of_v<Immediate, RPC>)
{
server.m_Router.loop()->call_soon(std::move(handler));
}
else
{
handler();
}
};
}
} // namespace llarp::rpc

View File

@ -0,0 +1,128 @@
#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 sent when the `invoke`
// method returns. If the response needs to happen later (i.e. not immediately after `invoke`
// returns) then you should call `defer()` to extract and clear this and then send the response
// via the returned DeferredSend object yourself.
std::optional<oxenmq::Message::DeferredSend> replier;
// Called to clear the current replier and return it. After this call the automatic reply will
// not be generated; the caller is responsible for calling `->reply` on the returned optional
// itself. This is typically used where a call has to be deferred, for example because it
// depends on some network response to build the reply.
oxenmq::Message::DeferredSend
move()
{
auto r{std::move(*replier)};
replier.reset();
return r;
}
};
// 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

View File

@ -0,0 +1,311 @@
#pragma once
#include "rpc_request_decorators.hpp"
#include "llarp/net/ip_range.hpp"
#include "llarp/router/abstractrouter.hpp"
#include "llarp/router/route_poker.hpp"
#include "llarp/service/address.hpp"
#include "llarp/service/endpoint.hpp"
#include "llarp/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>
#include <vector>
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: quic_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: map_exit
// Map a new connection to an exit node
//
// Inputs:
// "address" : ID of endpoint to map
// "range" : IP range to map to exit node
// "token" : auth token
//
// Returns:
//
struct MapExit : RPCRequest
{
MapExit()
{
if constexpr (platform::supports_ipv6)
request.ip_range.emplace_back("::/0");
else
request.ip_range.emplace_back("0.0.0.0/0");
}
static constexpr auto name = "map_exit"sv;
struct request_parameters
{
std::string address;
std::vector<IPRange> ip_range;
std::string token;
} request;
};
// RPC: list_exits
// List all currently mapped exit node connections
//
// Inputs: none
//
// Returns:
//
struct ListExits : NoArgs
{
static constexpr auto name = "list_exits"sv;
};
// RPC: unmap_exit
// Unmap a connection to an exit node
//
// Inputs:
// "endpoint" : ID of endpoint to map
// "range" : IP range to map to exit node
// "token" : auth token
//
// Returns:
//
struct UnmapExit : RPCRequest
{
UnmapExit()
{
if constexpr (platform::supports_ipv6)
request.ip_range.emplace_back("::/0");
else
request.ip_range.emplace_back("0.0.0.0/0");
}
static constexpr auto name = "unmap_exit"sv;
struct request_parameters
{
std::vector<IPRange> ip_range;
} request;
};
// RPC: swap_exit
// Swap a connection from one exit to another
//
// Inputs:
// "exits" : exit nodes to swap mappings from (index 0 = old exit, index 1 = new exit)
//
// Returns:
//
struct SwapExits : RPCRequest
{
static constexpr auto name = "swap_exits"sv;
struct request_parameters
{
std::vector<std::string> exit_addresses;
std::string token;
} request;
};
// RPC: dns_query
// Attempts to query endpoint by domain name
//
// 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,
MapExit,
ListExits,
SwapExits,
UnmapExit,
DNSQuery,
Config>;
} // namespace llarp::rpc

View File

@ -0,0 +1,110 @@
#include "rpc_request_parser.hpp"
#include "llarp/rpc/rpc_request_definitions.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(MapExit& mapexit, rpc_input input)
{
get_values(
input,
"address",
mapexit.request.address,
"ip_range",
mapexit.request.ip_range,
"token",
mapexit.request.token);
}
void
parse_request(UnmapExit& unmapexit, rpc_input input)
{
get_values(input, "ip_range", unmapexit.request.ip_range);
}
void
parse_request(SwapExits& swapexits, rpc_input input)
{
get_values(
input,
"exit_addresses",
swapexits.request.exit_addresses,
"token",
swapexits.request.token);
}
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

View File

@ -0,0 +1,35 @@
#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(MapExit& mapexit, rpc_input input);
void
parse_request(UnmapExit& unmapexit, rpc_input input);
void
parse_request(SwapExits& swapexits, 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

View File

@ -1,35 +1,165 @@
#pragma once
#include "rpc_request_definitions.hpp"
#include "json_bt.hpp"
#include <nlohmann/json_fwd.hpp>
#include <stdexcept>
#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;
struct RpcServer
class 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, 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>
void
SetJSONResponse(Result_t result, json& j)
{
j["result"] = result;
}
inline void
SetJSONError(std::string_view msg, json& j)
{
j["error"] = msg;
}
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(MapExit& mapexit);
void
invoke(ListExits& listexits);
void
invoke(UnmapExit& unmapexit);
void
invoke(SwapExits& swapexits);
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());
SetJSONError(
fmt::format("RPC request 'rpc.{}' failed with: {}", rpc.name, e.what()), rpc.response);
}
catch (const std::exception& e)
{
log::info(logcat, "RPC request 'rpc.{}' raised an exception: {}", rpc.name, e.what());
SetJSONError(
fmt::format("RPC request 'rpc.{}' raised an exception: {}", rpc.name, e.what()),
rpc.response);
}
if (rpc.replier.has_value())
{
rpc.send_response();
}
}
};
} // namespace llarp::rpc

View File

@ -1,15 +1,16 @@
#include <chrono>
#include <memory>
#include "endpoint.hpp"
#include "endpoint_state.hpp"
#include "endpoint_util.hpp"
#include "hidden_service_address_lookup.hpp"
#include "auth.hpp"
#include "llarp/util/logging.hpp"
#include "outbound_context.hpp"
#include "protocol.hpp"
#include "info.hpp"
#include "protocol_type.hpp"
#include <llarp/net/ip.hpp>
#include <llarp/net/ip_range.hpp>
#include <llarp/dht/context.hpp>
#include <llarp/dht/key.hpp>
#include <llarp/dht/messages/findintro.hpp>
@ -22,6 +23,7 @@
#include <llarp/nodedb.hpp>
#include <llarp/profiling.hpp>
#include <llarp/router/abstractrouter.hpp>
#include <llarp/router/route_poker.hpp>
#include <llarp/routing/dht_message.hpp>
#include <llarp/routing/path_transfer_message.hpp>
@ -35,6 +37,7 @@
#include <llarp/util/priority_queue.hpp>
#include <optional>
#include <type_traits>
#include <utility>
#include <uvw.hpp>
#include <variant>
@ -215,6 +218,75 @@ namespace llarp
return std::nullopt;
}
void
Endpoint::map_exit(
std::string name,
std::string token,
std::vector<IPRange> ranges,
std::function<void(bool, std::string)> result_handler)
{
if (ranges.empty())
{
result_handler(false, "no ranges provided");
return;
}
LookupNameAsync(
name,
[ptr = std::static_pointer_cast<Endpoint>(GetSelf()),
name,
auth = AuthInfo{token},
ranges,
result_handler,
poker = m_router->routePoker()](auto maybe_addr) {
if (not maybe_addr)
{
result_handler(false, "exit not found: {}"_format(name));
return;
}
if (auto* addr_ptr = std::get_if<Address>(&*maybe_addr))
{
Address addr{*addr_ptr};
ptr->SetAuthInfoForEndpoint(addr, auth);
ptr->MarkAddressOutbound(addr);
auto result = ptr->EnsurePathToService(
addr,
[ptr, name, ranges, result_handler, poker](auto addr, auto* ctx) {
if (ctx == nullptr)
{
result_handler(false, "could not establish flow to {}"_format(name));
return;
}
// make a lambda that sends the reply after doing auth
auto apply_result =
[ptr, poker, addr, result_handler, ranges](AuthResult result) {
if (result.code != AuthResultCode::eAuthAccepted)
{
result_handler(false, result.reason);
return;
}
for (const auto& range : ranges)
ptr->MapExitRange(range, addr);
if (poker)
poker->Up();
result_handler(true, result.reason);
};
ctx->AsyncSendAuth(apply_result);
},
ptr->PathAlignmentTimeout());
if (not result)
result_handler(false, "did not build path to {}"_format(name));
}
else
result_handler(false, "exit via snode not supported");
});
}
void
Endpoint::LookupServiceAsync(
std::string name,
@ -2086,6 +2158,11 @@ namespace llarp
void
Endpoint::SetAuthInfoForEndpoint(Address addr, AuthInfo info)
{
if (info.token.empty())
{
m_RemoteAuthInfos.erase(addr);
return;
}
m_RemoteAuthInfos[addr] = std::move(info);
}
@ -2112,6 +2189,26 @@ namespace llarp
LogInfo(Name(), " unmap ", item.first, " exit range mapping");
return true;
});
if (m_ExitMap.Empty())
m_router->routePoker()->Down();
}
void
Endpoint::UnmapRangeByExit(IPRange range, std::string exit)
{
// unmap all ranges that match the given exit when hot swapping
m_ExitMap.RemoveIf([&](const auto& item) -> bool {
if ((range.Contains(item.first)) and (item.second.ToString() == exit))
{
log::info(logcat, "{} unmap {} range mapping to exit node {}", Name(), item.first, exit);
return true;
}
return false;
});
if (m_ExitMap.Empty())
m_router->routePoker()->Down();
}
std::optional<AuthInfo>

View File

@ -284,6 +284,16 @@ namespace llarp
void
UnmapExitRange(IPRange range);
void
UnmapRangeByExit(IPRange range, std::string exit);
void
map_exit(
std::string name,
std::string token,
std::vector<IPRange> ranges,
std::function<void(bool, std::string)> result);
void
PutLookup(IServiceLookup* lookup, uint64_t txid) override;

View File

@ -125,8 +125,7 @@ namespace llarp
void
SendContext::AsyncSendAuth(std::function<void(AuthResult)> resultHandler)
{
const auto maybe = m_Endpoint->MaybeGetAuthInfoForEndpoint(remoteIdent.Addr());
if (maybe.has_value())
if (const auto maybe = m_Endpoint->MaybeGetAuthInfoForEndpoint(remoteIdent.Addr()))
{
// send auth message
const llarp_buffer_t authdata{maybe->token};
@ -134,7 +133,7 @@ namespace llarp
authResultListener = resultHandler;
}
else
resultHandler({AuthResultCode::eAuthFailed, "no auth for given endpoint"});
resultHandler({AuthResultCode::eAuthAccepted, "no auth needed"});
}
void

View File

@ -42,12 +42,12 @@ TEST_CASE("ConfigParser", "[config]")
SECTION("No key")
{
REQUIRE_FALSE(parser.LoadFromStr("[test]\n=1090\n"));
REQUIRE_THROWS(parser.LoadFromStr("[test]\n=1090\n"));
}
SECTION("Parse invalid")
{
REQUIRE_FALSE(
REQUIRE_THROWS(
parser.LoadFromStr("srged5ghe5\nf34wtge5\nw34tgfs4ygsd5yg=4;\n#"
"g4syhgd5\n"));
}