зеркало из https://github.com/microsoft/CCF.git
Some RPCs are GET-only (#966)
This commit is contained in:
Родитель
3303244ce6
Коммит
4d480f4a9a
|
@ -23,7 +23,7 @@ class AppUser:
|
|||
network.consortium.add_users(primary, [self.name])
|
||||
|
||||
with primary.user_client(user_id=self.name) as client:
|
||||
self.ccf_id = client.rpc("whoAmI", {}).result["caller_id"]
|
||||
self.ccf_id = client.get("whoAmI").result["caller_id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.ccf_id} ({self.name})"
|
||||
|
|
|
@ -44,7 +44,7 @@ def run(args):
|
|||
) as c:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
resp = reg_c.rpc("REG_poll_flagged", {}).to_dict()
|
||||
resp = reg_c.rpc("REG_poll_flagged").to_dict()
|
||||
|
||||
if "result" in resp:
|
||||
flagged_txs = resp["result"]
|
||||
|
|
|
@ -23,7 +23,7 @@ class AppUser:
|
|||
network.consortium.add_users(primary, [self.name])
|
||||
|
||||
with primary.user_client(user_id=self.name) as client:
|
||||
self.ccf_id = client.rpc("whoAmI", {}).result["caller_id"]
|
||||
self.ccf_id = client.get("whoAmI").result["caller_id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.ccf_id} ({self.name})"
|
||||
|
@ -99,20 +99,20 @@ def run(args):
|
|||
# Check permissions are enforced
|
||||
with primary.user_client(user_id=regulator.name) as c:
|
||||
check(
|
||||
c.rpc("REG_register", {}),
|
||||
c.rpc("REG_register"),
|
||||
error=check_status(http.HTTPStatus.FORBIDDEN),
|
||||
)
|
||||
check(
|
||||
c.rpc("BK_register", {}), error=check_status(http.HTTPStatus.FORBIDDEN),
|
||||
c.rpc("BK_register"), error=check_status(http.HTTPStatus.FORBIDDEN),
|
||||
)
|
||||
|
||||
with primary.user_client(user_id=banks[0].name) as c:
|
||||
check(
|
||||
c.rpc("REG_register", {}),
|
||||
c.rpc("REG_register"),
|
||||
error=check_status(http.HTTPStatus.FORBIDDEN),
|
||||
)
|
||||
check(
|
||||
c.rpc("BK_register", {}), error=check_status(http.HTTPStatus.FORBIDDEN),
|
||||
c.rpc("BK_register"), error=check_status(http.HTTPStatus.FORBIDDEN),
|
||||
)
|
||||
|
||||
# As permissioned manager, register regulator and banks
|
||||
|
@ -227,7 +227,7 @@ def run(args):
|
|||
with primary.user_client(user_id=bank.name) as c:
|
||||
# try to poll flagged but fail as you are not a regulator
|
||||
check(
|
||||
c.rpc("REG_poll_flagged", {}),
|
||||
c.rpc("REG_poll_flagged"),
|
||||
error=check_status(http.HTTPStatus.FORBIDDEN),
|
||||
)
|
||||
|
||||
|
@ -248,7 +248,7 @@ def run(args):
|
|||
with primary.node_client() as mc:
|
||||
with primary.user_client(user_id=regulator.name) as c:
|
||||
# assert that the flagged txs that we poll for are correct
|
||||
resp = c.rpc("REG_poll_flagged", {})
|
||||
resp = c.rpc("REG_poll_flagged")
|
||||
poll_flagged_ids = []
|
||||
for poll_flagged in resp.result:
|
||||
# poll flagged is a list [tx_id, regulator_id]
|
||||
|
|
|
@ -7,17 +7,18 @@ namespace http
|
|||
namespace headers
|
||||
{
|
||||
// All HTTP headers are expected to be lowercase
|
||||
static constexpr auto ALLOW = "allow";
|
||||
static constexpr auto AUTHORIZATION = "authorization";
|
||||
static constexpr auto DIGEST = "digest";
|
||||
static constexpr auto CONTENT_TYPE = "content-type";
|
||||
static constexpr auto CONTENT_LENGTH = "content-length";
|
||||
static constexpr auto CONTENT_TYPE = "content-type";
|
||||
static constexpr auto DIGEST = "digest";
|
||||
static constexpr auto LOCATION = "location";
|
||||
static constexpr auto WWW_AUTHENTICATE = "www-authenticate";
|
||||
|
||||
static constexpr auto CCF_COMMIT = "x-ccf-commit";
|
||||
static constexpr auto CCF_TERM = "x-ccf-term";
|
||||
static constexpr auto CCF_GLOBAL_COMMIT = "x-ccf-global-commit";
|
||||
static constexpr auto CCF_READ_ONLY = "x-ccf-read-only";
|
||||
static constexpr auto CCF_TERM = "x-ccf-term";
|
||||
}
|
||||
|
||||
namespace headervalues
|
||||
|
|
|
@ -179,7 +179,7 @@ namespace http
|
|||
void handle_request(
|
||||
http_method verb,
|
||||
const std::string_view& path,
|
||||
const std::string_view& query,
|
||||
const std::string& query,
|
||||
http::HeaderMap&& headers,
|
||||
std::vector<uint8_t>&& body) override
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace http
|
|||
virtual void handle_request(
|
||||
http_method method,
|
||||
const std::string_view& path,
|
||||
const std::string_view& query,
|
||||
const std::string& query,
|
||||
HeaderMap&& headers,
|
||||
std::vector<uint8_t>&& body) = 0;
|
||||
};
|
||||
|
@ -32,6 +32,51 @@ namespace http
|
|||
http_status status, HeaderMap&& headers, std::vector<uint8_t>&& body) = 0;
|
||||
};
|
||||
|
||||
static uint8_t hex_char_to_int(char c)
|
||||
{
|
||||
if (c <= '9')
|
||||
{
|
||||
return c - '0';
|
||||
}
|
||||
else if (c <= 'F')
|
||||
{
|
||||
return c - 'A' + 10;
|
||||
}
|
||||
else if (c <= 'f')
|
||||
{
|
||||
return c - 'a' + 10;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
static void url_unescape(std::string& s)
|
||||
{
|
||||
char const* src = s.c_str();
|
||||
char const* end = s.c_str() + s.size();
|
||||
char* dst = s.data();
|
||||
|
||||
while (src < end)
|
||||
{
|
||||
char const c = *src++;
|
||||
if (c == '%' && (src + 1) < end && isxdigit(src[0]) && isxdigit(src[1]))
|
||||
{
|
||||
const auto a = hex_char_to_int(*src++);
|
||||
const auto b = hex_char_to_int(*src++);
|
||||
*dst++ = (a << 4) | b;
|
||||
}
|
||||
else if (c == '+')
|
||||
{
|
||||
*dst++ = ' ';
|
||||
}
|
||||
else
|
||||
{
|
||||
*dst++ = c;
|
||||
}
|
||||
}
|
||||
|
||||
s.resize(dst - s.data());
|
||||
}
|
||||
|
||||
struct SimpleRequestProcessor : public http::RequestProcessor
|
||||
{
|
||||
public:
|
||||
|
@ -49,7 +94,7 @@ namespace http
|
|||
virtual void handle_request(
|
||||
http_method method,
|
||||
const std::string_view& path,
|
||||
const std::string_view& query,
|
||||
const std::string& query,
|
||||
http::HeaderMap&& headers,
|
||||
std::vector<uint8_t>&& body) override
|
||||
{
|
||||
|
@ -342,10 +387,12 @@ namespace http
|
|||
else
|
||||
{
|
||||
const auto [path, query] = parse_url(url);
|
||||
std::string unescaped_query(query);
|
||||
url_unescape(unescaped_query);
|
||||
proc.handle_request(
|
||||
http_method(parser.method),
|
||||
path,
|
||||
query,
|
||||
unescaped_query,
|
||||
std::move(headers),
|
||||
std::move(body_buf));
|
||||
}
|
||||
|
|
|
@ -115,11 +115,12 @@ namespace http
|
|||
}
|
||||
|
||||
const auto canonical_request_header = fmt::format(
|
||||
"{} {} HTTP/1.1\r\n"
|
||||
"{} {}{} HTTP/1.1\r\n"
|
||||
"{}"
|
||||
"\r\n",
|
||||
http_method_str(verb),
|
||||
fmt::format("{}{}", whole_path, query),
|
||||
whole_path,
|
||||
query.empty() ? "" : fmt::format("?{}", query),
|
||||
http::get_header_string(request_headers));
|
||||
|
||||
serialised_request.resize(
|
||||
|
|
|
@ -274,4 +274,38 @@ DOCTEST_TEST_CASE("Pessimal transport")
|
|||
|
||||
sp.received.pop();
|
||||
}
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("Escaping")
|
||||
{
|
||||
{
|
||||
const std::string unescaped =
|
||||
"This has many@many+many \\% \" AWKWARD :;-=?!& ++ characters %20%20";
|
||||
const std::string escaped =
|
||||
"This+has+many%40many%2Bmany+%5C%25+%22+AWKWARD+%3A%3B-%3D%3F%21%26+%2B%"
|
||||
"2b+"
|
||||
"characters+%2520%2520";
|
||||
|
||||
std::string s = escaped;
|
||||
http::url_unescape(s);
|
||||
DOCTEST_REQUIRE(s == unescaped);
|
||||
}
|
||||
|
||||
{
|
||||
const std::string request =
|
||||
"GET /foo/bar?this=that&awkward=escaped+string+%3A%3B-%3D%3F%21%22 "
|
||||
"HTTP/1.1\r\n\r\n";
|
||||
|
||||
http::SimpleRequestProcessor sp;
|
||||
http::RequestParser p(sp);
|
||||
|
||||
const std::vector<uint8_t> req(request.begin(), request.end());
|
||||
auto parsed = p.execute(req.data(), req.size());
|
||||
|
||||
DOCTEST_CHECK(!sp.received.empty());
|
||||
const auto& m = sp.received.front();
|
||||
DOCTEST_CHECK(m.method == HTTP_GET);
|
||||
DOCTEST_CHECK(m.path == "/foo/bar");
|
||||
DOCTEST_CHECK(m.query == "this=that&awkward=escaped string :;-=?!\"");
|
||||
}
|
||||
}
|
|
@ -83,8 +83,7 @@ namespace ccf
|
|||
};
|
||||
|
||||
auto who_am_i =
|
||||
[this](
|
||||
Store::Tx& tx, CallerId caller_id, const nlohmann::json& params) {
|
||||
[this](Store::Tx& tx, CallerId caller_id, nlohmann::json&& params) {
|
||||
if (certs == nullptr)
|
||||
{
|
||||
return make_error(
|
||||
|
@ -241,25 +240,32 @@ namespace ccf
|
|||
.set_auto_schema<GetCommit>();
|
||||
install(GeneralProcs::GET_METRICS, json_adapter(get_metrics), Read)
|
||||
.set_auto_schema<void, GetMetrics::Out>()
|
||||
.set_execute_locally(true);
|
||||
.set_execute_locally(true)
|
||||
.set_http_get_only();
|
||||
install(GeneralProcs::MK_SIGN, json_adapter(make_signature), Write)
|
||||
.set_auto_schema<void, bool>();
|
||||
install(GeneralProcs::WHO_AM_I, json_adapter(who_am_i), Read)
|
||||
.set_auto_schema<void, WhoAmI::Out>();
|
||||
.set_auto_schema<void, WhoAmI::Out>()
|
||||
.set_http_get_only();
|
||||
install(GeneralProcs::WHO_IS, json_adapter(who_is), Read)
|
||||
.set_auto_schema<WhoIs::In, WhoIs::Out>();
|
||||
install(
|
||||
GeneralProcs::GET_PRIMARY_INFO, json_adapter(get_primary_info), Read)
|
||||
.set_auto_schema<void, GetPrimaryInfo::Out>();
|
||||
.set_auto_schema<void, GetPrimaryInfo::Out>()
|
||||
.set_http_get_only();
|
||||
install(
|
||||
GeneralProcs::GET_NETWORK_INFO, json_adapter(get_network_info), Read)
|
||||
.set_auto_schema<void, GetNetworkInfo::Out>();
|
||||
.set_auto_schema<void, GetNetworkInfo::Out>()
|
||||
.set_http_get_only();
|
||||
install(GeneralProcs::LIST_METHODS, json_adapter(list_methods_fn), Read)
|
||||
.set_auto_schema<void, ListMethods::Out>();
|
||||
.set_auto_schema<void, ListMethods::Out>()
|
||||
.set_http_get_only();
|
||||
install(GeneralProcs::GET_SCHEMA, json_adapter(get_schema), Read)
|
||||
.set_auto_schema<GetSchema>();
|
||||
.set_auto_schema<GetSchema>()
|
||||
.set_http_get_only();
|
||||
install(GeneralProcs::GET_RECEIPT, json_adapter(get_receipt), Read)
|
||||
.set_auto_schema<GetReceipt>();
|
||||
.set_auto_schema<GetReceipt>()
|
||||
.set_http_get_only();
|
||||
install(GeneralProcs::VERIFY_RECEIPT, json_adapter(verify_receipt), Read)
|
||||
.set_auto_schema<VerifyReceipt>();
|
||||
}
|
||||
|
|
|
@ -403,6 +403,28 @@ namespace ccf
|
|||
if (handler == nullptr)
|
||||
{
|
||||
ctx->set_response_status(HTTP_STATUS_NOT_FOUND);
|
||||
ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE, http::headervalues::contenttype::TEXT);
|
||||
ctx->set_response_body(fmt::format("Unknown RPC: {}", method));
|
||||
return ctx->serialise_response();
|
||||
}
|
||||
|
||||
if (!(handler->allowed_verbs_mask &
|
||||
verb_to_mask(ctx->get_request_verb())))
|
||||
{
|
||||
ctx->set_response_status(HTTP_STATUS_METHOD_NOT_ALLOWED);
|
||||
std::string allow_header_value;
|
||||
bool first = true;
|
||||
for (size_t verb = 0; verb <= HTTP_SOURCE; ++verb)
|
||||
{
|
||||
if (handler->allowed_verbs_mask & verb_to_mask(verb))
|
||||
{
|
||||
allow_header_value += fmt::format(
|
||||
"{}{}", (first ? "" : ", "), http_method_str((http_method)verb));
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
ctx->set_response_header(http::headers::ALLOW, allow_header_value);
|
||||
return ctx->serialise_response();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
|
||||
#include "ds/json_schema.h"
|
||||
#include "enclave/rpccontext.h"
|
||||
#include "http/http_consts.h"
|
||||
#include "node/certs.h"
|
||||
#include "serialization.h"
|
||||
|
||||
#include <functional>
|
||||
#include <http-parser/http_parser.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <set>
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
|
@ -19,6 +22,11 @@ namespace ccf
|
|||
CallerId caller_id;
|
||||
};
|
||||
|
||||
static uint64_t verb_to_mask(size_t verb)
|
||||
{
|
||||
return 1ul << verb;
|
||||
}
|
||||
|
||||
using HandleFunction = std::function<void(RequestArgs& args)>;
|
||||
|
||||
class HandlerRegistry
|
||||
|
@ -119,6 +127,34 @@ namespace ccf
|
|||
execute_locally = v;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Bit mask. Bit i is 1 iff the http_method with value i is allowed.
|
||||
// Default is that all verbs are allowed
|
||||
uint64_t allowed_verbs_mask = ~0;
|
||||
|
||||
Handler& set_allowed_verbs(std::set<http_method>&& allowed_verbs)
|
||||
{
|
||||
// Reset mask to disallow everything
|
||||
allowed_verbs_mask = 0;
|
||||
|
||||
// Set bit for each allowed verb
|
||||
for (const auto& verb : allowed_verbs)
|
||||
{
|
||||
allowed_verbs_mask |= verb_to_mask(verb);
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Handler& set_http_get_only()
|
||||
{
|
||||
return set_allowed_verbs({HTTP_GET});
|
||||
}
|
||||
|
||||
Handler& set_http_post_only()
|
||||
{
|
||||
return set_allowed_verbs({HTTP_POST});
|
||||
}
|
||||
};
|
||||
|
||||
protected:
|
||||
|
|
|
@ -182,7 +182,10 @@ namespace ccf
|
|||
const auto pack = detect_json_pack(ctx);
|
||||
|
||||
nlohmann::json params = nullptr;
|
||||
if (!ctx->get_request_body().empty())
|
||||
if (
|
||||
!ctx->get_request_body().empty()
|
||||
// Body of GET is ignored
|
||||
&& ctx->get_request_verb() != HTTP_GET)
|
||||
{
|
||||
params = get_params_from_body(ctx, pack);
|
||||
}
|
||||
|
|
|
@ -465,7 +465,7 @@ namespace ccf
|
|||
auto read = [this](
|
||||
Store::Tx& tx,
|
||||
CallerId caller_id,
|
||||
const nlohmann::json& params) {
|
||||
nlohmann::json&& params) {
|
||||
if (!check_member_status(
|
||||
tx, caller_id, {MemberStatus::ACTIVE, MemberStatus::ACCEPTED}))
|
||||
{
|
||||
|
@ -496,8 +496,7 @@ namespace ccf
|
|||
.set_auto_schema<KVRead>();
|
||||
|
||||
auto query =
|
||||
[this](
|
||||
Store::Tx& tx, CallerId caller_id, const nlohmann::json& params) {
|
||||
[this](Store::Tx& tx, CallerId caller_id, nlohmann::json&& params) {
|
||||
if (!check_member_accepted(tx, caller_id))
|
||||
{
|
||||
return make_error(HTTP_STATUS_FORBIDDEN, "Member is not accepted");
|
||||
|
@ -510,7 +509,7 @@ namespace ccf
|
|||
install(MemberProcs::QUERY, json_adapter(query), Read)
|
||||
.set_auto_schema<Script, nlohmann::json>();
|
||||
|
||||
auto propose = [this](RequestArgs& args, const nlohmann::json& params) {
|
||||
auto propose = [this](RequestArgs& args, nlohmann::json&& params) {
|
||||
if (!check_member_active(args.tx, args.caller_id))
|
||||
{
|
||||
return make_error(HTTP_STATUS_FORBIDDEN, "Member is not active");
|
||||
|
@ -534,7 +533,7 @@ namespace ccf
|
|||
install(MemberProcs::PROPOSE, json_adapter(propose), Write)
|
||||
.set_auto_schema<Propose>();
|
||||
|
||||
auto withdraw = [this](RequestArgs& args, const nlohmann::json& params) {
|
||||
auto withdraw = [this](RequestArgs& args, nlohmann::json&& params) {
|
||||
if (!check_member_active(args.tx, args.caller_id))
|
||||
{
|
||||
return make_error(HTTP_STATUS_FORBIDDEN, "Member is not active");
|
||||
|
@ -586,7 +585,7 @@ namespace ccf
|
|||
.set_auto_schema<ProposalAction, ProposalInfo>()
|
||||
.set_require_client_signature(true);
|
||||
|
||||
auto vote = [this](RequestArgs& args, const nlohmann::json& params) {
|
||||
auto vote = [this](RequestArgs& args, nlohmann::json&& params) {
|
||||
if (!check_member_active(args.tx, args.caller_id))
|
||||
{
|
||||
return make_error(HTTP_STATUS_FORBIDDEN, "Member is not active");
|
||||
|
@ -634,8 +633,7 @@ namespace ccf
|
|||
.set_require_client_signature(true);
|
||||
|
||||
auto complete =
|
||||
[this](
|
||||
Store::Tx& tx, CallerId caller_id, const nlohmann::json& params) {
|
||||
[this](Store::Tx& tx, CallerId caller_id, nlohmann::json&& params) {
|
||||
if (!check_member_active(tx, caller_id))
|
||||
{
|
||||
return make_error(HTTP_STATUS_FORBIDDEN, "Member is not active");
|
||||
|
@ -661,7 +659,7 @@ namespace ccf
|
|||
.set_require_client_signature(true);
|
||||
|
||||
//! A member acknowledges state
|
||||
auto ack = [this](RequestArgs& args, const nlohmann::json& params) {
|
||||
auto ack = [this](RequestArgs& args, nlohmann::json&& params) {
|
||||
const auto signed_request = args.rpc_ctx->get_signed_request();
|
||||
|
||||
auto [ma_view, sig_view] =
|
||||
|
@ -707,8 +705,7 @@ namespace ccf
|
|||
|
||||
//! A member asks for a fresher state digest
|
||||
auto update_state_digest =
|
||||
[this](
|
||||
Store::Tx& tx, CallerId caller_id, const nlohmann::json& params) {
|
||||
[this](Store::Tx& tx, CallerId caller_id, nlohmann::json&& params) {
|
||||
auto [ma_view, sig_view] =
|
||||
tx.get_view(this->network.member_acks, this->network.signatures);
|
||||
auto ma = ma_view->get(caller_id);
|
||||
|
@ -737,7 +734,7 @@ namespace ccf
|
|||
.set_auto_schema<void, StateDigest>();
|
||||
|
||||
auto get_encrypted_recovery_share =
|
||||
[this](RequestArgs& args, const nlohmann::json& params) {
|
||||
[this](RequestArgs& args, nlohmann::json&& params) {
|
||||
// This check should depend on whether new shares are emitted when a
|
||||
// new member is added (status = Accepted) or when the new member acks
|
||||
// (status = Active).
|
||||
|
@ -781,7 +778,7 @@ namespace ccf
|
|||
|
||||
auto submit_recovery_share = [this](
|
||||
RequestArgs& args,
|
||||
const nlohmann::json& params) {
|
||||
nlohmann::json&& params) {
|
||||
// Only active members can submit their shares for recovery
|
||||
if (!check_member_active(args.tx, args.caller_id))
|
||||
{
|
||||
|
@ -831,7 +828,7 @@ namespace ccf
|
|||
Write)
|
||||
.set_auto_schema<SubmitRecoveryShare, bool>();
|
||||
|
||||
auto create = [this](Store::Tx& tx, const nlohmann::json& params) {
|
||||
auto create = [this](Store::Tx& tx, nlohmann::json&& params) {
|
||||
LOG_DEBUG_FMT("Processing create RPC");
|
||||
const auto in = params.get<CreateNetworkNodeToNode::In>();
|
||||
|
||||
|
|
|
@ -300,11 +300,14 @@ namespace ccf
|
|||
|
||||
install(NodeProcs::JOIN, json_adapter(accept), Write);
|
||||
install(NodeProcs::GET_SIGNED_INDEX, json_adapter(get_signed_index), Read)
|
||||
.set_auto_schema<GetSignedIndex>();
|
||||
.set_auto_schema<GetSignedIndex>()
|
||||
.set_http_get_only();
|
||||
install(NodeProcs::GET_NODE_QUOTE, json_adapter(get_quote), Read)
|
||||
.set_auto_schema<GetQuotes>();
|
||||
.set_auto_schema<GetQuotes>()
|
||||
.set_http_get_only();
|
||||
install(NodeProcs::GET_QUOTES, json_adapter(get_quotes), Read)
|
||||
.set_auto_schema<GetQuotes>();
|
||||
.set_auto_schema<GetQuotes>()
|
||||
.set_http_get_only();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ public:
|
|||
"get_caller", json_adapter(get_caller_function), HandlerRegistry::Read);
|
||||
|
||||
auto failable_function =
|
||||
[this](Store::Tx& tx, CallerId caller_id, const nlohmann::json& params) {
|
||||
[this](Store::Tx& tx, CallerId caller_id, nlohmann::json&& params) {
|
||||
const auto it = params.find("error");
|
||||
if (it != params.end())
|
||||
{
|
||||
|
@ -113,6 +113,31 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
class TestRestrictedVerbsFrontend : public SimpleUserRpcFrontend
|
||||
{
|
||||
public:
|
||||
TestRestrictedVerbsFrontend(Store& tables) : SimpleUserRpcFrontend(tables)
|
||||
{
|
||||
open();
|
||||
|
||||
auto get_only = [this](RequestArgs& args) {
|
||||
args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
|
||||
};
|
||||
install("get_only", get_only, HandlerRegistry::Read).set_http_get_only();
|
||||
|
||||
auto post_only = [this](RequestArgs& args) {
|
||||
args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
|
||||
};
|
||||
install("post_only", post_only, HandlerRegistry::Read).set_http_post_only();
|
||||
|
||||
auto put_or_delete = [this](RequestArgs& args) {
|
||||
args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
|
||||
};
|
||||
install("put_or_delete", put_or_delete, HandlerRegistry::Read)
|
||||
.set_allowed_verbs({HTTP_PUT, HTTP_DELETE});
|
||||
}
|
||||
};
|
||||
|
||||
class TestMemberFrontend : public MemberRpcFrontend
|
||||
{
|
||||
public:
|
||||
|
@ -807,6 +832,81 @@ TEST_CASE("MinimalHandleFunction")
|
|||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Restricted verbs")
|
||||
{
|
||||
prepare_callers();
|
||||
TestRestrictedVerbsFrontend frontend(*network.tables);
|
||||
|
||||
for (auto verb = HTTP_DELETE; verb <= HTTP_SOURCE;
|
||||
verb = (http_method)(size_t(verb) + 1))
|
||||
{
|
||||
INFO(http_method_str(verb));
|
||||
|
||||
{
|
||||
http::Request get("get_only", verb);
|
||||
const auto serialized_get = get.build_request();
|
||||
auto rpc_ctx = enclave::make_rpc_context(user_session, serialized_get);
|
||||
const auto serialized_response = frontend.process(rpc_ctx).value();
|
||||
const auto response = parse_response(serialized_response);
|
||||
if (verb == HTTP_GET)
|
||||
{
|
||||
CHECK(response.status == HTTP_STATUS_OK);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(response.status == HTTP_STATUS_METHOD_NOT_ALLOWED);
|
||||
const auto it = response.headers.find(http::headers::ALLOW);
|
||||
REQUIRE(it != response.headers.end());
|
||||
const auto v = it->second;
|
||||
CHECK(v.find(http_method_str(HTTP_GET)) != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
http::Request get("post_only", verb);
|
||||
const auto serialized_post = get.build_request();
|
||||
auto rpc_ctx = enclave::make_rpc_context(user_session, serialized_post);
|
||||
const auto serialized_response = frontend.process(rpc_ctx).value();
|
||||
const auto response = parse_response(serialized_response);
|
||||
if (verb == HTTP_POST)
|
||||
{
|
||||
CHECK(response.status == HTTP_STATUS_OK);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(response.status == HTTP_STATUS_METHOD_NOT_ALLOWED);
|
||||
const auto it = response.headers.find(http::headers::ALLOW);
|
||||
REQUIRE(it != response.headers.end());
|
||||
const auto v = it->second;
|
||||
CHECK(v.find(http_method_str(HTTP_POST)) != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
http::Request get("put_or_delete", verb);
|
||||
const auto serialized_put_or_delete = get.build_request();
|
||||
auto rpc_ctx =
|
||||
enclave::make_rpc_context(user_session, serialized_put_or_delete);
|
||||
const auto serialized_response = frontend.process(rpc_ctx).value();
|
||||
const auto response = parse_response(serialized_response);
|
||||
if (verb == HTTP_PUT || verb == HTTP_DELETE)
|
||||
{
|
||||
CHECK(response.status == HTTP_STATUS_OK);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(response.status == HTTP_STATUS_METHOD_NOT_ALLOWED);
|
||||
const auto it = response.headers.find(http::headers::ALLOW);
|
||||
REQUIRE(it != response.headers.end());
|
||||
const auto v = it->second;
|
||||
CHECK(v.find(http_method_str(HTTP_PUT)) != std::string::npos);
|
||||
CHECK(v.find(http_method_str(HTTP_DELETE)) != std::string::npos);
|
||||
CHECK(v.find(http_method_str(verb)) == std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Signed read requests can be executed on backup")
|
||||
{
|
||||
prepare_callers();
|
||||
|
|
|
@ -110,9 +110,9 @@ def test_forwarding_frontends(network, args):
|
|||
with primary.node_client() as nc:
|
||||
check_commit = infra.checker.Checker(nc)
|
||||
with backup.node_client() as c:
|
||||
check_commit(c.do("mkSign", params={}), result=True)
|
||||
check_commit(c.rpc("mkSign"), result=True)
|
||||
with backup.member_client() as c:
|
||||
check_commit(c.do("mkSign", params={}), result=True)
|
||||
check_commit(c.rpc("mkSign"), result=True)
|
||||
|
||||
return network
|
||||
|
||||
|
@ -143,7 +143,7 @@ def test_update_lua(network, args):
|
|||
member_id=1, remote_node=primary, app_script=new_app_file
|
||||
)
|
||||
with primary.user_client() as c:
|
||||
check(c.rpc("ping", params={}), result="pong")
|
||||
check(c.rpc("ping"), result="pong")
|
||||
|
||||
LOG.debug("Check that former endpoints no longer exists")
|
||||
for endpoint in [
|
||||
|
@ -153,7 +153,7 @@ def test_update_lua(network, args):
|
|||
"LOG_get_pub",
|
||||
]:
|
||||
check(
|
||||
c.rpc(endpoint, params={}),
|
||||
c.rpc(endpoint),
|
||||
error=lambda status, msg: status == http.HTTPStatus.NOT_FOUND.value,
|
||||
)
|
||||
else:
|
||||
|
|
|
@ -42,7 +42,7 @@ def run(args):
|
|||
check = infra.checker.Checker()
|
||||
check_commit = infra.checker.Checker(mc)
|
||||
with primary.user_client() as uc:
|
||||
check_commit(uc.do("mkSign", params={}), result=True)
|
||||
check_commit(uc.rpc("mkSign"), result=True)
|
||||
|
||||
for connection in scenario["connections"]:
|
||||
with (
|
||||
|
|
|
@ -26,7 +26,7 @@ def wait_for_index_globally_committed(index, term, nodes):
|
|||
up_to_date_f = []
|
||||
for f in nodes:
|
||||
with f.node_client() as c:
|
||||
res = c.request("getCommit", {"commit": index})
|
||||
res = c.get("getCommit", {"commit": index})
|
||||
if res.result["term"] == term and (res.global_commit >= index):
|
||||
up_to_date_f.append(f.node_id)
|
||||
if len(up_to_date_f) == len(nodes):
|
||||
|
@ -45,6 +45,7 @@ def run(args):
|
|||
with infra.ccf.network(
|
||||
hosts, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
||||
) as network:
|
||||
check = infra.checker.Checker()
|
||||
|
||||
network.start_and_join(args)
|
||||
current_term = None
|
||||
|
@ -76,15 +77,15 @@ def run(args):
|
|||
)
|
||||
commit_index = None
|
||||
with primary.user_client() as c:
|
||||
res = c.do(
|
||||
res = c.rpc(
|
||||
"LOG_record",
|
||||
{
|
||||
"id": current_term,
|
||||
"msg": "This log is committed in term {}".format(current_term),
|
||||
},
|
||||
readonly_hint=None,
|
||||
expected_result=True,
|
||||
)
|
||||
check(res, result=True)
|
||||
commit_index = res.commit
|
||||
|
||||
LOG.debug("Waiting for transaction to be committed by all nodes")
|
||||
|
|
|
@ -31,7 +31,7 @@ def run(args):
|
|||
with primary.node_client() as mc:
|
||||
check_commit = infra.checker.Checker(mc)
|
||||
check = infra.checker.Checker()
|
||||
r = mc.rpc("getQuotes", {})
|
||||
r = mc.get("getQuotes")
|
||||
quotes = r.result["quotes"]
|
||||
assert len(quotes) == len(hosts)
|
||||
primary_quote = quotes[0]
|
||||
|
|
|
@ -376,7 +376,7 @@ class Network:
|
|||
for _ in range(timeout):
|
||||
try:
|
||||
with node.node_client() as c:
|
||||
r = c.request("getSignedIndex", {})
|
||||
r = c.get("getSignedIndex")
|
||||
if r.result["state"] == state:
|
||||
break
|
||||
except ConnectionRefusedError:
|
||||
|
@ -411,7 +411,7 @@ class Network:
|
|||
for node in self.get_joined_nodes():
|
||||
with node.node_client(request_timeout=request_timeout) as c:
|
||||
try:
|
||||
res = c.do("getPrimaryInfo", {})
|
||||
res = c.get("getPrimaryInfo")
|
||||
if res.error is None:
|
||||
primary_id = res.result["primary_id"]
|
||||
term = res.term
|
||||
|
@ -453,7 +453,7 @@ class Network:
|
|||
which added the nodes).
|
||||
"""
|
||||
with primary.node_client() as c:
|
||||
res = c.do("getCommit", {})
|
||||
res = c.get("getCommit")
|
||||
local_commit_leader = res.commit
|
||||
term_leader = res.term
|
||||
|
||||
|
@ -461,7 +461,7 @@ class Network:
|
|||
caught_up_nodes = []
|
||||
for node in self.get_joined_nodes():
|
||||
with node.node_client() as c:
|
||||
resp = c.request("getCommit", {})
|
||||
resp = c.get("getCommit")
|
||||
if resp.error is not None:
|
||||
# Node may not have joined the network yet, try again
|
||||
break
|
||||
|
@ -486,7 +486,7 @@ class Network:
|
|||
commits = []
|
||||
for node in self.get_joined_nodes():
|
||||
with node.node_client() as c:
|
||||
r = c.request("getCommit", {})
|
||||
r = c.get("getCommit")
|
||||
commits.append(r.commit)
|
||||
if [commits[0]] * len(commits) == commits:
|
||||
break
|
||||
|
|
|
@ -21,12 +21,12 @@ def wait_for_global_commit(node_client, commit_index, term, mksign=False, timeou
|
|||
# Forcing a signature accelerates this process for common operations
|
||||
# (e.g. governance proposals)
|
||||
if mksign:
|
||||
r = node_client.rpc("mkSign", params={})
|
||||
r = node_client.rpc("mkSign")
|
||||
if r.error is not None:
|
||||
raise RuntimeError(f"mkSign returned an error: {r.error}")
|
||||
|
||||
for i in range(timeout * 10):
|
||||
r = node_client.rpc("getCommit", {"commit": commit_index})
|
||||
r = node_client.get("getCommit", {"commit": commit_index})
|
||||
if r.global_commit >= commit_index and r.result["term"] == term:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
|
|
@ -12,6 +12,7 @@ import subprocess
|
|||
import tempfile
|
||||
import base64
|
||||
import requests
|
||||
import urllib.parse
|
||||
from requests_http_signature import HTTPSignatureAuth
|
||||
from http.client import HTTPResponse
|
||||
from io import BytesIO
|
||||
|
@ -36,10 +37,11 @@ CCF_READ_ONLY_HEADER = "x-ccf-read-only"
|
|||
|
||||
|
||||
class Request:
|
||||
def __init__(self, method, params, readonly_hint=None):
|
||||
def __init__(self, method, params=None, readonly_hint=None, http_verb="POST"):
|
||||
self.method = method
|
||||
self.params = params
|
||||
self.readonly_hint = readonly_hint
|
||||
self.http_verb = http_verb
|
||||
|
||||
|
||||
def int_or_none(v):
|
||||
|
@ -113,8 +115,8 @@ def human_readable_size(n):
|
|||
class RPCLogger:
|
||||
def log_request(self, request, name, description):
|
||||
LOG.info(
|
||||
f"{name} {request.method} "
|
||||
+ truncate(f"{request.params}")
|
||||
f"{name} {request.http_verb} /{request.method}"
|
||||
+ (truncate(f" {request.params}") if request.params is not None else "")
|
||||
+ (
|
||||
f" (RO hint: {request.readonly_hint})"
|
||||
if request.readonly_hint is not None
|
||||
|
@ -145,7 +147,7 @@ class RPCFileLogger(RPCLogger):
|
|||
|
||||
def log_request(self, request, name, description):
|
||||
with open(self.path, "a") as f:
|
||||
f.write(f">> Request: {request.method}" + os.linesep)
|
||||
f.write(f">> Request: {request.http_verb} /{request.method}" + os.linesep)
|
||||
json.dump(request.params, f, indent=2)
|
||||
f.write(os.linesep)
|
||||
|
||||
|
@ -160,6 +162,13 @@ class CCFConnectionException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
def build_query_string(params):
|
||||
return "&".join(
|
||||
f"{urllib.parse.quote_plus(k)}={urllib.parse.quote_plus(json.dumps(v))}"
|
||||
for k, v in params.items()
|
||||
)
|
||||
|
||||
|
||||
class CurlClient:
|
||||
"""
|
||||
We keep this around in a limited fashion still, because
|
||||
|
@ -190,25 +199,39 @@ class CurlClient:
|
|||
|
||||
def _just_request(self, request, is_signed=False):
|
||||
with tempfile.NamedTemporaryFile() as nf:
|
||||
msg = json.dumps(request.params).encode()
|
||||
LOG.debug(f"Going to call {request.method} with {msg}")
|
||||
nf.write(msg)
|
||||
nf.flush()
|
||||
if is_signed:
|
||||
cmd = [os.path.join(self.binary_dir, "scurl.sh")]
|
||||
else:
|
||||
cmd = ["curl"]
|
||||
|
||||
url = f"https://{self.host}:{self.port}/{request.method}"
|
||||
|
||||
is_get = request.http_verb == "GET"
|
||||
if is_get:
|
||||
if request.params is not None:
|
||||
url += f"?{build_query_string(request.params)}"
|
||||
|
||||
cmd += [
|
||||
f"https://{self.host}:{self.port}/{request.method}",
|
||||
url,
|
||||
"-X",
|
||||
request.http_verb,
|
||||
"-H",
|
||||
"Content-Type: application/json",
|
||||
"--data-binary",
|
||||
f"@{nf.name}",
|
||||
"-i",
|
||||
f"-m {self.request_timeout}",
|
||||
]
|
||||
|
||||
if not is_get:
|
||||
msg = (
|
||||
json.dumps(request.params).encode()
|
||||
if request.params is not None
|
||||
else bytes()
|
||||
)
|
||||
LOG.debug(f"Writing request body: {msg}")
|
||||
nf.write(msg)
|
||||
nf.flush()
|
||||
cmd.extend(["--data-binary", f"@{nf.name}"])
|
||||
|
||||
if request.readonly_hint:
|
||||
cmd.extend(["-H", f"{CCF_READ_ONLY_HEADER}: true"])
|
||||
|
||||
|
@ -296,13 +319,21 @@ class RequestClient:
|
|||
if request.readonly_hint:
|
||||
extra_headers[CCF_READ_ONLY_HEADER] = "true"
|
||||
|
||||
response = self.session.post(
|
||||
f"https://{self.host}:{self.port}/{request.method}",
|
||||
json=request.params,
|
||||
timeout=self.request_timeout,
|
||||
auth=auth_value,
|
||||
headers=extra_headers,
|
||||
)
|
||||
request_args = {
|
||||
"method": request.http_verb,
|
||||
"url": f"https://{self.host}:{self.port}/{request.method}",
|
||||
"auth": auth_value,
|
||||
"headers": extra_headers,
|
||||
}
|
||||
|
||||
is_get = request.http_verb == "GET"
|
||||
if request.params is not None:
|
||||
if is_get:
|
||||
request_args["params"] = build_query_string(request.params)
|
||||
else:
|
||||
request_args["json"] = request.params
|
||||
|
||||
response = self.session.request(timeout=self.request_timeout, **request_args)
|
||||
return Response.from_requests_response(response)
|
||||
|
||||
def _request(self, request, is_signed=False):
|
||||
|
@ -382,8 +413,8 @@ class CCFClient:
|
|||
logger.log_response(response)
|
||||
return response
|
||||
|
||||
def request(self, method, params, *args, **kwargs):
|
||||
r = Request(f"{self.prefix}/{method}", params, *args, **kwargs)
|
||||
def request(self, method, *args, **kwargs):
|
||||
r = Request(f"{self.prefix}/{method}", *args, **kwargs)
|
||||
description = ""
|
||||
if self.description:
|
||||
description = f" ({self.description})"
|
||||
|
@ -392,8 +423,8 @@ class CCFClient:
|
|||
|
||||
return self._response(self.client_impl.request(r))
|
||||
|
||||
def signed_request(self, method, params, *args, **kwargs):
|
||||
r = Request(f"{self.prefix}/{method}", params, *args, **kwargs)
|
||||
def signed_request(self, method, *args, **kwargs):
|
||||
r = Request(f"{self.prefix}/{method}", *args, **kwargs)
|
||||
|
||||
description = ""
|
||||
if self.description:
|
||||
|
@ -403,29 +434,15 @@ class CCFClient:
|
|||
|
||||
return self._response(self.client_impl.signed_request(r))
|
||||
|
||||
def do(self, *args, **kwargs):
|
||||
expected_result = None
|
||||
expected_error_code = None
|
||||
if "expected_result" in kwargs:
|
||||
expected_result = kwargs.pop("expected_result")
|
||||
if "expected_error_code" in kwargs:
|
||||
expected_error_code = kwargs.pop("expected_error_code")
|
||||
|
||||
r = self.rpc(*args, **kwargs)
|
||||
|
||||
if expected_result is not None:
|
||||
assert expected_result == r.result
|
||||
|
||||
if expected_error_code is not None:
|
||||
assert expected_error_code == r.error["code"]
|
||||
return r
|
||||
|
||||
def rpc(self, *args, **kwargs):
|
||||
if "signed" in kwargs and kwargs.pop("signed"):
|
||||
return self.signed_request(*args, **kwargs)
|
||||
else:
|
||||
return self.request(*args, **kwargs)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.rpc(*args, http_verb="GET", **kwargs)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def client(
|
||||
|
|
|
@ -136,7 +136,7 @@ class Consortium:
|
|||
|
||||
def update_ack_state_digest(self, member_id, remote_node):
|
||||
with remote_node.member_client(member_id=member_id) as mc:
|
||||
res = mc.rpc("updateAckStateDigest", params={})
|
||||
res = mc.rpc("updateAckStateDigest")
|
||||
return bytearray(res.result["state_digest"])
|
||||
|
||||
def ack(self, member_id, remote_node):
|
||||
|
@ -156,7 +156,7 @@ class Consortium:
|
|||
"""
|
||||
|
||||
with remote_node.member_client(member_id=member_id) as c:
|
||||
rep = c.do("query", {"text": script})
|
||||
rep = c.rpc("query", {"text": script})
|
||||
return rep.result
|
||||
|
||||
def propose_retire_node(self, member_id, remote_node, node_id):
|
||||
|
@ -312,7 +312,7 @@ class Consortium:
|
|||
def get_decrypt_and_submit_shares(self, remote_node):
|
||||
for m in self.members:
|
||||
with remote_node.member_client(member_id=m) as mc:
|
||||
r = mc.rpc("getEncryptedRecoveryShare", params={})
|
||||
r = mc.rpc("getEncryptedRecoveryShare")
|
||||
|
||||
# For now, members rely on a copy of the original network encryption public key
|
||||
ctx = infra.crypto.CryptoBoxCtx(
|
||||
|
@ -356,7 +356,7 @@ class Consortium:
|
|||
# When opening the service in PBFT, the first transaction to be
|
||||
# completed when f = 1 takes a significant amount of time
|
||||
with remote_node.member_client(request_timeout=(30 if pbft_open else 3)) as c:
|
||||
rep = c.do(
|
||||
rep = c.rpc(
|
||||
"query",
|
||||
{
|
||||
"text": """tables = ...
|
||||
|
@ -378,7 +378,7 @@ class Consortium:
|
|||
|
||||
def _check_node_exists(self, remote_node, node_id, node_status=None):
|
||||
with remote_node.member_client() as c:
|
||||
rep = c.do("read", {"table": "ccf.nodes", "key": node_id})
|
||||
rep = c.rpc("read", {"table": "ccf.nodes", "key": node_id})
|
||||
|
||||
if rep.error is not None or (
|
||||
node_status and rep.result["status"] != node_status.name
|
||||
|
|
|
@ -204,7 +204,7 @@ class Node:
|
|||
# is not yet endorsed by the network certificate
|
||||
try:
|
||||
with self.node_client(connection_timeout=timeout) as nc:
|
||||
rep = nc.do("getCommit", {})
|
||||
rep = nc.get("getCommit")
|
||||
assert (
|
||||
rep.error is None and rep.result is not None
|
||||
), f"An error occured after node {self.node_id} joined the network"
|
||||
|
|
|
@ -55,7 +55,7 @@ class TxRates:
|
|||
|
||||
def process_next(self):
|
||||
with self.primary.user_client() as client:
|
||||
rv = client.rpc("getCommit", {})
|
||||
rv = client.get("getCommit")
|
||||
result = rv.to_dict()
|
||||
next_commit = result["result"]["commit"]
|
||||
more_to_process = self.commit != next_commit
|
||||
|
@ -65,7 +65,7 @@ class TxRates:
|
|||
|
||||
def get_metrics(self):
|
||||
with self.primary.user_client() as client:
|
||||
rv = client.rpc("getMetrics", {})
|
||||
rv = client.get("getMetrics")
|
||||
result = rv.to_dict()
|
||||
result = result["result"]
|
||||
self.all_metrics = result
|
||||
|
|
|
@ -34,7 +34,7 @@ def test(network, args, notifications_queue=None):
|
|||
check_commit(c.rpc("LOG_record", {"id": 42, "msg": msg}), result=True)
|
||||
r = c.rpc("LOG_get", {"id": 42})
|
||||
check(r, result={"msg": msg})
|
||||
r = c.rpc("getReceipt", {"commit": r.commit})
|
||||
r = c.get("getReceipt", {"commit": r.commit})
|
||||
check(
|
||||
c.rpc("verifyReceipt", {"receipt": r.result["receipt"]}),
|
||||
result={"valid": True},
|
||||
|
|
|
@ -17,7 +17,7 @@ def check_can_progress(node):
|
|||
with node.node_client() as mc:
|
||||
check_commit = infra.checker.Checker(mc)
|
||||
with node.node_client() as c:
|
||||
check_commit(c.rpc("mkSign", params={}), result=True)
|
||||
check_commit(c.rpc("mkSign"), result=True)
|
||||
|
||||
|
||||
@reqs.description("Adding a valid node from primary")
|
||||
|
|
|
@ -18,7 +18,7 @@ def test(network, args):
|
|||
# Retrieve current index version to check for sealed secrets later
|
||||
with primary.node_client() as nc:
|
||||
check_commit = infra.checker.Checker(nc)
|
||||
res = nc.rpc("mkSign", params={})
|
||||
res = nc.rpc("mkSign")
|
||||
check_commit(res, result=True)
|
||||
version_before_rekey = res.commit
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import sys
|
||||
import getpass
|
||||
import json
|
||||
import http
|
||||
import time
|
||||
import logging
|
||||
import multiprocessing
|
||||
|
@ -25,14 +26,19 @@ def run(args):
|
|||
methods_without_schema = set()
|
||||
|
||||
def fetch_schema(client):
|
||||
list_response = client.rpc("listMethods", {})
|
||||
check(list_response)
|
||||
list_response = client.get("listMethods")
|
||||
check(
|
||||
list_response, error=lambda status, msg: status == http.HTTPStatus.OK.value
|
||||
)
|
||||
methods = list_response.result["methods"]
|
||||
|
||||
for method in methods:
|
||||
schema_found = False
|
||||
schema_response = client.rpc("getSchema", {"method": method})
|
||||
check(schema_response)
|
||||
schema_response = client.get(f"getSchema", params={"method": method})
|
||||
check(
|
||||
schema_response,
|
||||
error=lambda status, msg: status == http.HTTPStatus.OK.value,
|
||||
)
|
||||
|
||||
if schema_response.result is not None:
|
||||
for schema_type in ["params", "result"]:
|
||||
|
|
|
@ -50,7 +50,7 @@ def supports_methods(*methods):
|
|||
def check(network, args, *nargs, **kwargs):
|
||||
primary, term = network.find_primary()
|
||||
with primary.user_client() as c:
|
||||
response = c.rpc("listMethods", {})
|
||||
response = c.get("listMethods")
|
||||
supported_methods = response.result["methods"]
|
||||
missing = {*methods}.difference(supported_methods)
|
||||
if missing:
|
||||
|
|
Загрузка…
Ссылка в новой задаче