970 lines
38 KiB
C++
970 lines
38 KiB
C++
#include "client_rpc_endpoints.h"
|
|
#include "request_handler.h"
|
|
#include <oxenss/logging/oxen_logger.h>
|
|
#include <oxenss/utils/string_utils.hpp>
|
|
#include <oxenss/utils/time.hpp>
|
|
#include <oxenss/version.h>
|
|
|
|
#include <chrono>
|
|
#include <limits>
|
|
#include <type_traits>
|
|
#include <unordered_set>
|
|
#include <variant>
|
|
|
|
#include <oxenc/base64.h>
|
|
#include <oxenc/hex.h>
|
|
|
|
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 <typename T>
|
|
constexpr bool is_timestamp = std::is_same_v<T, system_clock::time_point>;
|
|
template <typename T>
|
|
constexpr bool is_str_array = std::is_same_v<T, std::vector<std::string>>;
|
|
template <typename T>
|
|
constexpr bool is_int_array = std::is_same_v<T, std::vector<int>>;
|
|
template <typename T>
|
|
constexpr bool is_namespace_var = std::is_same_v<T, namespace_var>;
|
|
|
|
template <typename T>
|
|
constexpr std::string_view type_desc = std::is_same_v<T, bool> ? "boolean"sv
|
|
: std::is_unsigned_v<T> ? "positive integer"sv
|
|
: std::is_integral_v<T> ? "integer"sv
|
|
: is_namespace_var<T> ? "integer or \"all\""sv
|
|
: std::is_same_v<T, namespace_id> ? "16-bit integer"sv
|
|
: is_timestamp<T> ? "integer timestamp (in milliseconds)"sv
|
|
: is_str_array<T> ? "string array"sv
|
|
: is_int_array<T> ? "integer array"sv
|
|
: "string"sv;
|
|
|
|
template <typename T>
|
|
constexpr bool is_parseable_v =
|
|
std::is_unsigned_v<T> || std::is_integral_v<T> || is_timestamp<T> || is_str_array<T> ||
|
|
is_int_array<T> || is_namespace_var<T> || std::is_same_v<T, std::string_view> ||
|
|
std::is_same_v<T, std::string> || std::is_same_v<T, namespace_id>;
|
|
|
|
// 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 <typename T>
|
|
std::optional<T> parse_field(const json& params, const char* name) {
|
|
static_assert(is_parseable_v<T>);
|
|
auto it = params.find(name);
|
|
if (it == params.end() || it->is_null())
|
|
return std::nullopt;
|
|
|
|
bool right_type = std::is_same_v<T, bool> ? it->is_boolean()
|
|
: std::is_unsigned_v<T> || is_timestamp<T> ? it->is_number_unsigned()
|
|
: std::is_integral_v<T> || std::is_same_v<T, namespace_id>
|
|
? it->is_number_integer()
|
|
: is_namespace_var<T> ? it->is_number_integer() || it->is_string()
|
|
: is_str_array<T> || is_int_array<T> ? it->is_array()
|
|
: it->is_string();
|
|
if (is_str_array<T> && right_type) {
|
|
for (auto& x : *it)
|
|
if (!x.is_string())
|
|
right_type = false;
|
|
} else if (is_int_array<T> && 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<T>)};
|
|
if constexpr (std::is_same_v<T, std::string_view>)
|
|
return it->template get_ref<const std::string&>();
|
|
else if constexpr (is_timestamp<T>) {
|
|
auto time = from_epoch_ms(it->template get<int64_t>());
|
|
// 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<T> || std::is_same_v<T, namespace_id>) {
|
|
if (it->is_number_integer()) {
|
|
int64_t id = it->get<int64_t>();
|
|
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<std::underlying_type_t<namespace_id>>(id)};
|
|
}
|
|
if constexpr (is_namespace_var<T>)
|
|
if (it->is_string() && it->get_ref<const std::string&>() == "all")
|
|
return namespace_all;
|
|
throw parse_error{
|
|
fmt::format("Invalid value given for '{}': expected integer or \"all\"", name)};
|
|
} else {
|
|
return it->template get<T>();
|
|
}
|
|
}
|
|
|
|
// 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 <typename T>
|
|
std::optional<T> parse_field(bt_dict_consumer& params, const char* name) {
|
|
static_assert(is_parseable_v<T>);
|
|
if (!params.skip_until(name))
|
|
return std::nullopt;
|
|
|
|
try {
|
|
if constexpr (std::is_same_v<T, std::string_view>)
|
|
return params.consume_string_view();
|
|
else if constexpr (std::is_same_v<T, std::string>)
|
|
return params.consume_string();
|
|
else if constexpr (std::is_integral_v<T>)
|
|
return params.consume_integer<T>();
|
|
else if constexpr (is_timestamp<T>)
|
|
return from_epoch_ms(params.consume_integer<int64_t>());
|
|
else if constexpr (is_str_array<T> || is_int_array<T>) {
|
|
auto elems = std::make_optional<T>();
|
|
for (auto l = params.consume_list_consumer(); !l.is_finished();)
|
|
if constexpr (is_str_array<T>)
|
|
elems->push_back(l.consume_string());
|
|
else
|
|
elems->push_back(l.consume_integer<int>());
|
|
return elems;
|
|
} else if constexpr (is_namespace_var<T> || std::is_same_v<T, namespace_id>) {
|
|
if (params.is_integer())
|
|
return namespace_id{
|
|
params.consume_integer<std::underlying_type_t<namespace_id>>()};
|
|
if constexpr (is_namespace_var<T>)
|
|
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<T>)};
|
|
}
|
|
|
|
// Backwards compat code for fields like ttl and timestamp that are accepted either
|
|
// as integer *or* stringified integer.
|
|
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
|
|
std::optional<T> 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<const std::string&>(), value))
|
|
return value;
|
|
else
|
|
throw parse_error{
|
|
fmt::format("Invalid value given for '{}': {}", name, it->dump())};
|
|
}
|
|
return parse_field<T>(params, name);
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
constexpr bool check_ascending(std::string_view) {
|
|
return true;
|
|
}
|
|
template <typename... Args>
|
|
constexpr bool check_ascending(std::string_view a, std::string_view b, Args&&... args) {
|
|
return a < b && check_ascending(b, std::forward<Args>(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<Names, std::string_view> && ...)>>
|
|
std::tuple<std::optional<T>...> load_fields(Dict& params, const Names&... names) {
|
|
assert(check_ascending(names...));
|
|
return {parse_field<T>(params, names)...};
|
|
}
|
|
|
|
template <typename T>
|
|
void require(std::string_view name, const std::optional<T>& v) {
|
|
if (!v)
|
|
throw parse_error{fmt::format("Required field '{}' missing", name)};
|
|
}
|
|
|
|
template <typename... T>
|
|
void require(std::string_view name, const std::variant<std::monostate, T...>& v) {
|
|
if (v.index() == 0)
|
|
throw parse_error{fmt::format("Required field '{}' missing", name)};
|
|
}
|
|
|
|
template <typename T1, typename T2>
|
|
void require_at_most_one_of(
|
|
std::string_view first,
|
|
const std::optional<T1>& a,
|
|
std::string_view second,
|
|
const std::optional<T2>& b) {
|
|
if (a && b)
|
|
throw parse_error{fmt::format("Cannot specify both '{}' and '{}'", first, second)};
|
|
}
|
|
|
|
template <typename T1, typename T2>
|
|
void require_exactly_one_of(
|
|
std::string_view first,
|
|
const std::optional<T1>& a,
|
|
std::string_view second,
|
|
const std::optional<T2>& 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 <typename RPC>
|
|
void load_pk(RPC& rpc, std::optional<std::string>& 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 <typename T>
|
|
constexpr bool is_std_optional = false;
|
|
template <typename T>
|
|
constexpr bool is_std_optional<std::optional<T>> = true;
|
|
|
|
// Parses (but does not verify) a required request signature value.
|
|
template <typename RPC, typename Dict>
|
|
void load_pk_signature(
|
|
RPC& rpc,
|
|
const Dict&,
|
|
std::optional<std::string>& pk,
|
|
const std::optional<std::string_view>& pk_ed,
|
|
const std::optional<std::string_view>& 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<decltype(rpc.signature)>)
|
|
sig_data_ptr = rpc.signature.emplace().data();
|
|
else
|
|
sig_data_ptr = rpc.signature.data();
|
|
|
|
if constexpr (std::is_same_v<json, Dict>) {
|
|
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 <typename RPC, typename Dict>
|
|
void load_subkey(RPC& rpc, const Dict&, const std::optional<std::string_view>& subkey) {
|
|
if (!subkey || subkey->empty())
|
|
return;
|
|
const auto& sk = *subkey;
|
|
if constexpr (std::is_same_v<json, Dict>) {
|
|
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<namespace_id>(&ns))
|
|
dict[key] = static_cast<std::underlying_type_t<namespace_id>>(*id);
|
|
else {
|
|
assert(std::holds_alternative<namespace_all_t>(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 <typename T>
|
|
using Vec = std::vector<T>;
|
|
|
|
template <typename Dict>
|
|
static void load(store& s, Dict& d) {
|
|
auto [data, expiry, msg_ns, pubkey_alt, pubkey, pk_ed25519, sig_ts, sig, subkey] =
|
|
load_fields<SV, TP, namespace_id, Str, Str, SV, TP, SV, SV>(
|
|
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<uint64_t> ttl;
|
|
std::optional<system_clock::time_point> timestamp;
|
|
if constexpr (std::is_same_v<Dict, json>) {
|
|
if (auto ts = parse_stringified<int64_t>(d, "timestamp"))
|
|
timestamp = from_epoch_ms(*ts);
|
|
ttl = parse_stringified<uint64_t>(d, "ttl");
|
|
} else {
|
|
timestamp = parse_field<system_clock::time_point>(d, "timestamp");
|
|
ttl = parse_field<uint64_t>(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<Dict, json>) {
|
|
// 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<std::underlying_type_t<namespace_id>>(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<const char*>(pubkey_ed25519->data()), pubkey_ed25519->size()};
|
|
return d;
|
|
}
|
|
|
|
template <typename Dict>
|
|
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<Str, Str, int, int, namespace_id, Str, Str, SV, SV, SV, TP>(
|
|
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 <typename Dict>
|
|
static void load(delete_msgs& dm, Dict& d) {
|
|
auto [messages, pubkey, pubkey_ed25519, required, signature] =
|
|
load_fields<Vec<Str>, 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<const char*>(pubkey_ed25519->data()), pubkey_ed25519->size()};
|
|
return ret;
|
|
}
|
|
|
|
template <typename Dict>
|
|
static void load(revoke_subkey& rs, Dict& d) {
|
|
auto [pubkey, pubkey_ed25519, revoke_subkey, signature] = load_fields<Str, SV, SV, SV>(
|
|
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<json, Dict>) {
|
|
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<const char*>(pubkey_ed25519->data()), pubkey_ed25519->size()};
|
|
return ret;
|
|
}
|
|
|
|
template <typename Dict>
|
|
static void load(delete_all& da, Dict& d) {
|
|
auto [msgs_ns, pubkey, pubkey_ed25519, signature, timestamp] =
|
|
load_fields<namespace_var, Str, SV, SV, TP>(
|
|
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<const char*>(pubkey_ed25519->data()), pubkey_ed25519->size()};
|
|
return ret;
|
|
}
|
|
|
|
template <typename Dict>
|
|
static void load(delete_before& db, Dict& d) {
|
|
auto [before, msgs_ns, pubkey, pubkey_ed25519, signature] =
|
|
load_fields<TP, namespace_var, Str, SV, SV>(
|
|
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<const char*>(pubkey_ed25519->data()), pubkey_ed25519->size()};
|
|
return ret;
|
|
}
|
|
|
|
template <typename Dict>
|
|
static void load(expire_all& e, Dict& d) {
|
|
auto [expiry, msgs_ns, pubkey, pubkey_ed25519, signature] =
|
|
load_fields<TP, namespace_var, Str, SV, SV>(
|
|
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<const char*>(pubkey_ed25519->data()), pubkey_ed25519->size()};
|
|
return ret;
|
|
}
|
|
|
|
template <typename Dict>
|
|
static void load(expire_msgs& e, Dict& d) {
|
|
auto [expiry, extend, messages, pubkey, pubkey_ed25519, shorten, signature, subkey] =
|
|
load_fields<TP, bool, Vec<Str>, 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<const char*>(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<const char*>(subkey->data()), subkey->size()};
|
|
return ret;
|
|
}
|
|
|
|
template <typename Dict>
|
|
static void load(get_expiries& ge, Dict& d) {
|
|
auto [messages, pubkey, pk_ed25519, sig, subkey, timestamp] =
|
|
load_fields<Vec<Str>, 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 <typename Dict>
|
|
static void load(get_swarm& g, Dict& d) {
|
|
auto [pubKey, pubkey] = load_fields<Str, Str>(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<std::string_view> allowed_oxend_endpoints{
|
|
{"get_service_nodes"sv, "ons_resolve"sv}};
|
|
|
|
template <typename Dict>
|
|
static void load(oxend_request& o, Dict& d) {
|
|
auto endpoint = parse_field<std::string>(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<Dict, json>) {
|
|
if (auto it = d.find("params"); it != d.end() && !it->is_null())
|
|
o.params = *it;
|
|
} else {
|
|
if (auto json_str = parse_field<std::string_view>(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<decltype(r)>;
|
|
if constexpr (type_list_contains<T, client_rpc_subrequests>)
|
|
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<std::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} + "\""};
|
|
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 <size_t N, typename T>
|
|
static std::optional<std::array<T, N>> to_fixed_array(const std::optional<std::vector<T>>& in) {
|
|
if (!in || in->empty())
|
|
return std::nullopt;
|
|
std::array<T, N> out;
|
|
for (size_t i = 0; i < N; i++)
|
|
out[i] = i < in->size() ? (*in)[i] : T{0};
|
|
return out;
|
|
}
|
|
|
|
template <typename Dict>
|
|
static void load_condition(ifelse& i, Dict if_) {
|
|
auto [height_ge_, height_lt_, hf_ge_, hf_lt_, v_ge_, v_lt_] =
|
|
load_fields<int, int, Vec<int>, Vec<int>, Vec<int>, Vec<int>>(
|
|
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<int, 2> 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<int, 3> 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<int>(snode.blockheight());
|
|
if (height_ge)
|
|
result &= height >= *height_ge;
|
|
if (height_lt)
|
|
result &= height < *height_lt;
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
static std::unique_ptr<client_request> 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<std::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 + "\""};
|
|
|
|
return var::visit(
|
|
[](auto&& r) { return std::make_unique<client_request>(std::move(r)); },
|
|
rpc_it->second.load_req(std::move(*pit)));
|
|
}
|
|
|
|
static std::unique_ptr<client_request> 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<client_request>(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
|