#include "client_rpc_endpoints.h" #include "request_handler.h" #include #include #include #include #include #include #include #include #include #include #include namespace oxen::rpc { using nlohmann::json; using oxenc::bt_dict; using oxenc::bt_dict_consumer; using oxenc::bt_list; using oxenc::bt_value; using std::chrono::system_clock; namespace { template constexpr bool is_timestamp = std::is_same_v; template constexpr bool is_str_array = std::is_same_v>; template constexpr bool is_int_array = std::is_same_v>; template constexpr bool is_namespace_var = std::is_same_v; template constexpr std::string_view type_desc = std::is_same_v ? "boolean"sv : std::is_unsigned_v ? "positive integer"sv : std::is_integral_v ? "integer"sv : is_namespace_var ? "integer or \"all\""sv : std::is_same_v ? "16-bit integer"sv : is_timestamp ? "integer timestamp (in milliseconds)"sv : is_str_array ? "string array"sv : is_int_array ? "integer array"sv : "string"sv; template constexpr bool is_parseable_v = std::is_unsigned_v || std::is_integral_v || is_timestamp || is_str_array || is_int_array || is_namespace_var || std::is_same_v || std::is_same_v || std::is_same_v; // Extracts a field suitable for a `T` value from the given json with name `name`. Takes // the json params and the name. Throws if it encounters an invalid value (i.e. expecting a // number but given a bool). Returns nullopt if the field isn't present or is present and // set to null. template std::optional parse_field(const json& params, const char* name) { static_assert(is_parseable_v); auto it = params.find(name); if (it == params.end() || it->is_null()) return std::nullopt; bool right_type = std::is_same_v ? it->is_boolean() : std::is_unsigned_v || is_timestamp ? it->is_number_unsigned() : std::is_integral_v || std::is_same_v ? it->is_number_integer() : is_namespace_var ? it->is_number_integer() || it->is_string() : is_str_array || is_int_array ? it->is_array() : it->is_string(); if (is_str_array && right_type) { for (auto& x : *it) if (!x.is_string()) right_type = false; } else if (is_int_array && right_type) { for (auto& x : *it) if (!x.is_number_integer()) right_type = false; } if (!right_type) throw parse_error{ fmt::format("Invalid value given for '{}': expected {}", name, type_desc)}; if constexpr (std::is_same_v) return it->template get_ref(); else if constexpr (is_timestamp) { auto time = from_epoch_ms(it->template get()); // If we get a small timestamp value (less than 1M seconds since epoch) then this // was very likely given as unix epoch seconds rather than milliseconds if (time.time_since_epoch() < 1'000'000s) throw parse_error{fmt::format( "Invalid timestamp for '{}': timestamp must be in milliseconds", name)}; return time; } else if constexpr (is_namespace_var || std::is_same_v) { if (it->is_number_integer()) { int64_t id = it->get(); if (id < NAMESPACE_MIN || id > NAMESPACE_MAX) throw parse_error{ fmt::format("Invalid value given for '{}': value out of range", name)}; return namespace_id{static_cast>(id)}; } if constexpr (is_namespace_var) if (it->is_string() && it->get_ref() == "all") return namespace_all; throw parse_error{ fmt::format("Invalid value given for '{}': expected integer or \"all\"", name)}; } else { return it->template get(); } } // Equivalent to the above, but for a bt_dict_consumer. Note that this advances the // current state of the bt_dict_consumer to just after the given field and so this // *must* be called in sorted key order. template std::optional parse_field(bt_dict_consumer& params, const char* name) { static_assert(is_parseable_v); if (!params.skip_until(name)) return std::nullopt; try { if constexpr (std::is_same_v) return params.consume_string_view(); else if constexpr (std::is_same_v) return params.consume_string(); else if constexpr (std::is_integral_v) return params.consume_integer(); else if constexpr (is_timestamp) return from_epoch_ms(params.consume_integer()); else if constexpr (is_str_array || is_int_array) { auto elems = std::make_optional(); for (auto l = params.consume_list_consumer(); !l.is_finished();) if constexpr (is_str_array) elems->push_back(l.consume_string()); else elems->push_back(l.consume_integer()); return elems; } else if constexpr (is_namespace_var || std::is_same_v) { if (params.is_integer()) return namespace_id{ params.consume_integer>()}; if constexpr (is_namespace_var) if (params.is_string() && params.consume_string_view() == "all"sv) return namespace_all; } } catch (...) { } throw parse_error{ fmt::format("Invalid value given for '{}': expected {}", name, type_desc)}; } // Backwards compat code for fields like ttl and timestamp that are accepted either // as integer *or* stringified integer. template >> std::optional parse_stringified(const json& params, const char* name) { if (auto it = params.find(name); it != params.end() && it->is_string()) { if (T value; util::parse_int(it->get_ref(), value)) return value; else throw parse_error{ fmt::format("Invalid value given for '{}': {}", name, it->dump())}; } return parse_field(params, name); } #ifndef NDEBUG constexpr bool check_ascending(std::string_view) { return true; } template constexpr bool check_ascending(std::string_view a, std::string_view b, Args&&... args) { return a < b && check_ascending(b, std::forward(args)...); } #endif // Loads fields from a bt_dict_consumer or a json object. Names must be specified // in alphabetical order. Throws a parse_error if the field exists but cannot be // converted into a `T`. template < typename... T, typename Dict, typename... Names, typename = std::enable_if_t< sizeof...(T) == sizeof...(Names) && (std::is_convertible_v && ...)>> std::tuple...> load_fields(Dict& params, const Names&... names) { assert(check_ascending(names...)); return {parse_field(params, names)...}; } template void require(std::string_view name, const std::optional& v) { if (!v) throw parse_error{fmt::format("Required field '{}' missing", name)}; } template void require(std::string_view name, const std::variant& v) { if (v.index() == 0) throw parse_error{fmt::format("Required field '{}' missing", name)}; } template void require_at_most_one_of( std::string_view first, const std::optional& a, std::string_view second, const std::optional& b) { if (a && b) throw parse_error{fmt::format("Cannot specify both '{}' and '{}'", first, second)}; } template void require_exactly_one_of( std::string_view first, const std::optional& a, std::string_view second, const std::optional& b, bool alias = false) { require_at_most_one_of(first, a, second, b); if (!(a || b)) throw parse_error{fmt::format( alias ? "Required field '{}' missing" : "Required field '{}' or '{}' missing", first, second)}; } template void load_pk(RPC& rpc, std::optional& pk) { require("pubkey", pk); if (!rpc.pubkey.load(std::move(*pk))) throw parse_error{fmt::format( "Pubkey must be {} hex digits ({} bytes) long", USER_PUBKEY_SIZE_HEX, USER_PUBKEY_SIZE_BYTES)}; } template constexpr bool is_std_optional = false; template constexpr bool is_std_optional> = true; // Parses (but does not verify) a required request signature value. template void load_pk_signature( RPC& rpc, const Dict&, std::optional& pk, const std::optional& pk_ed, const std::optional& sig) { load_pk(rpc, pk); require("signature", sig); if (pk_ed) { if (rpc.pubkey.type() != 5) throw parse_error{"pubkey_ed25519 is only permitted for 05[...] pubkeys"}; if (pk_ed->size() == 64) { if (!oxenc::is_hex(*pk_ed)) throw parse_error{"invalid pubkey_ed25519: value is not hex"}; oxenc::from_hex(pk_ed->begin(), pk_ed->end(), rpc.pubkey_ed25519.emplace().begin()); } else if (pk_ed->size() == 32) { std::memcpy(rpc.pubkey_ed25519.emplace().data(), pk_ed->data(), pk_ed->size()); } else { throw parse_error{ "Invalid pubkey_ed25519: expected 64 hex char or 32 byte " "pubkey"}; } } unsigned char* sig_data_ptr; if constexpr (is_std_optional) sig_data_ptr = rpc.signature.emplace().data(); else sig_data_ptr = rpc.signature.data(); if constexpr (std::is_same_v) { if (!oxenc::is_base64(*sig) || !(sig->size() == 86 || (sig->size() == 88 && sig->substr(86) == "=="))) throw parse_error{"invalid signature: expected base64 encoded Ed25519 signature"}; oxenc::from_base64(sig->begin(), sig->end(), sig_data_ptr); } else { if (sig->size() != 64) throw parse_error{"invalid signature: expected 64-byte Ed25519 signature"}; std::memcpy(sig_data_ptr, sig->data(), 64); } } template void load_subkey(RPC& rpc, const Dict&, const std::optional& subkey) { if (!subkey || subkey->empty()) return; const auto& sk = *subkey; if constexpr (std::is_same_v) { if (oxenc::is_base64(sk) && (sk.size() == 43 || (sk.size() == 44 && sk.back() == '='))) oxenc::from_base64(sk.begin(), sk.end(), rpc.subkey.emplace().begin()); else if (oxenc::is_hex(sk) && sk.size() == 64) oxenc::from_hex(sk.begin(), sk.end(), rpc.subkey.emplace().begin()); else throw parse_error{ "invalid subkey: expected base64 or hex-encoded 32-byte subkey index"}; } else { if (sk.size() != 32) throw parse_error{"invalid subkey: expected 32-byte subkey index"}; std::memcpy(rpc.subkey.emplace().data(), sk.data(), 32); } } void set_variant(bt_dict& dict, const std::string& key, const namespace_var& ns) { if (auto* id = std::get_if(&ns)) dict[key] = static_cast>(*id); else { assert(std::holds_alternative(ns)); dict[key] = "all"; } } } // namespace // Aliases used in `load_fields<...>` to make formatting less obtuse using Str = std::string; using SV = std::string_view; using TP = system_clock::time_point; template using Vec = std::vector; template static void load(store& s, Dict& d) { auto [data, expiry, msg_ns, pubkey_alt, pubkey, pk_ed25519, sig_ts, sig, subkey] = load_fields( d, "data", "expiry", "namespace", "pubKey", "pubkey", "pubkey_ed25519", "sig_timestamp", "signature", "subkey"); // timestamp and ttl are special snowflakes: for backwards compat reasons, they can // be passed as strings when loading from json. std::optional ttl; std::optional timestamp; if constexpr (std::is_same_v) { if (auto ts = parse_stringified(d, "timestamp")) timestamp = from_epoch_ms(*ts); ttl = parse_stringified(d, "ttl"); } else { timestamp = parse_field(d, "timestamp"); ttl = parse_field(d, "ttl"); } require("timestamp", timestamp); require_exactly_one_of("expiry", expiry, "ttl", ttl); s.timestamp = *timestamp; s.expiry = expiry ? *expiry : s.timestamp + std::chrono::milliseconds{*ttl}; require_exactly_one_of("pubkey", pubkey, "pubKey", pubkey_alt, true); auto& pk = pubkey ? pubkey : pubkey_alt; if (msg_ns) s.msg_namespace = *msg_ns; if (sig) { load_pk_signature(s, d, pk, pk_ed25519, sig); load_subkey(s, d, subkey); s.sig_ts = sig_ts.value_or(s.timestamp); } else load_pk(s, pk); require("data", data); if constexpr (std::is_same_v) { // For json we require data be base64 encoded if (!oxenc::is_base64(*data)) throw parse_error{"Invalid 'data' value: not base64 encoded"}; static_assert( store::MAX_MESSAGE_BODY % 3 == 0, "MAX_MESSAGE_BODY should be divisible by 3 so that max base64 encoded size " "avoids padding"); if (data->size() > store::MAX_MESSAGE_BODY / 3 * 4) throw parse_error{fmt::format( "Message body exceeds maximum allowed length of {} bytes", store::MAX_MESSAGE_BODY)}; s.data = oxenc::from_base64(*data); } else { // Otherwise (i.e. bencoded) then we take data as bytes if (data->size() > store::MAX_MESSAGE_BODY) throw parse_error{fmt::format( "Message body exceeds maximum allowed length of {} bytes", store::MAX_MESSAGE_BODY)}; s.data = *data; } } void store::load_from(json params) { load(*this, params); } void store::load_from(bt_dict_consumer params) { load(*this, params); } bt_value store::to_bt() const { bt_dict d{ {"pubkey", pubkey.prefixed_raw()}, {"timestamp", to_epoch_ms(timestamp)}, {"expiry", to_epoch_ms(expiry)}, {"data", std::string_view{data}}}; if (msg_namespace != namespace_id::Default) d["namespace"] = static_cast>(msg_namespace); if (subkey) d["subkey"] = util::view_guts(*subkey); if (signature) d["signature"] = util::view_guts(*signature); if (pubkey_ed25519) d["pubkey_ed25519"] = std::string_view{ reinterpret_cast(pubkey_ed25519->data()), pubkey_ed25519->size()}; return d; } template static void load(retrieve& r, Dict& d) { auto [lastHash, last_hash, max_count, max_size, msg_ns, pubKey, pubkey, pk_ed25519, sig, subkey, ts] = load_fields( d, "lastHash", "last_hash", "max_count", "max_size", "namespace", "pubKey", "pubkey", "pubkey_ed25519", "signature", "subkey", "timestamp"); require_exactly_one_of("pubkey", pubkey, "pubKey", pubKey, true); auto& pk = pubkey ? pubkey : pubKey; if (pk_ed25519 || sig || ts || (msg_ns && *msg_ns != namespace_id::LegacyClosed)) { load_pk_signature(r, d, pk, pk_ed25519, sig); load_subkey(r, d, subkey); r.timestamp = std::move(*ts); r.check_signature = true; } else { load_pk(r, pk); } if (msg_ns) r.msg_namespace = *msg_ns; require_at_most_one_of("last_hash", last_hash, "lastHash", lastHash); if (lastHash) last_hash = std::move(lastHash); if (last_hash) { if (last_hash->empty()) // Treat empty string as not provided last_hash.reset(); else if (last_hash->size() == 43) { if (!oxenc::is_base64(*last_hash)) throw parse_error{"Invalid last_hash: not base64"}; } else throw parse_error{"Invalid last_hash: expected base64 (43 chars)"}; } r.last_hash = std::move(last_hash); r.max_count = max_count; r.max_size = max_size; } void retrieve::load_from(json params) { load(*this, params); } void retrieve::load_from(bt_dict_consumer params) { load(*this, params); } static bool is_valid_message_hash(std::string_view hash) { return (hash.size() == 43 && oxenc::is_base64(hash)); } template static void load(delete_msgs& dm, Dict& d) { auto [messages, pubkey, pubkey_ed25519, required, signature] = load_fields, Str, SV, bool, SV>( d, "messages", "pubkey", "pubkey_ed25519", "required", "signature"); load_pk_signature(dm, d, pubkey, pubkey_ed25519, signature); require("messages", messages); dm.messages = std::move(*messages); if (dm.messages.empty()) throw parse_error{"messages does not contain any message hashes"}; dm.required = required.value_or(false); for (const auto& m : dm.messages) if (!is_valid_message_hash(m)) throw parse_error{"invalid message hash: " + m}; } void delete_msgs::load_from(json params) { load(*this, params); } void delete_msgs::load_from(bt_dict_consumer params) { load(*this, params); } bt_value delete_msgs::to_bt() const { bt_list msgs; for (auto& m : messages) msgs.emplace_back(std::string_view{m}); bt_dict ret{ {"pubkey", pubkey.prefixed_raw()}, {"messages", std::move(msgs)}, {"signature", util::view_guts(signature)}, }; if (pubkey_ed25519) ret["pubkey_ed25519"] = std::string_view{ reinterpret_cast(pubkey_ed25519->data()), pubkey_ed25519->size()}; return ret; } template static void load(revoke_subkey& rs, Dict& d) { auto [pubkey, pubkey_ed25519, revoke_subkey, signature] = load_fields( d, "pubkey", "pubkey_ed25519", "revoke_subkey", "signature"); load_pk_signature(rs, d, pubkey, pubkey_ed25519, signature); require("revoke_subkey", revoke_subkey); const auto& sk = *revoke_subkey; if constexpr (std::is_same_v) { if (oxenc::is_base64(sk) && (sk.size() == 43 || (sk.size() == 44 && sk.back() == '='))) oxenc::from_base64(sk.begin(), sk.end(), rs.revoke_subkey.begin()); else if (oxenc::is_hex(sk) && sk.size() == 64) oxenc::from_hex(sk.begin(), sk.end(), rs.revoke_subkey.begin()); else throw parse_error{"invalid subkey: expected base64 or hex-encoded 32-byte subkey tag"}; } else { if (sk.size() != 32) throw parse_error{"invalid subkey: expected 32-byte subkey tag"}; std::memcpy(rs.revoke_subkey.data(), sk.data(), 32); } } void revoke_subkey::load_from(json params) { load(*this, params); } void revoke_subkey::load_from(bt_dict_consumer params) { load(*this, params); } bt_value revoke_subkey::to_bt() const { bt_dict ret{ {"pubkey", pubkey.prefixed_raw()}, {"revoke_subkey", util::view_guts(revoke_subkey)}, {"signature", util::view_guts(signature)}}; if (pubkey_ed25519) ret["pubkey_ed25519"] = std::string_view{ reinterpret_cast(pubkey_ed25519->data()), pubkey_ed25519->size()}; return ret; } template static void load(delete_all& da, Dict& d) { auto [msgs_ns, pubkey, pubkey_ed25519, signature, timestamp] = load_fields( d, "namespace", "pubkey", "pubkey_ed25519", "signature", "timestamp"); load_pk_signature(da, d, pubkey, pubkey_ed25519, signature); require("timestamp", timestamp); da.msg_namespace = msgs_ns.value_or(namespace_id::Default); da.timestamp = std::move(*timestamp); } void delete_all::load_from(json params) { load(*this, params); } void delete_all::load_from(bt_dict_consumer params) { load(*this, params); } bt_value delete_all::to_bt() const { bt_dict ret{ {"pubkey", pubkey.prefixed_raw()}, {"signature", util::view_guts(signature)}, {"timestamp", to_epoch_ms(timestamp)}}; set_variant(ret, "namespace", msg_namespace); if (pubkey_ed25519) ret["pubkey_ed25519"] = std::string_view{ reinterpret_cast(pubkey_ed25519->data()), pubkey_ed25519->size()}; return ret; } template static void load(delete_before& db, Dict& d) { auto [before, msgs_ns, pubkey, pubkey_ed25519, signature] = load_fields( d, "before", "namespace", "pubkey", "pubkey_ed25519", "signature"); load_pk_signature(db, d, pubkey, pubkey_ed25519, signature); require("before", before); db.before = std::move(*before); db.msg_namespace = msgs_ns.value_or(namespace_id::Default); } void delete_before::load_from(json params) { load(*this, params); } void delete_before::load_from(bt_dict_consumer params) { load(*this, params); } bt_value delete_before::to_bt() const { bt_dict ret{ {"pubkey", pubkey.prefixed_raw()}, {"signature", util::view_guts(signature)}, {"before", to_epoch_ms(before)}}; set_variant(ret, "namespace", msg_namespace); if (pubkey_ed25519) ret["pubkey_ed25519"] = std::string_view{ reinterpret_cast(pubkey_ed25519->data()), pubkey_ed25519->size()}; return ret; } template static void load(expire_all& e, Dict& d) { auto [expiry, msgs_ns, pubkey, pubkey_ed25519, signature] = load_fields( d, "expiry", "namespace", "pubkey", "pubkey_ed25519", "signature"); load_pk_signature(e, d, pubkey, pubkey_ed25519, signature); require("expiry", expiry); e.expiry = std::move(*expiry); e.msg_namespace = msgs_ns.value_or(namespace_id::Default); } void expire_all::load_from(json params) { load(*this, params); } void expire_all::load_from(bt_dict_consumer params) { load(*this, params); } bt_value expire_all::to_bt() const { bt_dict ret{ {"pubkey", pubkey.prefixed_raw()}, {"signature", util::view_guts(signature)}, {"expiry", to_epoch_ms(expiry)}}; set_variant(ret, "namespace", msg_namespace); if (pubkey_ed25519) ret["pubkey_ed25519"] = std::string_view{ reinterpret_cast(pubkey_ed25519->data()), pubkey_ed25519->size()}; return ret; } template static void load(expire_msgs& e, Dict& d) { auto [expiry, extend, messages, pubkey, pubkey_ed25519, shorten, signature, subkey] = load_fields, Str, SV, bool, SV, SV>( d, "expiry", "extend", "messages", "pubkey", "pubkey_ed25519", "shorten", "signature", "subkey"); load_pk_signature(e, d, pubkey, pubkey_ed25519, signature); load_subkey(e, d, subkey); require("expiry", expiry); e.expiry = std::move(*expiry); e.shorten = shorten.value_or(false); e.extend = extend.value_or(false); if (e.shorten && e.extend) throw parse_error{"cannot specify both 'shorten' and 'extend'"}; require("messages", messages); e.messages = std::move(*messages); if (e.messages.empty()) throw parse_error{"messages does not contain any message hashes"}; for (const auto& m : e.messages) if (!is_valid_message_hash(m)) throw parse_error{"invalid message hash: " + m}; } void expire_msgs::load_from(json params) { load(*this, params); } void expire_msgs::load_from(bt_dict_consumer params) { load(*this, params); } bt_value expire_msgs::to_bt() const { bt_list msgs; for (const auto& m : messages) msgs.emplace_back(std::string_view{m}); bt_dict ret{ {"pubkey", pubkey.prefixed_raw()}, {"signature", util::view_guts(signature)}, {"expiry", to_epoch_ms(expiry)}, {"messages", std::move(msgs)}, }; if (pubkey_ed25519) ret["pubkey_ed25519"] = std::string_view{ reinterpret_cast(pubkey_ed25519->data()), pubkey_ed25519->size()}; if (shorten) ret["shorten"] = 1; if (extend) ret["extend"] = 1; if (subkey) ret["subkey"] = std::string_view{reinterpret_cast(subkey->data()), subkey->size()}; return ret; } template static void load(get_expiries& ge, Dict& d) { auto [messages, pubkey, pk_ed25519, sig, subkey, timestamp] = load_fields, Str, SV, SV, SV, TP>( d, "messages", "pubkey", "pubkey_ed25519", "signature", "subkey", "timestamp"); load_pk_signature(ge, d, pubkey, pk_ed25519, sig); load_subkey(ge, d, subkey); require("timestamp", timestamp); ge.sig_ts = *timestamp; require("messages", messages); ge.messages = std::move(*messages); if (ge.messages.empty()) throw parse_error{"messages does not contain any message hashes"}; } void get_expiries::load_from(json params) { load(*this, params); } void get_expiries::load_from(bt_dict_consumer params) { load(*this, params); } template static void load(get_swarm& g, Dict& d) { auto [pubKey, pubkey] = load_fields(d, "pubKey", "pubkey"); require_exactly_one_of("pubkey", pubkey, "pubKey", pubKey, true); if (!g.pubkey.load(std::move(pubkey ? *pubkey : *pubKey))) throw parse_error{fmt::format( "Pubkey must be {} hex digits/{} bytes long", USER_PUBKEY_SIZE_HEX, USER_PUBKEY_SIZE_BYTES)}; } void get_swarm::load_from(json params) { load(*this, params); } void get_swarm::load_from(bt_dict_consumer params) { load(*this, params); } inline const static std::unordered_set allowed_oxend_endpoints{ {"get_service_nodes"sv, "ons_resolve"sv}}; template static void load(oxend_request& o, Dict& d) { auto endpoint = parse_field(d, "endpoint"); require("endpoint", endpoint); o.endpoint = *endpoint; if (!allowed_oxend_endpoints.count(o.endpoint)) throw parse_error{fmt::format("Invalid oxend endpoint '{}'", o.endpoint)}; if constexpr (std::is_same_v) { if (auto it = d.find("params"); it != d.end() && !it->is_null()) o.params = *it; } else { if (auto json_str = parse_field(d, "params")) { json params = json::parse(*json_str, nullptr, false); if (params.is_discarded()) throw parse_error{"oxend_request params field does not contain valid json"}; if (!params.is_null()) o.params = std::move(params); } } } void oxend_request::load_from(json params) { load(*this, params); } void oxend_request::load_from(bt_dict_consumer params) { load(*this, params); } static client_subrequest as_subrequest(client_request&& req) { return var::visit( [](auto&& r) -> client_subrequest { using T = std::decay_t; if constexpr (type_list_contains) return std::move(r); else throw parse_error{ "Invalid batch subrequest: subrequests may not contain meta-requests"}; }, std::move(req)); } void batch::load_from(json params) { auto reqs_it = params.find("requests"); if (reqs_it == params.end() || !reqs_it->is_array() || reqs_it->empty()) throw parse_error{"Invalid batch request: no valid \"requests\" field"}; if (reqs_it->size() > BATCH_REQUEST_MAX) throw parse_error{"Invalid batch request: subrequest limit exceeded"}; for (auto& j : *reqs_it) { if (!j.is_object()) throw parse_error{"Invalid batch request: requests must be objects"}; auto meth_it = j.find("method"); auto params_it = j.find("params"); if (meth_it == j.end() || params_it == j.end() || !meth_it->is_string() || !params_it->is_object()) throw parse_error{"Invalid batch request: subrequests must have method/params keys"}; auto method = meth_it->get(); auto rpc_it = RequestHandler::client_rpc_endpoints.find(method); if (rpc_it == RequestHandler::client_rpc_endpoints.end()) throw parse_error{ "Invalid batch subrequest: invalid method \"" + std::string{method} + "\""}; subreqs.push_back(as_subrequest(rpc_it->second.load_req(std::move(*params_it)))); } } void batch::load_from(bt_dict_consumer params) { if (!params.skip_until("requests") || !params.is_list()) throw parse_error{"Invalid batch request: no valid \"requests\" field"}; auto requests = params.consume_list_consumer(); while (!requests.is_finished()) { if (!requests.is_dict()) throw parse_error{"Invalid batch request: requests must be dicts"}; if (subreqs.size() >= BATCH_REQUEST_MAX) throw parse_error{"Invalid batch request: subrequest limit exceeded"}; auto sr = requests.consume_dict_consumer(); if (!sr.skip_until("method") || !sr.is_string()) throw parse_error{"Invalid batch request: subrequests must have a method"}; auto method = sr.consume_string_view(); auto rpc_it = RequestHandler::client_rpc_endpoints.find(method); if (rpc_it == RequestHandler::client_rpc_endpoints.end()) throw parse_error{ "Invalid batch subrequest: invalid method \"" + std::string{method} + "\""}; if (!sr.skip_until("params") || !sr.is_dict()) throw parse_error{"Invalid batch request: subrequests must have a params dict"}; subreqs.push_back(as_subrequest(rpc_it->second.load_req(sr.consume_dict_consumer()))); } if (subreqs.empty()) throw parse_error{"Invalid batch request: empty \"requests\" list"}; } // Copies an optional vector into a fixed-size array, substituting 0's for omitted vector elements, // and ignoring anything in the vector longer than the given size. Gives nullopt if the input // vector is itself nullopt or empty. template static std::optional> to_fixed_array(const std::optional>& in) { if (!in || in->empty()) return std::nullopt; std::array out; for (size_t i = 0; i < N; i++) out[i] = i < in->size() ? (*in)[i] : T{0}; return out; } template static void load_condition(ifelse& i, Dict if_) { auto [height_ge_, height_lt_, hf_ge_, hf_lt_, v_ge_, v_lt_] = load_fields, Vec, Vec, Vec>( if_, "height_at_least", "height_before", "hf_at_least", "hf_before", "v_at_least", "v_before"); auto hf_ge = to_fixed_array<2>(hf_ge_); auto hf_lt = to_fixed_array<2>(hf_lt_); auto v_ge = to_fixed_array<3>(v_ge_); auto v_lt = to_fixed_array<3>(v_lt_); auto height_ge = height_ge_; auto height_lt = height_lt_; if (!(height_ge_ || height_lt_ || hf_ge || hf_lt || v_ge || v_lt)) throw parse_error{"Invalid ifelse request: must specify at least one \"if\" condition"}; i.condition = [=](const snode::ServiceNode& snode) { bool result = true; if (hf_ge || hf_lt) { std::array hf = {snode.hf().first, snode.hf().second}; if (hf_ge) result &= hf >= *hf_ge; if (hf_lt) result &= hf < *hf_lt; } if (v_ge || v_lt) { std::array v = { STORAGE_SERVER_VERSION[0], STORAGE_SERVER_VERSION[1], STORAGE_SERVER_VERSION[2]}; if (v_ge) result &= v >= *v_ge; if (v_lt) result &= v < *v_lt; } if (height_ge || height_lt) { auto height = static_cast(snode.blockheight()); if (height_ge) result &= height >= *height_ge; if (height_lt) result &= height < *height_lt; } return result; }; } static std::unique_ptr load_ifelse_request(json& params, const std::string& key) { auto it = params.find(key); if (it == params.end()) return nullptr; if (!it->is_object()) throw parse_error{"Invalid ifelse request: " + key + " must be an object"}; auto mit = it->find("method"); auto pit = it->find("params"); if (mit == it->end() || !mit->is_string() || pit == it->end()) throw parse_error{"Invalid ifelse request: " + key + " must have method/params keys"}; auto method = mit->get(); auto rpc_it = RequestHandler::client_rpc_endpoints.find(method); if (rpc_it == RequestHandler::client_rpc_endpoints.end()) throw parse_error{"Invalid ifelse request method \"" + key + "\""}; return var::visit( [](auto&& r) { return std::make_unique(std::move(r)); }, rpc_it->second.load_req(std::move(*pit))); } static std::unique_ptr load_ifelse_request( bt_dict_consumer& params, const std::string& key) { if (!params.skip_until(key)) return nullptr; if (!params.is_dict()) throw parse_error{"Invalid ifelse request: " + key + " must be a dict"}; auto req = params.consume_dict_consumer(); if (!req.skip_until("method") || !req.is_string()) throw parse_error{"Invalid ifelse request: " + key + " missing method"}; auto method = req.consume_string_view(); auto rpc_it = RequestHandler::client_rpc_endpoints.find(method); if (rpc_it == RequestHandler::client_rpc_endpoints.end()) throw parse_error{"Invalid ifelse request method \"" + key + "\""}; if (!req.skip_until("params") || !req.is_dict()) throw parse_error{"Invalid ifelse request: " + key + " missing params"}; return var::visit( [](auto&& r) { return std::make_unique(std::move(r)); }, rpc_it->second.load_req(req.consume_dict_consumer())); } void ifelse::load_from(json params) { auto cond_it = params.find("if"); if (cond_it == params.end() || !cond_it->is_object()) throw parse_error{"Invalid ifelse request: no valid \"if\" field"}; load_condition(*this, std::move(*cond_it)); action_true = load_ifelse_request(params, "then"); action_false = load_ifelse_request(params, "else"); if (!action_true && !action_false) throw parse_error{"Invalid ifelse request: at least one of \"then\"/\"else\" required"}; } void ifelse::load_from(bt_dict_consumer params) { action_false = load_ifelse_request(params, "else"); if (!params.skip_until("if") || !params.is_dict()) throw parse_error{"Invalid ifelse request: no valid \"if\" field"}; load_condition(*this, params.consume_dict_consumer()); action_true = load_ifelse_request(params, "then"); if (!action_true && !action_false) throw parse_error{"Invalid ifelse request: at least one of \"then\"/\"else\" required"}; } } // namespace oxen::rpc