diff --git a/contrib/omq-rpc.py b/contrib/omq-rpc.py new file mode 100755 index 000000000..2f2a8cded --- /dev/null +++ b/contrib/omq-rpc.py @@ -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 diff --git a/daemon/lokinet-vpn.cpp b/daemon/lokinet-vpn.cpp index c1c78f4d1..083ba8b91 100644 --- a/daemon/lokinet-vpn.cpp +++ b/daemon/lokinet-vpn.cpp @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include +#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 range; + std::vector 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 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::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; diff --git a/external/oxen-logging b/external/oxen-logging index 12c17d6ea..9f2323a2d 160000 --- a/external/oxen-logging +++ b/external/oxen-logging @@ -1 +1 @@ -Subproject commit 12c17d6eab754908cd88f05d09b9388381e47515 +Subproject commit 9f2323a2db5fc54fe8394892769eff859967f735 diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 643c485ec..6653a5f08 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -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 ) diff --git a/llarp/config/ini.cpp b/llarp/config/ini.cpp index 72cff9f61..06be65b56 100644 --- a/llarp/config/ini.cpp +++ b/llarp/config/ini.cpp @@ -9,6 +9,7 @@ #include #include #include +#include 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(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 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 diff --git a/llarp/config/ini.hpp b/llarp/config/ini.hpp index 847e77c94..d32761393 100644 --- a/llarp/config/ini.hpp +++ b/llarp/config/ini.hpp @@ -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(); diff --git a/llarp/context.cpp b/llarp/context.cpp index b8652a256..e38a1adcd 100644 --- a/llarp/context.cpp +++ b/llarp/context.cpp @@ -25,10 +25,10 @@ #include #endif -static auto logcat = llarp::log::Cat("llarp-context"); - namespace llarp { + static auto logcat = llarp::log::Cat("llarp-context"); + bool Context::CallSafe(std::function f) { diff --git a/llarp/net/ip_range.hpp b/llarp/net/ip_range.hpp index a4e9b0be3..f085048b2 100644 --- a/llarp/net/ip_range.hpp +++ b/llarp/net/ip_range.hpp @@ -8,6 +8,7 @@ #include #include +#include #include 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 diff --git a/llarp/net/ip_range_map.hpp b/llarp/net/ip_range_map.hpp index bf659ae78..d38bcf83e 100644 --- a/llarp/net/ip_range_map.hpp +++ b/llarp/net/ip_range_map.hpp @@ -2,6 +2,7 @@ #include "ip_range.hpp" #include +#include #include namespace llarp diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 38de414e0..cc9e0e735 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -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(m_lmq, this); + m_RPCServer = std::make_unique(m_lmq, *this); return true; } diff --git a/llarp/router/router.hpp b/llarp/router/router.hpp index a5dd11a66..16efb1fcc 100644 --- a/llarp/router/router.hpp +++ b/llarp/router/router.hpp @@ -298,7 +298,7 @@ namespace llarp void PumpLL(); - std::unique_ptr m_RPCServer; + std::unique_ptr m_RPCServer; const llarp_time_t _randomStartDelay; diff --git a/llarp/rpc/json_binary_proxy.cpp b/llarp/rpc/json_binary_proxy.cpp new file mode 100644 index 000000000..cc766dc22 --- /dev/null +++ b/llarp/rpc/json_binary_proxy.cpp @@ -0,0 +1,63 @@ +#include "json_binary_proxy.hpp" +#include +#include + +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 diff --git a/llarp/rpc/json_binary_proxy.hpp b/llarp/rpc/json_binary_proxy.hpp new file mode 100644 index 000000000..dc4800dfb --- /dev/null +++ b/llarp/rpc/json_binary_proxy.hpp @@ -0,0 +1,158 @@ +#pragma once + +#include +#include +#include + +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 + inline constexpr bool json_is_binary = false; + + template + inline constexpr bool json_is_binary_container = false; + template + inline constexpr bool json_is_binary_container> = json_is_binary; + template + inline constexpr bool json_is_binary_container> = json_is_binary; + + // De-referencing wrappers around the above: + template + inline constexpr bool json_is_binary = json_is_binary; + template + inline constexpr bool json_is_binary = json_is_binary; + template + inline constexpr bool json_is_binary_container = json_is_binary_container; + template + inline constexpr bool json_is_binary_container = json_is_binary_container; + + 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 >> + void + load_binary_parameter(std::string_view bytes, bool allow_raw, T& val) + { + load_binary_parameter_impl(bytes, sizeof(T), allow_raw, reinterpret_cast(&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 + json_binary_proxy + operator[](T&& key) + { + return json_binary_proxy{e[std::forward(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, int> = 0> + nlohmann::json& + operator=(std::basic_string_view binary_data) + { + return *this = std::string_view{ + reinterpret_cast(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 , int> = 0> + nlohmann::json& + operator=(const T& val) + { + return *this = std::string_view{reinterpret_cast(&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 , 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 + json_binary_proxy + emplace_back(Args&&... args) + { + return json_binary_proxy{e.emplace_back(std::forward(args)...), format}; + } + + /// Adds an element to an underlying list, then copies or moves the given argument onto it via + /// json_binary_proxy assignment. + template + void + push_back(T&& val) + { + emplace_back() = std::forward(val); + } + }; + +} // namespace llarp::rpc diff --git a/llarp/rpc/json_bt.hpp b/llarp/rpc/json_bt.hpp new file mode 100644 index 000000000..0a87f99a0 --- /dev/null +++ b/llarp/rpc/json_bt.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +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()); + } + if (j.is_boolean()) + return j.get() ? 1 : 0; + if (j.is_number_unsigned()) + return j.get(); + if (j.is_number_integer()) + return j.get(); + throw std::domain_error{ + "internal error: encountered some unhandled/invalid type in json-to-bt translation"}; + } + +} // namespace llarp::rpc diff --git a/llarp/rpc/json_conversions.cpp b/llarp/rpc/json_conversions.cpp new file mode 100644 index 000000000..d3e036468 --- /dev/null +++ b/llarp/rpc/json_conversions.cpp @@ -0,0 +1,18 @@ +#include "json_conversions.hpp" +#include + +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()}; + } + +} // namespace llarp diff --git a/llarp/rpc/json_conversions.hpp b/llarp/rpc/json_conversions.hpp new file mode 100644 index 000000000..95b9d547c --- /dev/null +++ b/llarp/rpc/json_conversions.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#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 + struct adl_serializer>> + { + static_assert(std::is_trivially_copyable_v && std::has_unique_object_representations_v); + + 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(), false /*no raw*/, val); + } + }; + +} // namespace nlohmann diff --git a/llarp/rpc/param_parser.hpp b/llarp/rpc/param_parser.hpp new file mode 100644 index 000000000..64779979c --- /dev/null +++ b/llarp/rpc/param_parser.hpp @@ -0,0 +1,369 @@ +#pragma once + +#include "json_binary_proxy.hpp" +#include "json_bt.hpp" +#include "json_conversions.hpp" +#include +#include +#include +#include +#include +#include + +namespace llarp::rpc +{ + using json_range = std::pair; + using rpc_input = std::variant; + + // Checks that key names are given in ascending order + template + 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 + struct required + { + T& value; + required(T& ref) : value{ref} + {} + }; + template + constexpr bool is_required_wrapper = false; + template + constexpr bool is_required_wrapper> = true; + + template + constexpr bool is_std_optional = false; + template + constexpr bool is_std_optional> = 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 + 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().empty()); + } + }; + + template + constexpr bool is_ignore_empty_string_wrapper = false; + template + constexpr bool is_ignore_empty_string_wrapper> = 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 + constexpr bool is_expandable_list = false; + template + constexpr bool is_expandable_list> = true; + + // Types that are constructible from string + template + constexpr bool is_string_constructible = false; + template <> + inline constexpr bool is_string_constructible = true; + + // Fixed size elements: tuples, pairs, and std::array's; we accept list input as long as the + // list length matches exactly. + template + constexpr bool is_tuple_like = false; + template + constexpr bool is_tuple_like> = true; + template + constexpr bool is_tuple_like> = true; + template + constexpr bool is_tuple_like> = true; + + // True if T is a `std::unordered_map` + template + constexpr bool is_unordered_string_map = false; + template + constexpr bool is_unordered_string_map> = true; + + template + void + load_tuple_values(oxenc::bt_list_consumer&, TupleLike&, std::index_sequence); + + // 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, + int> = 0> + void + load_value(BTConsumer& c, T& target) + { + if constexpr (std::is_integral_v) + target = c.template consume_integer(); + else if constexpr (std::is_same_v || std::is_same_v) + target = c.consume_string_view(); + else if constexpr (is_string_constructible) + target = T{c.consume_string()}; + else if constexpr (llarp::rpc::json_is_binary) + llarp::rpc::load_binary_parameter(c.consume_string_view(), true /*allow raw*/, target); + else if constexpr (is_expandable_list) + { + auto lc = c.consume_list_consumer(); + target.clear(); + while (!lc.is_finished()) + load_value(lc, target.emplace_back()); + } + else if constexpr (is_tuple_like) + { + auto lc = c.consume_list_consumer(); + load_tuple_values(lc, target, std::make_index_sequence>{}); + } + else if constexpr (is_unordered_string_map) + { + 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, "Unsupported load_value type"); + } + + // Copies the next value from the json range into `val`, and advances the iterator. Throws + // on unconvertible values. + template + 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) + { + if (current.is_boolean()) + target = current.get(); + 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(); + 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) + { + if (!current.is_number_unsigned()) + throw std::domain_error{"Invalid value for '" + key + "': non-negative value required"}; + auto i = current.get(); + if (sizeof(T) < sizeof(uint64_t) && i > std::numeric_limits::max()) + throw std::domain_error{"Invalid value for '" + key + "': value too large"}; + target = i; + } + else if constexpr (std::is_integral_v) + { + if (!current.is_number_integer()) + throw std::domain_error{"Invalid value for '" + key + "': value is not an integer"}; + auto i = current.get(); + if (sizeof(T) < sizeof(int64_t)) + { + if (i < std::numeric_limits::lowest()) + throw std::domain_error{ + "Invalid value for '" + key + "': negative value magnitude is too large"}; + if (i > std::numeric_limits::max()) + throw std::domain_error{"Invalid value for '" + key + "': value is too large"}; + } + target = i; + } + else if constexpr (std::is_same_v || std::is_same_v) + { + target = current.get(); + } + else if constexpr ( + llarp::rpc::json_is_binary< + T> || is_expandable_list || is_tuple_like || is_unordered_string_map) + { + 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, "Unsupported load type"); + } + ++range_itr.first; + } + + template + void + load_tuple_values(oxenc::bt_list_consumer& c, TupleLike& val, std::index_sequence) + { + (load_value(c, std::get(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 + void + load_curr_value(In& in, T& val) + { + if constexpr (is_required_wrapper) + { + load_curr_value(in, val.value); + } + else if constexpr (is_ignore_empty_string_wrapper) + { + if (!val.should_ignore(in)) + load_curr_value(in, val.value); + } + else if constexpr (is_std_optional) + { + 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 + void + get_next_value(In& in, [[maybe_unused]] std::string_view name, T& val) + { + if constexpr (std::is_same_v) + ; + else if (skip_until(in, name)) + load_curr_value(in, val); + else if constexpr (is_required_wrapper) + 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 + void + get_values(Input& in, std::string_view name, T&& val, More&&... more) + { + if constexpr (std::is_same_v) + { + if (auto* json_in = std::get_if(&in)) + { + json_range r{json_in->cbegin(), json_in->cend()}; + get_values(r, name, val, std::forward(more)...); + } + else if (auto* dict = std::get_if(&in)) + { + get_values(*dict, name, val, std::forward(more)...); + } + else + { + // A monostate indicates that no parameters field was provided at all + get_values(var::get(in), name, val, std::forward(more)...); + } + } + else if constexpr (std::is_same_v) + { + if (in.front() == 'd') + { + oxenc::bt_dict_consumer d{in}; + get_values(d, name, val, std::forward(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)...); + } + } + else + { + static_assert( + std::is_same_v< + json_range, + Input> || std::is_same_v || std::is_same_v); + get_next_value(in, name, val); + if constexpr (sizeof...(More) > 0) + { + check_ascending_names(name, more...); + get_values(in, std::forward(more)...); + } + } + } +} // namespace llarp::rpc \ No newline at end of file diff --git a/llarp/rpc/rpc_request.hpp b/llarp/rpc/rpc_request.hpp new file mode 100644 index 000000000..caf50d9f3 --- /dev/null +++ b/llarp/rpc/rpc_request.hpp @@ -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 +#include +#include +#include +#include +#include + +namespace llarp::rpc +{ + using nlohmann::json; + + template + auto + make_invoke() + { + return [](oxenmq::Message& m, RPCServer& server) { + EndpointHandler 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) + { + server.m_Router.loop()->call_soon(std::move(handler)); + } + else + { + handler(); + } + }; + } + +} // namespace llarp::rpc diff --git a/llarp/rpc/rpc_request_decorators.hpp b/llarp/rpc/rpc_request_decorators.hpp new file mode 100644 index 000000000..5973c06dc --- /dev/null +++ b/llarp/rpc/rpc_request_decorators.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include "json_binary_proxy.hpp" +#include "json_bt.hpp" +#include +#include +#include +#include +#include +#include + +namespace tools +{ + // Type wrapper that contains an arbitrary list of types. + template + 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 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 diff --git a/llarp/rpc/rpc_request_definitions.hpp b/llarp/rpc/rpc_request_definitions.hpp new file mode 100644 index 000000000..565f34572 --- /dev/null +++ b/llarp/rpc/rpc_request_definitions.hpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 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 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 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 diff --git a/llarp/rpc/rpc_request_parser.cpp b/llarp/rpc/rpc_request_parser.cpp new file mode 100644 index 000000000..53eee05ae --- /dev/null +++ b/llarp/rpc/rpc_request_parser.cpp @@ -0,0 +1,110 @@ +#include "rpc_request_parser.hpp" +#include "llarp/rpc/rpc_request_definitions.hpp" +#include "param_parser.hpp" +#include +#include +#include +#include +#include + +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 \ No newline at end of file diff --git a/llarp/rpc/rpc_request_parser.hpp b/llarp/rpc/rpc_request_parser.hpp new file mode 100644 index 000000000..e28c12855 --- /dev/null +++ b/llarp/rpc/rpc_request_parser.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "rpc_request_definitions.hpp" +#include +#include +#include +#include +#include + +namespace llarp::rpc +{ + using rpc_input = std::variant; + + 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 \ No newline at end of file diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index 954c6d7fe..746557f63 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -1,6 +1,12 @@ #include "rpc_server.hpp" +#include "llarp/rpc/rpc_request_definitions.hpp" +#include "rpc_request.hpp" +#include "llarp/service/address.hpp" +#include +#include #include #include +#include #include #include #include @@ -13,64 +19,12 @@ #include #include #include +#include #include -namespace -{ - static auto logcat = llarp::log::Cat("lokinet.rpc"); -} // namespace - namespace llarp::rpc { - RpcServer::RpcServer(LMQ_ptr lmq, AbstractRouter* r) - : m_LMQ{std::move(lmq)}, m_Router{r}, log_subs{*m_LMQ, llarp::logRingBuffer} - { - for (const auto& addr : r->GetConfig()->api.m_rpcBindAddresses) - { - m_LMQ->listen_plain(addr.zmq_address()); - LogInfo("Bound RPC server to ", addr.full_address()); - } - - this->AddRPCCategories(); - } - - /// maybe parse json from message paramter at index - std::optional - MaybeParseJSON(const oxenmq::Message& msg, size_t index = 0) - { - try - { - const auto& str = msg.data.at(index); - return nlohmann::json::parse(str); - } - catch (std::exception&) - { - return std::nullopt; - } - } - - template - std::string - CreateJSONResponse(Result_t result) - { - const auto obj = nlohmann::json{ - {"error", nullptr}, - {"result", result}, - }; - return obj.dump(); - } - - std::string - CreateJSONError(std::string_view msg) - { - const auto obj = nlohmann::json{ - {"error", msg}, - }; - return obj.dump(); - } - - /// fake packet source that serializes repsonses back into dns - + // Fake packet source that serializes repsonses back into dns class DummyPacketSource : public dns::PacketSource_Base { std::function)> func; @@ -107,626 +61,530 @@ namespace llarp::rpc } }; - /// a function that replies to an rpc request - using ReplyFunction_t = std::function; + bool + check_path(std::string path) + { + for (auto c : path) + { + if (not((c >= '0' and c <= '9') or (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') + or (c == '_') or (c == '-'))) + { + return false; + } + } + + return true; + } std::shared_ptr - GetEndpointByName(AbstractRouter* r, std::string name) + GetEndpointByName(AbstractRouter& r, std::string name) { - if (r->IsServiceNode()) + if (r.IsServiceNode()) { - return r->exitContext().GetExitEndpoint(name); + return r.exitContext().GetExitEndpoint(name); } - else + + return r.hiddenServiceContext().GetEndpointByName(name); + } + + template + void + register_rpc_command(std::unordered_map& regs) + { + static_assert(std::is_base_of_v); + rpc_callback cback{}; + + cback.invoke = make_invoke(); + + regs.emplace(RPC::name, std::move(cback)); + } + + RPCServer::RPCServer(LMQ_ptr lmq, AbstractRouter& r) + : m_LMQ{std::move(lmq)}, m_Router(r), log_subs{*m_LMQ, llarp::logRingBuffer} + { + // copied logic loop as placeholder + for (const auto& addr : r.GetConfig()->api.m_rpcBindAddresses) { - return r->hiddenServiceContext().GetEndpointByName(name); + m_LMQ->listen_plain(addr.zmq_address()); + LogInfo("Bound RPC server to ", addr.full_address()); } + + AddCategories(); + } + + template + std::unordered_map + register_rpc_requests(tools::type_list) + { + std::unordered_map regs; + + (register_rpc_command(regs), ...); + + return regs; } + const std::unordered_map rpc_request_map = + register_rpc_requests(rpc::rpc_request_types{}); + void - HandleJSONRequest( - oxenmq::Message& msg, std::function handleRequest) + RPCServer::AddCategories() { - const auto maybe = MaybeParseJSON(msg); - if (not maybe.has_value()) + m_LMQ->add_category("llarp", oxenmq::AuthLevel::none) + .add_request_command("logs", [this](oxenmq::Message& msg) { HandleLogsSubRequest(msg); }); + + for (auto& req : rpc_request_map) { - msg.send_reply(CreateJSONError("failed to parse json")); + m_LMQ->add_request_command( + "llarp", + req.first, + [name = std::string_view{req.first}, &call = req.second, this](oxenmq::Message& m) { + call.invoke(m, *this); + }); + } + } + + void + RPCServer::invoke(Halt& halt) + { + if (not m_Router.IsRunning()) + { + SetJSONError("Router is not running", halt.response); return; } - if (not maybe->is_object()) + SetJSONResponse("OK", halt.response); + m_Router.Stop(); + } + + void + RPCServer::invoke(Version& version) + { + util::StatusObject result{ + {"version", llarp::VERSION_FULL}, {"uptime", to_json(m_Router.Uptime())}}; + + SetJSONResponse(result, version.response); + } + + void + RPCServer::invoke(Status& status) + { + (m_Router.IsRunning()) ? SetJSONResponse(m_Router.ExtractStatus(), status.response) + : SetJSONError("Router is not yet ready", status.response); + } + + void + RPCServer::invoke(GetStatus& getstatus) + { + SetJSONResponse(m_Router.ExtractSummaryStatus(), getstatus.response); + } + + void + RPCServer::invoke(QuicConnect& quicconnect) + { + if (quicconnect.request.port == 0 and quicconnect.request.closeID == 0) + { + SetJSONError("Port not provided", quicconnect.response); + return; + } + + if (quicconnect.request.remoteHost.empty() and quicconnect.request.closeID == 0) { - msg.send_reply(CreateJSONError("request data not a json object")); + SetJSONError("Host not provided", quicconnect.response); return; } + + auto endpoint = (quicconnect.request.endpoint.empty()) + ? GetEndpointByName(m_Router, "default") + : GetEndpointByName(m_Router, quicconnect.request.endpoint); + + if (not endpoint) + { + SetJSONError("No such local endpoint found.", quicconnect.response); + return; + } + + auto quic = endpoint->GetQUICTunnel(); + + if (not quic) + { + SetJSONError( + "No quic interface available on endpoint " + quicconnect.request.endpoint, + quicconnect.response); + return; + } + + if (quicconnect.request.closeID) + { + quic->forget(quicconnect.request.closeID); + SetJSONResponse("OK", quicconnect.response); + return; + } + + SockAddr laddr{quicconnect.request.bindAddr}; + try { - handleRequest( - *maybe, [defer = msg.send_later()](std::string result) { defer.reply(result); }); + auto [addr, id] = quic->open( + quicconnect.request.remoteHost, quicconnect.request.port, [](auto&&) {}, laddr); + + util::StatusObject status; + status["addr"] = addr.ToString(); + status["id"] = id; + + SetJSONResponse(status, quicconnect.response); } - catch (std::exception& ex) + catch (std::exception& e) { - msg.send_reply(CreateJSONError(ex.what())); + SetJSONError(e.what(), quicconnect.response); } } void - RpcServer::AddRPCCategories() + RPCServer::invoke(QuicListener& quiclistener) { - m_LMQ->add_category("llarp", oxenmq::AuthLevel::none) - .add_request_command("logs", [this](oxenmq::Message& msg) { HandleLogsSubRequest(msg); }) - .add_request_command( - "halt", - [&](oxenmq::Message& msg) { - if (not m_Router->IsRunning()) - { - msg.send_reply(CreateJSONError("router is not running")); - return; - } - msg.send_reply(CreateJSONResponse("OK")); - m_Router->Stop(); - }) - .add_request_command( - "version", - [r = m_Router](oxenmq::Message& msg) { - util::StatusObject result{ - {"version", llarp::VERSION_FULL}, {"uptime", to_json(r->Uptime())}}; - msg.send_reply(CreateJSONResponse(result)); - }) - .add_request_command( - "status", - [&](oxenmq::Message& msg) { - m_Router->loop()->call([defer = msg.send_later(), r = m_Router]() { - std::string data; - if (r->IsRunning()) - { - data = CreateJSONResponse(r->ExtractStatus()); - } - else - { - data = CreateJSONError("router not yet ready"); - } - defer.reply(data); - }); - }) - .add_request_command( - "get_status", - [&](oxenmq::Message& msg) { - m_Router->loop()->call([defer = msg.send_later(), r = m_Router]() { - defer.reply(CreateJSONResponse(r->ExtractSummaryStatus())); - }); - }) - .add_request_command( - "quic_connect", - [&](oxenmq::Message& msg) { - HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { - std::string endpoint = "default"; - if (auto itr = obj.find("endpoint"); itr != obj.end()) - endpoint = itr->get(); - - std::string bindAddr = "127.0.0.1:0"; - if (auto itr = obj.find("bind"); itr != obj.end()) - bindAddr = itr->get(); - - std::string remoteHost; - if (auto itr = obj.find("host"); itr != obj.end()) - remoteHost = itr->get(); - - uint16_t port = 0; - if (auto itr = obj.find("port"); itr != obj.end()) - port = itr->get(); - - int closeID = 0; - if (auto itr = obj.find("close"); itr != obj.end()) - closeID = itr->get(); - - if (port == 0 and closeID == 0) - { - reply(CreateJSONError("port not provided")); - return; - } - if (remoteHost.empty() and closeID == 0) - { - reply(CreateJSONError("host not provided")); - return; - } - SockAddr laddr{}; - laddr.fromString(bindAddr); - - r->loop()->call([reply, endpoint, r, remoteHost, port, closeID, laddr]() { - auto ep = GetEndpointByName(r, endpoint); - if (not ep) - { - reply(CreateJSONError("no such local endpoint")); - return; - } - auto quic = ep->GetQUICTunnel(); - if (not quic) - { - reply(CreateJSONError("local endpoint has no quic tunnel")); - return; - } - if (closeID) - { - quic->close(closeID); - reply(CreateJSONResponse("OK")); - return; - } - - try - { - auto [addr, id] = quic->open( - remoteHost, port, [](auto&&) {}, laddr); - util::StatusObject status; - status["addr"] = addr.ToString(); - status["id"] = id; - reply(CreateJSONResponse(status)); - } - catch (std::exception& ex) - { - reply(CreateJSONError(ex.what())); - } - }); - }); - }) - .add_request_command( - "quic_listener", - [&](oxenmq::Message& msg) { - HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { - std::string endpoint = "default"; - if (auto itr = obj.find("endpoint"); itr != obj.end()) - endpoint = itr->get(); - - std::string remote = "127.0.0.1"; - if (auto itr = obj.find("host"); itr != obj.end()) - remote = itr->get(); - - uint16_t port = 0; - if (auto itr = obj.find("port"); itr != obj.end()) - port = itr->get(); - - int closeID = 0; - if (auto itr = obj.find("close"); itr != obj.end()) - closeID = itr->get(); - - std::string srvProto; - if (auto itr = obj.find("srv-proto"); itr != obj.end()) - srvProto = itr->get(); - - if (port == 0 and closeID == 0) - { - reply(CreateJSONError("invalid arguments")); - return; - } - r->loop()->call([reply, endpoint, remote, port, r, closeID, srvProto]() { - auto ep = GetEndpointByName(r, endpoint); - if (not ep) - { - reply(CreateJSONError("no such local endpoint")); - return; - } - auto quic = ep->GetQUICTunnel(); - if (not quic) - { - reply(CreateJSONError("no quic interface available on endpoint " + endpoint)); - return; - } - if (port) - { - int id = 0; - try - { - SockAddr addr{remote + ":" + std::to_string(port)}; - id = quic->listen(addr); - } - catch (std::exception& ex) - { - reply(CreateJSONError(ex.what())); - return; - } - util::StatusObject result; - result["id"] = id; - std::string localAddress; - var::visit( - [&](auto&& addr) { localAddress = addr.ToString(); }, ep->LocalAddress()); - result["addr"] = localAddress + ":" + std::to_string(port); - if (not srvProto.empty()) - { - auto srvData = - dns::SRVData::fromTuple(std::make_tuple(srvProto, 1, 1, port, "")); - ep->PutSRVRecord(std::move(srvData)); - } - reply(CreateJSONResponse(result)); - } - else if (closeID) - { - quic->forget(closeID); - reply(CreateJSONResponse("OK")); - } - }); - }); - }) - .add_request_command( - "lookup_snode", - [&](oxenmq::Message& msg) { - HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { - if (not r->IsServiceNode()) - { - reply(CreateJSONError("not supported")); - return; - } - RouterID routerID; - if (auto itr = obj.find("snode"); itr != obj.end()) - { - std::string remote = itr->get(); - if (not routerID.FromString(remote)) - { - reply(CreateJSONError("invalid remote: " + remote)); - return; - } - } - else - { - reply(CreateJSONError("no remote provided")); - return; - } - std::string endpoint = "default"; - r->loop()->call([r, endpoint, routerID, reply]() { - auto ep = r->exitContext().GetExitEndpoint(endpoint); - if (ep == nullptr) - { - reply(CreateJSONError("cannot find local endpoint: " + endpoint)); - return; - } - ep->ObtainSNodeSession(routerID, [routerID, ep, reply](auto session) { - if (session and session->IsReady()) - { - const auto ip = net::TruncateV6(ep->GetIPForIdent(PubKey{routerID})); - util::StatusObject status{{"ip", ip.ToString()}}; - reply(CreateJSONResponse(status)); - } - else - reply(CreateJSONError("failed to obtain snode session")); - }); - }); - }); - }) - .add_request_command( - "endpoint", - [&](oxenmq::Message& msg) { - HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { - if (r->IsServiceNode()) - { - reply(CreateJSONError("not supported")); - return; - } - std::string endpoint = "default"; - std::unordered_set kills; - { - const auto itr = obj.find("endpoint"); - if (itr != obj.end()) - endpoint = itr->get(); - } - { - const auto itr = obj.find("kill"); - if (itr != obj.end()) - { - if (itr->is_array()) - { - for (auto kill_itr = itr->begin(); kill_itr != itr->end(); ++kill_itr) - { - if (kill_itr->is_string()) - kills.emplace(kill_itr->get()); - } - } - else if (itr->is_string()) - { - kills.emplace(itr->get()); - } - } - } - if (kills.empty()) - { - reply(CreateJSONError("no action taken")); - return; - } - r->loop()->call([r, endpoint, kills, reply]() { - auto ep = r->hiddenServiceContext().GetEndpointByName(endpoint); - if (ep == nullptr) - { - reply(CreateJSONError("no endpoint with name " + endpoint)); - return; - } - std::size_t removed = 0; - for (auto kill : kills) - { - removed += ep->RemoveAllConvoTagsFor(std::move(kill)); - } - reply(CreateJSONResponse( - "removed " + std::to_string(removed) + " flow" + (removed == 1 ? "" : "s"))); - }); - }); - }) - .add_request_command( - "exit", - [&](oxenmq::Message& msg) { - HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { - if (r->IsServiceNode()) - { - reply(CreateJSONError("not supported")); - return; - } - std::optional exit; - std::optional lnsExit; - IPRange range; - bool map = true; - const auto exit_itr = obj.find("exit"); - if (exit_itr != obj.end()) - { - service::Address addr; - const auto exit_str = exit_itr->get(); - if (service::NameIsValid(exit_str) or exit_str == "null") - { - lnsExit = exit_str; - } - else if (not addr.FromString(exit_str)) - { - reply(CreateJSONError("invalid exit address")); - return; - } - else - { - exit = addr; - } - } - - const auto unmap_itr = obj.find("unmap"); - if (unmap_itr != obj.end() and unmap_itr->get()) - { - map = false; - } - const auto range_itr = obj.find("range"); - if (range_itr == obj.end() or range_itr->is_null()) - { - // platforms without ipv6 support will shit themselves - // here if we give them an exit mapping that is ipv6 - if constexpr (platform::supports_ipv6) - { - range.FromString("::/0"); - } - else - { - range.FromString("0.0.0.0/0"); - } - } - else if (not range.FromString(range_itr->get())) - { - reply(CreateJSONError("invalid ip range")); - return; - } - if (not platform::supports_ipv6 and not range.IsV4()) - { - reply(CreateJSONError("ipv6 ranges not supported on this platform")); - return; - } - std::string token; - const auto token_itr = obj.find("token"); - if (token_itr != obj.end() and not token_itr->is_null()) - { - token = token_itr->get(); - } - - std::string endpoint = "default"; - const auto endpoint_itr = obj.find("endpoint"); - if (endpoint_itr != obj.end()) - { - endpoint = endpoint_itr->get(); - } - r->loop()->call([map, exit, lnsExit, range, token, endpoint, r, reply]() mutable { - auto ep = r->hiddenServiceContext().GetEndpointByName(endpoint); - if (ep == nullptr) - { - reply(CreateJSONError("no endpoint with name " + endpoint)); - return; - } - if (map and (exit.has_value() or lnsExit.has_value())) - { - auto mapExit = [=](service::Address addr) mutable { - ep->MapExitRange(range, addr); - - bool shouldSendAuth = false; - if (not token.empty()) - { - shouldSendAuth = true; - ep->SetAuthInfoForEndpoint(*exit, service::AuthInfo{token}); - } - auto onGoodResult = [r, reply](std::string reason) { - if (r->HasClientExit()) - reply(CreateJSONResponse(reason)); - else - reply(CreateJSONError("we dont have an exit?")); - }; - auto onBadResult = [r, reply, ep, range](std::string reason) { - r->routePoker()->Down(); - ep->UnmapExitRange(range); - reply(CreateJSONError(reason)); - }; - if (addr.IsZero()) - { - onGoodResult("added null exit"); - return; - } - ep->MarkAddressOutbound(addr); - ep->EnsurePathToService( - addr, - [onBadResult, onGoodResult, shouldSendAuth, addrStr = addr.ToString()]( - auto, service::OutboundContext* ctx) { - if (ctx == nullptr) - { - onBadResult("could not find exit"); - return; - } - if (not shouldSendAuth) - { - onGoodResult("OK: connected to " + addrStr); - return; - } - ctx->AsyncSendAuth( - [onGoodResult, onBadResult](service::AuthResult result) { - // TODO: refactor this code. We are 5 lambdas deep here! - if (result.code != service::AuthResultCode::eAuthAccepted) - { - onBadResult(result.reason); - return; - } - onGoodResult(result.reason); - }); - }); - }; - if (exit.has_value()) - { - mapExit(*exit); - } - else if (lnsExit.has_value()) - { - const std::string name = *lnsExit; - if (name == "null") - { - service::Address nullAddr{}; - mapExit(nullAddr); - return; - } - ep->LookupNameAsync(name, [reply, mapExit](auto maybe) mutable { - if (not maybe.has_value()) - { - reply(CreateJSONError("we could not find an exit with that name")); - return; - } - if (auto ptr = std::get_if(&*maybe)) - { - if (ptr->IsZero()) - reply(CreateJSONError("name does not exist")); - else - mapExit(*ptr); - } - else - { - reply(CreateJSONError("lns name resolved to a snode")); - } - }); - } - else - { - reply( - CreateJSONError("WTF inconsistent request, no exit address or lns " - "name provided?")); - } - return; - } - else if (map and not exit.has_value()) - { - reply(CreateJSONError("no exit address provided")); - return; - } - else if (not map) - { - r->routePoker()->Down(); - ep->UnmapExitRange(range); - reply(CreateJSONResponse("OK")); - } - }); - }); - }) - .add_request_command( - "dns_query", - [&](oxenmq::Message& msg) { - HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { - std::string endpoint{"default"}; - if (const auto itr = obj.find("endpoint"); itr != obj.end()) - { - endpoint = itr->get(); - } - std::string qname{}; - dns::QType_t qtype = dns::qTypeA; - if (const auto itr = obj.find("qname"); itr != obj.end()) - { - qname = itr->get(); - } - - if (const auto itr = obj.find("qtype"); itr != obj.end()) - { - qtype = itr->get(); - } - - dns::Message msg{dns::Question{qname, qtype}}; - - if (auto ep_ptr = GetEndpointByName(r, endpoint)) - { - if (auto dns = ep_ptr->DNS()) - { - auto src = std::make_shared([reply](auto result) { - if (result) - reply(CreateJSONResponse(result->ToJSON())); - else - reply(CreateJSONError("no response from dns")); - }); - if (not dns->MaybeHandlePacket(src, src->dumb, src->dumb, msg.ToBuffer())) - { - reply(CreateJSONError("dns query not accepted by endpoint")); - } - } - else - reply(CreateJSONError("endpoint does not have dns")); - return; - } - reply(CreateJSONError("no such endpoint for dns query")); - }); - }) - .add_request_command("config", [&](oxenmq::Message& msg) { - HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { + if (quiclistener.request.port == 0 and quiclistener.request.closeID == 0) + { + SetJSONError("Invalid arguments", quiclistener.response); + return; + } + + auto endpoint = (quiclistener.request.endpoint.empty()) + ? GetEndpointByName(m_Router, "default") + : GetEndpointByName(m_Router, quiclistener.request.endpoint); + + if (not endpoint) + { + SetJSONError("No such local endpoint found", quiclistener.response); + return; + } + + auto quic = endpoint->GetQUICTunnel(); + + if (not quic) + { + SetJSONError( + "No quic interface available on endpoint " + quiclistener.request.endpoint, + quiclistener.response); + return; + } + + if (quiclistener.request.closeID) + { + quic->forget(quiclistener.request.closeID); + SetJSONResponse("OK", quiclistener.response); + return; + } + + if (quiclistener.request.port) + { + auto id = 0; + try + { + SockAddr addr{quiclistener.request.remoteHost, huint16_t{quiclistener.request.port}}; + id = quic->listen(addr); + } + catch (std::exception& e) + { + SetJSONError(e.what(), quiclistener.response); + return; + } + + util::StatusObject result; + result["id"] = id; + std::string localAddress; + var::visit([&](auto&& addr) { localAddress = addr.ToString(); }, endpoint->LocalAddress()); + result["addr"] = localAddress + ":" + std::to_string(quiclistener.request.port); + + if (not quiclistener.request.srvProto.empty()) + { + auto srvData = dns::SRVData::fromTuple( + std::make_tuple(quiclistener.request.srvProto, 1, 1, quiclistener.request.port, "")); + endpoint->PutSRVRecord(std::move(srvData)); + } + + SetJSONResponse(result, quiclistener.response); + return; + } + } + + // TODO: fix this because it's bad + void + RPCServer::invoke(LookupSnode& lookupsnode) + { + if (not m_Router.IsServiceNode()) + { + SetJSONError("Not supported", lookupsnode.response); + return; + } + + RouterID routerID; + if (lookupsnode.request.routerID.empty()) + { + SetJSONError("No remote ID provided", lookupsnode.response); + return; + } + + if (not routerID.FromString(lookupsnode.request.routerID)) + { + SetJSONError("Invalid remote: " + lookupsnode.request.routerID, lookupsnode.response); + return; + } + + m_Router.loop()->call([&]() { + auto endpoint = m_Router.exitContext().GetExitEndpoint("default"); + + if (endpoint == nullptr) + { + SetJSONError("Cannot find local endpoint: default", lookupsnode.response); + return; + } + + endpoint->ObtainSNodeSession(routerID, [&](auto session) { + if (session and session->IsReady()) + { + const auto ip = net::TruncateV6(endpoint->GetIPForIdent(PubKey{routerID})); + util::StatusObject status{{"ip", ip.ToString()}}; + SetJSONResponse(status, lookupsnode.response); + return; + } + + SetJSONError("Failed to obtain snode session", lookupsnode.response); + return; + }); + }); + } + + void + RPCServer::invoke(MapExit& mapexit) + { + MapExit exit_request; + // steal replier from exit RPC endpoint + exit_request.replier.emplace(mapexit.move()); + + m_Router.hiddenServiceContext().GetDefault()->map_exit( + mapexit.request.address, + mapexit.request.token, + mapexit.request.ip_range, + [exit = std::move(exit_request)](bool success, std::string result) mutable { + if (success) + exit.send_response({{"result"}, std::move(result)}); + else + exit.send_response({{"error"}, std::move(result)}); + }); + } + + void + RPCServer::invoke(ListExits& listexits) + { + if (not m_Router.hiddenServiceContext().hasEndpoints()) + { + SetJSONError("No mapped endpoints found", listexits.response); + return; + } + + auto status = m_Router.hiddenServiceContext().GetDefault()->ExtractStatus()["exitMap"]; + + SetJSONResponse((status.empty()) ? "No exits" : status, listexits.response); + } + + void + RPCServer::invoke(UnmapExit& unmapexit) + { + try + { + for (auto& ip : unmapexit.request.ip_range) + m_Router.hiddenServiceContext().GetDefault()->UnmapExitRange(ip); + } + catch (std::exception& e) + { + SetJSONError("Unable to unmap to given range", unmapexit.response); + return; + } + + SetJSONResponse("OK", unmapexit.response); + } + + // Sequentially calls map_exit and unmap_exit to hotswap mapped connection from old exit + // to new exit. Similar to how map_exit steals the oxenmq deferredsend object, swapexit + // moves the replier object to the unmap_exit struct, as that is called second. Rather than + // the nested lambda within map_exit making the reply call, it instead calls the unmap_exit logic + // and leaves the message handling to the unmap_exit struct + void + RPCServer::invoke(SwapExits& swapexits) + { + MapExit map_request; + UnmapExit unmap_request; + auto endpoint = m_Router.hiddenServiceContext().GetDefault(); + auto current_exits = endpoint->ExtractStatus()["exitMap"]; + + if (current_exits.empty()) + { + SetJSONError("Cannot swap to new exit: no exits currently mapped", swapexits.response); + return; + } + + if (swapexits.request.exit_addresses.size() < 2) + { + SetJSONError("Exit addresses not passed", swapexits.response); + return; + } + + // steal replier from swapexit RPC endpoint + unmap_request.replier.emplace(swapexits.move()); + + // set map_exit request to new address + map_request.request.address = swapexits.request.exit_addresses[1]; + + // set token for new exit node mapping + if (not swapexits.request.token.empty()) + map_request.request.token = swapexits.request.token; + + // populate map_exit request with old IP ranges + for (auto& [range, exit] : current_exits.items()) + { + if (exit.get() == swapexits.request.exit_addresses[0]) + { + map_request.request.ip_range.emplace_back(range); + unmap_request.request.ip_range.emplace_back(range); + } + } + + if (map_request.request.ip_range.empty() or unmap_request.request.ip_range.empty()) + { + SetJSONError("No mapped ranges found matching requested swap", swapexits.response); + return; + } + + endpoint->map_exit( + map_request.request.address, + map_request.request.token, + map_request.request.ip_range, + [unmap = std::move(unmap_request), + ep = endpoint, + old_exit = swapexits.request.exit_addresses[0]](bool success, std::string result) mutable { + if (not success) + unmap.send_response({{"error"}, std::move(result)}); + else + { + try { - const auto itr = obj.find("override"); - if (itr != obj.end()) - { - if (not itr->is_object()) - { - reply(CreateJSONError("override is not an object")); - return; - } - for (const auto& [section, value] : itr->items()) - { - if (not value.is_object()) - { - reply(CreateJSONError( - fmt::format("failed to set [{}]: section is not an object", section))); - return; - } - for (const auto& [key, value] : value.items()) - { - if (not value.is_string()) - { - reply(CreateJSONError(fmt::format( - "failed to set [{}]:{}: value is not a string", section, key))); - return; - } - r->GetConfig()->Override(section, key, value.get()); - } - } - } + for (auto& ip : unmap.request.ip_range) + ep->UnmapRangeByExit(ip, old_exit); } + catch (std::exception& e) { - const auto itr = obj.find("reload"); - if (itr != obj.end() and itr->get()) - { - r->QueueDiskIO([conf = r->GetConfig()]() { conf->Save(); }); - } + SetJSONError("Unable to unmap to given range", unmap.response); + return; } - reply(CreateJSONResponse("OK")); - }); + + SetJSONResponse("OK", unmap.response); + unmap.send_response(); + } }); } void - RpcServer::HandleLogsSubRequest(oxenmq::Message& m) + RPCServer::invoke(DNSQuery& dnsquery) + { + std::string qname = (dnsquery.request.qname.empty()) ? "" : dnsquery.request.qname; + dns::QType_t qtype = (dnsquery.request.qtype) ? dnsquery.request.qtype : dns::qTypeA; + + dns::Message msg{dns::Question{qname, qtype}}; + + auto endpoint = (dnsquery.request.endpoint.empty()) + ? GetEndpointByName(m_Router, "default") + : GetEndpointByName(m_Router, dnsquery.request.endpoint); + + if (endpoint == nullptr) + { + SetJSONError("No such endpoint found for dns query", dnsquery.response); + return; + } + + if (auto dns = endpoint->DNS()) + { + auto packet_src = std::make_shared([&](auto result) { + if (result) + SetJSONResponse(result->ToJSON(), dnsquery.response); + else + SetJSONError("No response from DNS", dnsquery.response); + }); + if (not dns->MaybeHandlePacket( + packet_src, packet_src->dumb, packet_src->dumb, msg.ToBuffer())) + SetJSONError("DNS query not accepted by endpoint", dnsquery.response); + } + else + SetJSONError("Endpoint does not have dns", dnsquery.response); + return; + } + + void + RPCServer::invoke(Config& config) + { + if (config.request.filename.empty() and not config.request.ini.empty()) + { + SetJSONError("No filename specified for .ini file", config.response); + return; + } + if (config.request.ini.empty() and not config.request.filename.empty()) + { + SetJSONError("No .ini chunk provided", config.response); + return; + } + + if (not ends_with(config.request.filename, ".ini")) + { + SetJSONError("Must append '.ini' to filename", config.response); + return; + } + + if (not check_path(config.request.filename)) + { + SetJSONError("Bad filename passed", config.response); + return; + } + + fs::path conf_d{"conf.d"}; + + if (config.request.del and not config.request.filename.empty()) + { + try + { + if (fs::exists(conf_d / (config.request.filename))) + fs::remove(conf_d / (config.request.filename)); + } + catch (std::exception& e) + { + SetJSONError(e.what(), config.response); + return; + } + } + else + { + try + { + if (not fs::exists(conf_d)) + fs::create_directory(conf_d); + + auto parser = ConfigParser(); + + if (parser.LoadNewFromStr(config.request.ini)) + { + parser.Filename(conf_d / (config.request.filename)); + parser.SaveNew(); + } + } + catch (std::exception& e) + { + SetJSONError(e.what(), config.response); + return; + } + } + + SetJSONResponse("OK", config.response); + } + + void + RPCServer::HandleLogsSubRequest(oxenmq::Message& m) { if (m.data.size() != 1) { diff --git a/llarp/rpc/rpc_server.hpp b/llarp/rpc/rpc_server.hpp index 02c44202f..bda5d6888 100644 --- a/llarp/rpc/rpc_server.hpp +++ b/llarp/rpc/rpc_server.hpp @@ -1,35 +1,165 @@ #pragma once +#include "rpc_request_definitions.hpp" +#include "json_bt.hpp" +#include +#include #include #include #include +#include #include #include 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; + 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; + // 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 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 + 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 + 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 diff --git a/llarp/service/endpoint.cpp b/llarp/service/endpoint.cpp index 7768c8685..89c24bc6b 100644 --- a/llarp/service/endpoint.cpp +++ b/llarp/service/endpoint.cpp @@ -1,15 +1,16 @@ -#include -#include #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 +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include #include @@ -35,6 +37,7 @@ #include #include +#include #include #include #include @@ -215,6 +218,75 @@ namespace llarp return std::nullopt; } + void + Endpoint::map_exit( + std::string name, + std::string token, + std::vector ranges, + std::function result_handler) + { + if (ranges.empty()) + { + result_handler(false, "no ranges provided"); + return; + } + + LookupNameAsync( + name, + [ptr = std::static_pointer_cast(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
(&*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 diff --git a/llarp/service/endpoint.hpp b/llarp/service/endpoint.hpp index 619f58fdd..46fd42e7b 100644 --- a/llarp/service/endpoint.hpp +++ b/llarp/service/endpoint.hpp @@ -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 ranges, + std::function result); + void PutLookup(IServiceLookup* lookup, uint64_t txid) override; diff --git a/llarp/service/sendcontext.cpp b/llarp/service/sendcontext.cpp index 20fb744a0..0629ba3d0 100644 --- a/llarp/service/sendcontext.cpp +++ b/llarp/service/sendcontext.cpp @@ -125,8 +125,7 @@ namespace llarp void SendContext::AsyncSendAuth(std::function 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 diff --git a/test/config/test_llarp_config_ini.cpp b/test/config/test_llarp_config_ini.cpp index b876e0d20..b0b79eb91 100644 --- a/test/config/test_llarp_config_ini.cpp +++ b/test/config/test_llarp_config_ini.cpp @@ -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")); }