mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-25 05:07:17 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
198e69d10d | ||
|
|
b1ce7de738 | ||
|
|
2c165c8be1 | ||
|
|
d2f1331748 |
No files matched your search
+71
-5
@@ -315,6 +315,9 @@
|
||||
# - The admin RPC "pwa_info" reports requests by outcome and bytes
|
||||
# served; "reset": true zeroes them, "flush_dns": true drops the
|
||||
# custom domain cache (CLI: xahaud pwa_info [reset] [flush_dns]).
|
||||
# - Which accounts are served is set node-wide, not per port, by
|
||||
# [pwa_allowlist], [pwa_blocklist], [pwa_list_file] and
|
||||
# [pwa_list_providers]: see "PWA lists" in section 3.
|
||||
#
|
||||
# Example (also add port_pwa to the [server] list):
|
||||
#
|
||||
@@ -375,11 +378,6 @@
|
||||
# the node but can still send requests anywhere, including the
|
||||
# visitor's own machine, so keep admin ports behind credentials.
|
||||
#
|
||||
# [pwa_allowlist] and [pwa_blocklist], one r-address per line, apply to
|
||||
# every pwa port: if the allowlist is non-empty only those accounts are
|
||||
# served; blocklisted accounts are never served (403), whatever the
|
||||
# allowlist says. An account on both is a startup error.
|
||||
#
|
||||
# ssl_key = <filename>
|
||||
# ssl_cert = <filename>
|
||||
# ssl_chain = <filename>
|
||||
@@ -1025,6 +1023,74 @@
|
||||
# And the ledger is built by applying the transactions to the parent
|
||||
# ledger.
|
||||
#
|
||||
#
|
||||
#
|
||||
# PWA lists
|
||||
#
|
||||
# Which accounts' AppLoader documents the pwa ports serve (see "pwa ports"
|
||||
# in section 1). Node-wide: every pwa port applies the same rules, in
|
||||
# this order:
|
||||
#
|
||||
# 1. [pwa_blocklist]: never served (403).
|
||||
# 2. [pwa_allowlist]: served.
|
||||
# 3. [pwa_list_providers]: the first provider with a hook state entry
|
||||
# for the account decides.
|
||||
# 4. Listed nowhere: served, unless [pwa_allowlist] is non-empty.
|
||||
#
|
||||
# Steps 1, 2 and 4 are settled before a request is queued; step 3 reads
|
||||
# the validated ledger.
|
||||
#
|
||||
#
|
||||
# [pwa_allowlist]
|
||||
# [pwa_blocklist]
|
||||
#
|
||||
# One r-address per line. Entries here are fixed for the node's lifetime;
|
||||
# an account on both lists is a startup error. The same two sections may
|
||||
# also live in [pwa_list_file], below, where they can be changed at
|
||||
# runtime; the two sources are merged.
|
||||
#
|
||||
# Example:
|
||||
# [pwa_blocklist]
|
||||
# rBadActor...
|
||||
#
|
||||
#
|
||||
# [pwa_list_file]
|
||||
#
|
||||
# Path of a file holding [pwa_allowlist] and [pwa_blocklist] in the same
|
||||
# format ('#' comments allowed), relative to the folder of xahaud.cfg
|
||||
# unless absolute. Set this to change the lists without a restart: the
|
||||
# admin RPC pwa_list rewrites the file (atomically, keeping its mode;
|
||||
# comments are dropped and entries sorted) before the change takes
|
||||
# effect, and starts from the file as it is on disk, so edits made by
|
||||
# hand are kept. "pwa_list reload" applies hand edits without a change.
|
||||
# The file need not exist yet, but its directory must, and xahaud must
|
||||
# be able to write there. Entries from xahaud.cfg cannot be removed by
|
||||
# the RPC. Without this setting pwa_list only shows the lists.
|
||||
#
|
||||
# pwa_list show the lists
|
||||
# pwa_list block rBad... add to the blocklist
|
||||
# pwa_list unblock rOk... allow rOk... move: removals apply first
|
||||
# pwa_list reload pick up hand edits
|
||||
#
|
||||
# Over JSON: {"allow" | "block" | "unallow" | "unblock": r-address or
|
||||
# array, "reload": bool}. A request applies whole or not at all.
|
||||
#
|
||||
# Example:
|
||||
# /var/lib/xahaud/pwa-lists.txt
|
||||
#
|
||||
#
|
||||
# [pwa_list_providers]
|
||||
#
|
||||
# One r-address per line, most trusted first. Each is an account whose
|
||||
# hook publishes a list on-ledger as hook state under the namespace
|
||||
# 5057415F414C4C4F575F424C4F434B5F4C495354000000000000000000000000
|
||||
# (ASCII "PWA_ALLOW_BLOCK_LIST"),
|
||||
# with key = the listed account's 20-byte AccountID, zero padded on the
|
||||
# left (as state_set pads it): all-zero data blocks, any other data
|
||||
# allows, no entry means this provider has no opinion and the next is
|
||||
# tried. Consulted only for accounts on neither local list. Providers
|
||||
# are configured here only, not in [pwa_list_file]; pwa_list shows them.
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 4. HTTPS Client
|
||||
|
||||
@@ -115,6 +115,14 @@ std::size_t constexpr maxAppLoaderLength = 4096;
|
||||
*/
|
||||
std::uint64_t constexpr appLoaderFeeDropsPerByte = 1;
|
||||
|
||||
/** Hook state namespace of PWA allow/block list providers: key = the
|
||||
listed AccountID, zero padded on the left as state_set does; all-zero
|
||||
data blocks, any other data allows. A convention between provider hooks
|
||||
and the nodes reading them, not consensus. ASCII "PWA_ALLOW_BLOCK_LIST".
|
||||
*/
|
||||
uint256 const pwaListNamespace{
|
||||
"5057415F414C4C4F575F424C4F434B5F4C495354000000000000000000000000"};
|
||||
|
||||
/** The maximum length of a URI inside a Credential */
|
||||
std::size_t constexpr maxCredentialURILength = 256;
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ struct Port
|
||||
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
|
||||
// "pwa" ports only; see the "pwa ports" section of xahaud-example.cfg.
|
||||
std::set<std::string> pwa_domains; // zones, lower case, no trailing dot
|
||||
std::set<AccountID> pwa_allowlist; // if non-empty, only these are served
|
||||
std::set<AccountID> pwa_blocklist; // never served; wins over allowlist
|
||||
bool pwa_custom_domains = false; // serve hosts named by TXT records
|
||||
std::optional<beast::IP::Endpoint> pwa_resolver; // for those; else system
|
||||
std::string user;
|
||||
|
||||
@@ -32,9 +32,11 @@
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/beast/test/yield_to.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -49,6 +51,8 @@
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -375,6 +379,12 @@ class PWAServe_test : public beast::unit_test::suite,
|
||||
return env.rpc("json", "pwa_info", params)[jss::result];
|
||||
}
|
||||
|
||||
Json::Value
|
||||
pwaList(jtx::Env& env, std::string const& params = "{}")
|
||||
{
|
||||
return env.rpc("json", "pwa_list", params)[jss::result];
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
@@ -453,6 +463,43 @@ class PWAServe_test : public beast::unit_test::suite,
|
||||
c["pwa_allowlist"].append(a.human());
|
||||
c["pwa_blocklist"].append(a.human());
|
||||
}));
|
||||
|
||||
// The lists file need not exist yet, but its directory must; what it
|
||||
// holds must parse, and count with the cfg's lists.
|
||||
beast::temp_dir td;
|
||||
auto withFile = [&](std::string const& text,
|
||||
std::function<void(Config&)> const& more = {}) {
|
||||
auto const f = td.file("lists.txt");
|
||||
boost::system::error_code ec;
|
||||
writeFileContents(ec, f, text);
|
||||
return accepts([&](Config& c) {
|
||||
c[SECTION_PWA_LIST_FILE].legacy(f);
|
||||
if (more)
|
||||
more(c);
|
||||
});
|
||||
};
|
||||
BEAST_EXPECT(withFile("# nothing listed\n"));
|
||||
BEAST_EXPECT(withFile(
|
||||
" [pwa_allowlist]\r\n" + a.human() + "\r\n[pwa_blocklist]\n"));
|
||||
for (auto const& bad :
|
||||
{a.human() + "\n",
|
||||
"[pwa_list_providers]\n" + a.human() + "\n",
|
||||
std::string("[pwa_blocklist]\nnot-an-address\n"),
|
||||
"[pwa_blocklist]\n" + toBase58(AccountID{}) + "\n",
|
||||
"[pwa_allowlist]\n" + a.human() + "\n[pwa_blocklist]\n" +
|
||||
a.human() + "\n"})
|
||||
BEAST_EXPECTS(!withFile(bad), bad);
|
||||
BEAST_EXPECT(!withFile(
|
||||
"[pwa_allowlist]\n" + a.human() + "\n",
|
||||
[&](Config& c) { c["pwa_blocklist"].append(a.human()); }));
|
||||
BEAST_EXPECT(accepts([&](Config& c) {
|
||||
c[SECTION_PWA_LIST_FILE].legacy(td.file("absent.txt"));
|
||||
}));
|
||||
BEAST_EXPECT(!accepts([&](Config& c) {
|
||||
c[SECTION_PWA_LIST_FILE].legacy(td.file("absent/lists.txt"));
|
||||
}));
|
||||
BEAST_EXPECT(!accepts(
|
||||
[&](Config& c) { c[SECTION_PWA_LIST_FILE].legacy(td.path()); }));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -741,6 +788,227 @@ class PWAServe_test : public beast::unit_test::suite,
|
||||
{200, 200, 403, 403});
|
||||
}
|
||||
|
||||
void
|
||||
testListRPC(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("pwa_list edits the lists at runtime, through the lists file");
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"};
|
||||
beast::temp_dir td;
|
||||
auto const file = td.file("pwa-lists.txt");
|
||||
auto const cfg = [&] {
|
||||
return pwaConfig([&](Config& c) {
|
||||
c[SECTION_PWA_LIST_FILE].legacy(file);
|
||||
c["pwa_blocklist"].append(carol.human());
|
||||
});
|
||||
};
|
||||
using Names = std::set<std::string>;
|
||||
auto const names = [](Json::Value const& v) {
|
||||
Names out;
|
||||
for (auto const& e : v)
|
||||
out.insert(e.asString());
|
||||
return out;
|
||||
};
|
||||
auto const onDisk = [&] {
|
||||
boost::system::error_code ec;
|
||||
return getFileContents(ec, file);
|
||||
};
|
||||
auto const one = [](char const* key, Account const& a) {
|
||||
return std::string("{\"") + key + "\": \"" + a.human() + "\"}";
|
||||
};
|
||||
|
||||
{
|
||||
Env env{*this, cfg()};
|
||||
env.fund(XRP(1000), alice, bob, carol);
|
||||
for (auto const& a : {alice, bob, carol})
|
||||
publish(env, a, doc());
|
||||
auto const status = [&](Account const& a) {
|
||||
return getDoc(env, a, yield).result_int();
|
||||
};
|
||||
|
||||
// Nothing in the file yet, which need not exist; carol is fixed.
|
||||
auto r = pwaList(env);
|
||||
BEAST_EXPECT(r[jss::status] == "success" && r["changed"] == false);
|
||||
BEAST_EXPECT(
|
||||
r["allowlist"].size() == 0 && r["blocklist"].size() == 0);
|
||||
BEAST_EXPECT(names(r["cfg_blocklist"]) == Names{carol.human()});
|
||||
BEAST_EXPECT(!r.isMember("cfg_allowlist") && r["file"] == file);
|
||||
BEAST_EXPECT(!boost::filesystem::exists(file));
|
||||
|
||||
// An edit is written to the file and applies at once.
|
||||
r = pwaList(env, one("block", bob));
|
||||
BEAST_EXPECT(r["changed"] == true);
|
||||
BEAST_EXPECT(names(r["blocklist"]) == Names{bob.human()});
|
||||
BEAST_EXPECT(
|
||||
onDisk().find("[pwa_blocklist]\n" + bob.human() + "\n") !=
|
||||
std::string::npos);
|
||||
BEAST_EXPECT(
|
||||
status(alice) == 200 && status(bob) == 403 &&
|
||||
status(carol) == 403);
|
||||
BEAST_EXPECT(pwaList(env, one("block", bob))["changed"] == false);
|
||||
|
||||
// Removals come first, so a move is one step: never on neither.
|
||||
r = env.rpc(
|
||||
"pwa_list",
|
||||
"unblock",
|
||||
bob.human(),
|
||||
"allow",
|
||||
bob.human())[jss::result];
|
||||
BEAST_EXPECT(r["changed"] == true && r["blocklist"].size() == 0);
|
||||
BEAST_EXPECT(names(r["allowlist"]) == Names{bob.human()});
|
||||
BEAST_EXPECT(status(alice) == 403 && status(bob) == 200);
|
||||
|
||||
// Refused whole: cfg entries, both lists, ambiguity, bad input.
|
||||
auto const text = onDisk();
|
||||
for (auto const& bad :
|
||||
{one("unblock", carol),
|
||||
one("allow", carol),
|
||||
one("block", bob),
|
||||
R"({"allow": [")" + alice.human() + R"("], "unallow": ")" +
|
||||
alice.human() + R"("})",
|
||||
R"({"block": [")" + alice.human() + R"(", "nope"]})",
|
||||
std::string(R"({"allow": "rrrrrrrrrrrrrrrrrrrrrhoLvTp"})"),
|
||||
std::string(R"({"allow": {}})"),
|
||||
std::string(R"({"block": [1]})"),
|
||||
std::string(R"({"reload": 1})")})
|
||||
BEAST_EXPECTS(
|
||||
pwaList(env, bad)[jss::error] == "invalidParams", bad);
|
||||
BEAST_EXPECT(onDisk() == text);
|
||||
BEAST_EXPECT(status(alice) == 403 && status(bob) == 200);
|
||||
BEAST_EXPECT(
|
||||
env.rpc("pwa_list", "allow")[jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(
|
||||
env.rpc("pwa_list", "add", alice.human())[jss::error] ==
|
||||
"invalidParams");
|
||||
BEAST_EXPECT(
|
||||
env.rpc("pwa_list", "reload", "reload")[jss::error] ==
|
||||
"invalidParams");
|
||||
|
||||
// Hand edits wait for reload (or the next edit, which keeps them).
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
writeFileContents(
|
||||
ec,
|
||||
file,
|
||||
text + "[pwa_allowlist]\n" + alice.human() + "\n");
|
||||
}
|
||||
BEAST_EXPECT(status(alice) == 403);
|
||||
r = env.rpc("pwa_list", "reload")[jss::result];
|
||||
BEAST_EXPECT(r["changed"] == true);
|
||||
BEAST_EXPECT(
|
||||
names(r["allowlist"]) == Names({alice.human(), bob.human()}));
|
||||
BEAST_EXPECT(status(alice) == 200);
|
||||
|
||||
// A file broken by hand is reported and changes nothing.
|
||||
auto const good = onDisk();
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
writeFileContents(ec, file, good + "[nope]\n");
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
pwaList(env, R"({"reload": true})")[jss::error] == "internal");
|
||||
BEAST_EXPECT(
|
||||
pwaList(env, one("unallow", bob))[jss::error] == "internal");
|
||||
BEAST_EXPECT(status(bob) == 200);
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
writeFileContents(ec, file, good);
|
||||
}
|
||||
}
|
||||
|
||||
// A restart picks the file up again.
|
||||
{
|
||||
Env env{*this, cfg()};
|
||||
auto const r = pwaList(env);
|
||||
BEAST_EXPECT(
|
||||
names(r["allowlist"]) == Names({alice.human(), bob.human()}) &&
|
||||
r["blocklist"].size() == 0);
|
||||
}
|
||||
|
||||
// Without a lists file there is nothing to edit; and admin only.
|
||||
{
|
||||
Env env{*this, pwaConfig()};
|
||||
BEAST_EXPECT(!pwaList(env).isMember("file"));
|
||||
BEAST_EXPECT(
|
||||
pwaList(env, one("block", alice))[jss::error] ==
|
||||
"notSupported");
|
||||
}
|
||||
{
|
||||
Env env{*this, no_admin(cfg())};
|
||||
auto const jrc = makeJSONRPCClient(env.app().config());
|
||||
BEAST_EXPECT(jrc->invoke("pwa_list", Json::objectValue)[jss::result]
|
||||
.isNull());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testHookLists()
|
||||
{
|
||||
testcase("hook-controlled list providers");
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"}, bob{"bob"}, carol{"carol"}, dave{"dave"},
|
||||
p1{"p1"}, p2{"p2"};
|
||||
using V = std::optional<bool>;
|
||||
constexpr V ask = std::nullopt;
|
||||
auto check = [&](PWAPolicy policy,
|
||||
std::array<V, 4> expect,
|
||||
bool withView = true) {
|
||||
Env env{*this};
|
||||
env.app().openLedger().modify([&](OpenView& view, beast::Journal) {
|
||||
// What each provider's hook would state_set.
|
||||
auto put = [&](Account const& p, Account const& a, Blob v) {
|
||||
uint256 key;
|
||||
std::copy(a.id().begin(), a.id().end(), key.end() - 20);
|
||||
auto sle = std::make_shared<SLE>(
|
||||
keylet::hookState(p.id(), key, pwaListNamespace));
|
||||
sle->setFieldH256(sfHookStateKey, key);
|
||||
sle->setFieldVL(sfHookStateData, v);
|
||||
view.rawInsert(sle);
|
||||
};
|
||||
put(p1, alice, {0, 0});
|
||||
put(p1, bob, {0, 1});
|
||||
put(p2, alice, {1});
|
||||
put(p2, carol, {0});
|
||||
int i = 0;
|
||||
for (auto const& a : {alice, bob, carol, dave})
|
||||
BEAST_EXPECTS(
|
||||
policy.verdict(withView ? &view : nullptr, a.id()) ==
|
||||
expect[i++],
|
||||
a.name());
|
||||
return false;
|
||||
});
|
||||
};
|
||||
auto const providers = [&](PWALists cfg, PWALists file = {}) {
|
||||
PWAPolicy p;
|
||||
p.cfg = std::move(cfg);
|
||||
p.file = std::move(file);
|
||||
p.providers = {p1.id(), p2.id()};
|
||||
return p;
|
||||
};
|
||||
// First provider with an entry wins; no entry passes through.
|
||||
check(providers({}), {false, true, false, true});
|
||||
// Local lists first, cfg and file alike; listed nowhere falls to
|
||||
// the allowlist rule.
|
||||
check(
|
||||
providers({{alice.id()}, {bob.id()}}), {true, false, false, false});
|
||||
check(
|
||||
providers({{}, {bob.id()}}, {{alice.id()}, {}}),
|
||||
{true, false, false, false});
|
||||
// Without a view (the I/O thread's early check) providers are not
|
||||
// consulted: what the lists settle is settled, the rest is open.
|
||||
check(providers({}), {ask, ask, ask, ask}, false);
|
||||
check(
|
||||
providers({{alice.id()}, {bob.id()}}),
|
||||
{true, false, ask, ask},
|
||||
false);
|
||||
// With no providers the lists settle everything, view or not.
|
||||
PWAPolicy local;
|
||||
local.file = {{alice.id()}, {bob.id()}};
|
||||
check(local, {true, false, false, false}, false);
|
||||
check(local, {true, false, false, false});
|
||||
check(PWAPolicy{}, {true, true, true, true}, false);
|
||||
}
|
||||
|
||||
void
|
||||
testProxy(boost::asio::yield_context& yield)
|
||||
{
|
||||
@@ -1003,10 +1271,12 @@ public:
|
||||
run() override
|
||||
{
|
||||
testConfigValidation();
|
||||
testHookLists();
|
||||
yield_to([&](boost::asio::yield_context& yield) {
|
||||
testServe(yield);
|
||||
testRouting(yield);
|
||||
testLists(yield);
|
||||
testListRPC(yield);
|
||||
testProxy(yield);
|
||||
testCustomDomains(yield);
|
||||
testPWAInfo(yield);
|
||||
|
||||
@@ -79,6 +79,8 @@ struct ConfigSection
|
||||
#define SECTION_PORT_GRPC "port_grpc"
|
||||
#define SECTION_PWA_ALLOWLIST "pwa_allowlist"
|
||||
#define SECTION_PWA_BLOCKLIST "pwa_blocklist"
|
||||
#define SECTION_PWA_LIST_PROVIDERS "pwa_list_providers"
|
||||
#define SECTION_PWA_LIST_FILE "pwa_list_file"
|
||||
#define SECTION_PREFETCH_WORKERS "prefetch_workers"
|
||||
#define SECTION_REDUCE_RELAY "reduce_relay"
|
||||
#define SECTION_RELATIONAL_DB "relational_db"
|
||||
|
||||
@@ -1412,6 +1412,30 @@ private:
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// pwa_list [reload] [allow|block|unallow|unblock <account>]...
|
||||
Json::Value
|
||||
parsePWAList(Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest(Json::objectValue);
|
||||
for (Json::UInt i = 0; i < jvParams.size(); ++i)
|
||||
{
|
||||
auto const op = jvParams[i].asString();
|
||||
if (op == "reload" && !jvRequest.isMember(op))
|
||||
{
|
||||
jvRequest[op] = true;
|
||||
continue;
|
||||
}
|
||||
if ((op != "allow" && op != "block" && op != "unallow" &&
|
||||
op != "unblock") ||
|
||||
++i == jvParams.size())
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
if (!jvRequest.isMember(op))
|
||||
jvRequest[op] = Json::arrayValue;
|
||||
jvRequest[op].append(jvParams[i].asString());
|
||||
}
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// pwa_validate <account> <hash> [<domain>]
|
||||
Json::Value
|
||||
parsePWAValidate(Json::Value const& jvParams)
|
||||
@@ -1515,6 +1539,7 @@ public:
|
||||
{"ping", &RPCParser::parseAsIs, 0, 0},
|
||||
{"print", &RPCParser::parseAsIs, 0, 1},
|
||||
{"pwa_info", &RPCParser::parsePWAInfo, 0, 2},
|
||||
{"pwa_list", &RPCParser::parsePWAList, 0, -1},
|
||||
{"pwa_validate", &RPCParser::parsePWAValidate, 2, 3},
|
||||
// { "profile", &RPCParser::parseProfile, 1, 9
|
||||
// },
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/ssl/ssl_stream.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/utility/string_view.hpp>
|
||||
|
||||
#include <array>
|
||||
@@ -40,7 +41,10 @@
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
@@ -51,6 +55,51 @@ operator<(Port const& lhs, Port const& rhs)
|
||||
return lhs.name < rhs.name;
|
||||
}
|
||||
|
||||
class ReadView;
|
||||
|
||||
/** A [pwa_allowlist] and a [pwa_blocklist], from one source. */
|
||||
struct PWALists
|
||||
{
|
||||
std::set<AccountID> allow; // if non-empty, only these are served
|
||||
std::set<AccountID> block; // never served; wins over allowlist
|
||||
|
||||
bool
|
||||
operator==(PWALists const&) const = default;
|
||||
};
|
||||
|
||||
/** Which accounts' loaders the pwa ports serve. Node-global: every pwa
|
||||
port applies the same policy. It has three sources, and `verdict`
|
||||
consults them in this order:
|
||||
|
||||
1. the blocklists (cfg and file): refused;
|
||||
2. the allowlists (cfg and file): served;
|
||||
3. `providers`, in order: the first with a hook state entry for the
|
||||
account under pwaListNamespace decides (all-zero data refuses,
|
||||
anything else serves). Needs a ledger;
|
||||
4. listed nowhere: served, unless an allowlist is non-empty.
|
||||
|
||||
A snapshot is immutable once built; ServerHandler replaces it whole. */
|
||||
struct PWAPolicy
|
||||
{
|
||||
PWALists cfg; // from xahaud.cfg: fixed
|
||||
PWALists file; // from the lists file: pwa_list edits
|
||||
std::vector<AccountID> providers; // [pwa_list_providers], first wins
|
||||
|
||||
bool
|
||||
operator==(PWAPolicy const&) const = default;
|
||||
|
||||
/** Whether `account` may be served. With a view the answer is always
|
||||
settled. Without one the providers are not consulted, so nullopt
|
||||
means "listed nowhere; ask again with a ledger". */
|
||||
std::optional<bool>
|
||||
verdict(ReadView const* view, AccountID const& account) const;
|
||||
|
||||
/** Accounts on both an allowlist and a blocklist, cfg and file
|
||||
counted together. Non-empty is a configuration error. */
|
||||
std::set<AccountID>
|
||||
conflicts() const;
|
||||
};
|
||||
|
||||
class ServerHandler
|
||||
{
|
||||
public:
|
||||
@@ -116,6 +165,11 @@ public:
|
||||
// Configuration for the Overlay
|
||||
boost::asio::ip::tcp::endpoint overlay;
|
||||
|
||||
// The PWA policy as read at startup; pwaListsFile (empty: none) is
|
||||
// where the pwa_list RPC persists its edits to policy.file.
|
||||
PWAPolicy pwaPolicy;
|
||||
boost::filesystem::path pwaListsFile;
|
||||
|
||||
void
|
||||
makeContexts();
|
||||
};
|
||||
@@ -165,6 +219,14 @@ private:
|
||||
static constexpr std::size_t maxPWADnsInFlight = 4;
|
||||
static constexpr std::size_t maxPWADnsEntries = 10'000;
|
||||
|
||||
// The live PWA policy, replaced whole on each edit so a request sees
|
||||
// one consistent snapshot. pwaEditMutex_ serialises edits (and so the
|
||||
// lists file write); pwaPolicyMutex_ guards only the pointer.
|
||||
std::mutex pwaEditMutex_;
|
||||
mutable std::mutex pwaPolicyMutex_;
|
||||
std::shared_ptr<PWAPolicy const> pwaPolicy_ =
|
||||
std::make_shared<PWAPolicy const>();
|
||||
|
||||
void
|
||||
resolvePWADomain(
|
||||
std::string const& host,
|
||||
@@ -228,6 +290,20 @@ public:
|
||||
return pwaStats_;
|
||||
}
|
||||
|
||||
std::shared_ptr<PWAPolicy const>
|
||||
pwaPolicy() const
|
||||
{
|
||||
std::lock_guard lock(pwaPolicyMutex_);
|
||||
return pwaPolicy_;
|
||||
}
|
||||
|
||||
/** The pwa_list RPC: optionally reread the lists file, take `remove`
|
||||
off and then put `add` on the file's lists, write the file if that
|
||||
changed it, then publish the new policy. All or nothing. Returns the
|
||||
RPC result. */
|
||||
Json::Value
|
||||
pwaListsEdit(PWALists const& add, PWALists const& remove, bool reload);
|
||||
|
||||
/** Drop every cached custom domain answer; returns how many. */
|
||||
std::size_t
|
||||
pwaDnsFlush()
|
||||
@@ -323,8 +399,9 @@ private:
|
||||
void
|
||||
onPWARequest(Session& session);
|
||||
|
||||
/** Apply the allowlist and blocklist, then post the ledger read to the
|
||||
job queue as jtCLIENT_PWA. Callable from any thread. */
|
||||
/** Refuse what the PWA policy settles without a ledger, then post the
|
||||
ledger read to the job queue as jtCLIENT_PWA. Callable from any
|
||||
thread. */
|
||||
void
|
||||
queuePWALookup(
|
||||
std::shared_ptr<Session> const& session,
|
||||
|
||||
@@ -145,6 +145,7 @@ Handler const handlerArray[]{
|
||||
{"ping", byRef(&doPing), Role::USER, NO_CONDITION},
|
||||
{"print", byRef(&doPrint), Role::ADMIN, NO_CONDITION},
|
||||
{"pwa_info", byRef(&doPWAInfo), Role::ADMIN, NO_CONDITION},
|
||||
{"pwa_list", byRef(&doPWAList), Role::ADMIN, NO_CONDITION},
|
||||
{"pwa_validate", byRef(&doPWAValidate), Role::USER, NO_CONDITION},
|
||||
// { "profile", byRef (&doProfile), Role::USER,
|
||||
// NEEDS_CURRENT_LEDGER },
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
#include <xrpld/rpc/json_body.h>
|
||||
#include <xrpl/basics/FileUtilities.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
@@ -60,13 +61,19 @@
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <arpa/nameser.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <resolv.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -152,6 +159,12 @@ void
|
||||
ServerHandler::setup(Setup const& setup, beast::Journal journal)
|
||||
{
|
||||
setup_ = setup;
|
||||
{
|
||||
std::lock_guard editLock(pwaEditMutex_);
|
||||
auto policy = std::make_shared<PWAPolicy const>(setup.pwaPolicy);
|
||||
std::lock_guard lock(pwaPolicyMutex_);
|
||||
pwaPolicy_ = std::move(policy);
|
||||
}
|
||||
endpoints_ = m_server->ports(setup.ports);
|
||||
|
||||
// fix auto ports
|
||||
@@ -866,15 +879,251 @@ ServerHandler::resolvePWADomain(
|
||||
pwaDnsCache_[host] = dns;
|
||||
}
|
||||
|
||||
std::optional<bool>
|
||||
PWAPolicy::verdict(ReadView const* view, AccountID const& account) const
|
||||
{
|
||||
auto const on = [&](std::set<AccountID> PWALists::*list) {
|
||||
return (cfg.*list).count(account) > 0 ||
|
||||
(file.*list).count(account) > 0;
|
||||
};
|
||||
if (on(&PWALists::block))
|
||||
return false;
|
||||
if (on(&PWALists::allow))
|
||||
return true;
|
||||
if (!providers.empty())
|
||||
{
|
||||
if (!view)
|
||||
return std::nullopt;
|
||||
uint256 key;
|
||||
std::copy(account.begin(), account.end(), key.end() - account.size());
|
||||
for (auto const& provider : providers)
|
||||
if (auto const sle = view->read(
|
||||
keylet::hookState(provider, key, pwaListNamespace)))
|
||||
{
|
||||
auto const v = sle->getFieldVL(sfHookStateData);
|
||||
return std::any_of(
|
||||
v.begin(), v.end(), [](auto b) { return b != 0; });
|
||||
}
|
||||
}
|
||||
return cfg.allow.empty() && file.allow.empty();
|
||||
}
|
||||
|
||||
std::set<AccountID>
|
||||
PWAPolicy::conflicts() const
|
||||
{
|
||||
std::set<AccountID> out;
|
||||
for (auto const* allow : {&cfg.allow, &file.allow})
|
||||
for (auto const& a : *allow)
|
||||
if (cfg.block.count(a) > 0 || file.block.count(a) > 0)
|
||||
out.insert(a);
|
||||
return out;
|
||||
}
|
||||
|
||||
// The lists file: [pwa_allowlist] and [pwa_blocklist] sections of one
|
||||
// r-address per line, '#' comments. Returns why it is unusable, if it is;
|
||||
// no file at all is two empty lists.
|
||||
static std::optional<std::string>
|
||||
readPWAListsFile(boost::filesystem::path const& path, PWALists& out)
|
||||
{
|
||||
out = {};
|
||||
boost::system::error_code ec;
|
||||
if (!boost::filesystem::exists(path, ec))
|
||||
{
|
||||
if (ec)
|
||||
return path.string() + ": " + ec.message();
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!boost::filesystem::is_regular_file(path, ec))
|
||||
return path.string() + ": not a regular file";
|
||||
auto const data = getFileContents(ec, path);
|
||||
if (ec)
|
||||
return path.string() + ": " + ec.message();
|
||||
|
||||
std::set<AccountID>* list = nullptr;
|
||||
std::istringstream in(data);
|
||||
std::string line;
|
||||
for (int n = 1; std::getline(in, line); ++n)
|
||||
{
|
||||
boost::algorithm::trim(line);
|
||||
auto const at = path.string() + ":" + std::to_string(n) + ": ";
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
if (line == "[" SECTION_PWA_ALLOWLIST "]")
|
||||
list = &out.allow;
|
||||
else if (line == "[" SECTION_PWA_BLOCKLIST "]")
|
||||
list = &out.block;
|
||||
else if (!list || line[0] == '[')
|
||||
return at +
|
||||
"expected [" SECTION_PWA_ALLOWLIST
|
||||
"] or [" SECTION_PWA_BLOCKLIST "]";
|
||||
else if (auto const a = parseBase58<AccountID>(line); a && !a->isZero())
|
||||
list->insert(*a);
|
||||
else
|
||||
return at + "invalid r-address '" + line + "'";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static std::string
|
||||
pwaListsText(PWALists const& lists)
|
||||
{
|
||||
std::string text =
|
||||
"# PWA lists for every pwa port, applied together with any\n"
|
||||
"# [" SECTION_PWA_ALLOWLIST "] and [" SECTION_PWA_BLOCKLIST
|
||||
"] in the cfg file. One r-address per line.\n"
|
||||
"# Written by the pwa_list admin RPC, which does not keep comments.\n"
|
||||
"# After editing by hand, run: pwa_list reload\n";
|
||||
auto const section = [&](char const* name, std::set<AccountID> const& v) {
|
||||
text += std::string("\n[") + name + "]\n";
|
||||
for (auto const& a : v)
|
||||
text += toBase58(a) + "\n";
|
||||
};
|
||||
section(SECTION_PWA_ALLOWLIST, lists.allow);
|
||||
section(SECTION_PWA_BLOCKLIST, lists.block);
|
||||
return text;
|
||||
}
|
||||
|
||||
// Replace the file at `path` (through a symlink: its target) with `text`:
|
||||
// write a sibling temporary with the old file's mode, fsync it and rename it
|
||||
// over. Returns why that failed, if it did; the old file is then untouched.
|
||||
static std::optional<std::string>
|
||||
replaceFile(boost::filesystem::path path, std::string const& text)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
if (boost::filesystem::is_symlink(path, ec))
|
||||
path = boost::filesystem::canonical(path, ec);
|
||||
if (ec)
|
||||
return path.string() + ": " + ec.message();
|
||||
auto const tmp = path.string() + ".tmp";
|
||||
int const fd =
|
||||
::open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
|
||||
if (fd < 0)
|
||||
return tmp + ": " + std::strerror(errno);
|
||||
|
||||
std::optional<std::string> err;
|
||||
auto const check = [&](bool ok, char const* what) {
|
||||
if (!ok && !err)
|
||||
err = tmp + ": " + what + ": " + std::strerror(errno);
|
||||
};
|
||||
struct stat old;
|
||||
if (::stat(path.c_str(), &old) == 0)
|
||||
check(::fchmod(fd, old.st_mode & 07777) == 0, "fchmod");
|
||||
for (std::size_t done = 0; !err && done < text.size();)
|
||||
{
|
||||
auto const n = ::write(fd, text.data() + done, text.size() - done);
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
check(n > 0, "write");
|
||||
if (n > 0)
|
||||
done += static_cast<std::size_t>(n);
|
||||
}
|
||||
if (!err)
|
||||
check(::fsync(fd) == 0, "fsync");
|
||||
check(::close(fd) == 0, "close");
|
||||
if (!err)
|
||||
check(::rename(tmp.c_str(), path.c_str()) == 0, "rename");
|
||||
if (err)
|
||||
::unlink(tmp.c_str());
|
||||
return err;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
ServerHandler::pwaListsEdit(
|
||||
PWALists const& add,
|
||||
PWALists const& remove,
|
||||
bool reload)
|
||||
{
|
||||
using List = std::set<AccountID> PWALists::*;
|
||||
static constexpr List lists[] = {&PWALists::allow, &PWALists::block};
|
||||
auto const name = [](List l) {
|
||||
return std::string(l == &PWALists::allow ? "allow" : "block");
|
||||
};
|
||||
auto const& path = setup_.pwaListsFile;
|
||||
bool const edit = !(add == PWALists{} && remove == PWALists{});
|
||||
if ((edit || reload) && path.empty())
|
||||
return RPC::make_error(
|
||||
rpcNOT_SUPPORTED,
|
||||
"No lists file to edit: set [" SECTION_PWA_LIST_FILE "]");
|
||||
|
||||
std::lock_guard editLock(pwaEditMutex_);
|
||||
auto const current = pwaPolicy();
|
||||
auto const& cfg = current->cfg;
|
||||
for (auto const l : lists)
|
||||
for (auto const& a : remove.*l)
|
||||
{
|
||||
if ((add.*l).count(a) > 0)
|
||||
return RPC::make_param_error(
|
||||
toBase58(a) + " is in both " + name(l) + " and un" +
|
||||
name(l));
|
||||
if ((cfg.*l).count(a) > 0)
|
||||
return RPC::make_param_error(
|
||||
toBase58(a) + " is on [pwa_" + name(l) +
|
||||
"list] in the cfg file, which this cannot change");
|
||||
}
|
||||
|
||||
// Every edit starts from the file as it is now, so hand edits made since
|
||||
// the last write are kept rather than overwritten.
|
||||
PWALists onDisk = current->file;
|
||||
if (edit || reload)
|
||||
if (auto const why = readPWAListsFile(path, onDisk))
|
||||
return RPC::make_error(rpcINTERNAL, *why);
|
||||
PWAPolicy next = *current;
|
||||
next.file = onDisk;
|
||||
for (auto const l : lists)
|
||||
{
|
||||
for (auto const& a : remove.*l)
|
||||
(next.file.*l).erase(a);
|
||||
for (auto const& a : add.*l)
|
||||
if ((cfg.*l).count(a) == 0)
|
||||
(next.file.*l).insert(a);
|
||||
}
|
||||
if (auto const both = next.conflicts(); !both.empty())
|
||||
return RPC::make_param_error(
|
||||
toBase58(*both.begin()) + " would be on both lists");
|
||||
|
||||
// Persist first: if the write fails nothing has changed.
|
||||
if (next.file != onDisk)
|
||||
if (auto const why = replaceFile(path, pwaListsText(next.file)))
|
||||
return RPC::make_error(
|
||||
rpcINTERNAL, "Cannot write the lists file: " + *why);
|
||||
bool const changed = next != *current;
|
||||
auto published = current;
|
||||
if (changed)
|
||||
{
|
||||
published = std::make_shared<PWAPolicy const>(std::move(next));
|
||||
std::lock_guard lock(pwaPolicyMutex_);
|
||||
pwaPolicy_ = published;
|
||||
}
|
||||
|
||||
auto const& shown = *published;
|
||||
Json::Value ret(Json::objectValue);
|
||||
auto const put = [&](char const* key, auto const& v) {
|
||||
Json::Value& out = ret[key] = Json::arrayValue;
|
||||
for (auto const& a : v)
|
||||
out.append(toBase58(a));
|
||||
};
|
||||
put("allowlist", shown.file.allow);
|
||||
put("blocklist", shown.file.block);
|
||||
if (!shown.cfg.allow.empty())
|
||||
put("cfg_allowlist", shown.cfg.allow);
|
||||
if (!shown.cfg.block.empty())
|
||||
put("cfg_blocklist", shown.cfg.block);
|
||||
if (!shown.providers.empty())
|
||||
put("providers", shown.providers);
|
||||
if (!path.empty())
|
||||
ret["file"] = path.string();
|
||||
ret["changed"] = changed;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::queuePWALookup(
|
||||
std::shared_ptr<Session> const& session,
|
||||
PWALookup const& lookup)
|
||||
{
|
||||
auto const& port = session->port();
|
||||
if (port.pwa_blocklist.count(lookup.account) > 0 ||
|
||||
(!port.pwa_allowlist.empty() &&
|
||||
port.pwa_allowlist.count(lookup.account) == 0))
|
||||
// Refuse early what the policy settles without a ledger; providers are
|
||||
// consulted in the job, against the validated ledger.
|
||||
if (auto const v = pwaPolicy()->verdict(nullptr, lookup.account); v && !*v)
|
||||
{
|
||||
++pwaStats_.outcomes[std::size_t(Outcome::blocked)];
|
||||
return pwaError(*session, 403, lookup.head);
|
||||
@@ -930,6 +1179,11 @@ ServerHandler::processPWARequest(
|
||||
if (!ledger)
|
||||
{
|
||||
}
|
||||
else if (!*pwaPolicy()->verdict(ledger.get(), account))
|
||||
{
|
||||
outcome = Outcome::blocked;
|
||||
status = 403;
|
||||
}
|
||||
else if (!lookup.redirectTail.empty())
|
||||
{
|
||||
if (index)
|
||||
@@ -2112,32 +2366,8 @@ parse_Ports(Config const& config, std::ostream& log)
|
||||
}
|
||||
|
||||
// PWA ports host third-party content and must only ever be reached
|
||||
// through a reverse proxy. [pwa_allowlist] and [pwa_blocklist], one
|
||||
// r-address per line, apply to all of them.
|
||||
auto accounts = [&](char const* name) {
|
||||
std::set<AccountID> out;
|
||||
for (auto const& line : config.section(name).values())
|
||||
{
|
||||
auto const account = parseBase58<AccountID>(line);
|
||||
if (!account || account->isZero())
|
||||
{
|
||||
log << "Invalid r-address '" << line << "' in [" << name << "]";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
out.insert(*account);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
auto const allowlist = accounts(SECTION_PWA_ALLOWLIST);
|
||||
auto const blocklist = accounts(SECTION_PWA_BLOCKLIST);
|
||||
for (auto const& a : allowlist)
|
||||
if (blocklist.count(a) > 0)
|
||||
{
|
||||
log << "Account " << toBase58(a)
|
||||
<< " is in both [" SECTION_PWA_ALLOWLIST
|
||||
"] and [" SECTION_PWA_BLOCKLIST "]";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
// through a reverse proxy. Which accounts they serve is node-global,
|
||||
// not per port: see setup_PWAPolicy.
|
||||
for (auto& p : result)
|
||||
{
|
||||
auto fail = [&](char const* why) {
|
||||
@@ -2175,8 +2405,6 @@ parse_Ports(Config const& config, std::ostream& log)
|
||||
!p.user.empty() || !p.password.empty() || !p.ssl_key.empty() ||
|
||||
!p.ssl_cert.empty() || !p.ssl_chain.empty())
|
||||
fail("pwa port cannot have admin, credentials or TLS");
|
||||
p.pwa_allowlist = allowlist;
|
||||
p.pwa_blocklist = blocklist;
|
||||
}
|
||||
|
||||
if (config.standalone())
|
||||
@@ -2255,11 +2483,74 @@ setup_Overlay(ServerHandler::Setup& setup)
|
||||
setup.overlay = {iter->ip, iter->port};
|
||||
}
|
||||
|
||||
// The three sources of the PWA policy, one r-address per line each:
|
||||
// [pwa_allowlist] and [pwa_blocklist] in the cfg file (fixed) and in the
|
||||
// lists file (edited by the pwa_list RPC), and [pwa_list_providers] in the
|
||||
// cfg file. The lists file is opt-in: the one named by [pwa_list_file],
|
||||
// relative to the cfg file's directory. It need not exist yet (the RPC
|
||||
// creates it), but its directory must.
|
||||
static void
|
||||
setup_PWAPolicy(
|
||||
Config const& config,
|
||||
ServerHandler::Setup& setup,
|
||||
std::ostream& log)
|
||||
{
|
||||
auto const fail = [&](std::string const& why) {
|
||||
log << why;
|
||||
Throw<std::exception>();
|
||||
};
|
||||
auto const read = [&](char const* name, auto&& put) {
|
||||
for (auto const& line : config.section(name).values())
|
||||
{
|
||||
auto const a = parseBase58<AccountID>(line);
|
||||
if (!a || a->isZero())
|
||||
fail("Invalid r-address '" + line + "' in [" + name + "]");
|
||||
put(*a);
|
||||
}
|
||||
};
|
||||
auto& policy = setup.pwaPolicy;
|
||||
read(SECTION_PWA_ALLOWLIST, [&](auto const& a) {
|
||||
policy.cfg.allow.insert(a);
|
||||
});
|
||||
read(SECTION_PWA_BLOCKLIST, [&](auto const& a) {
|
||||
policy.cfg.block.insert(a);
|
||||
});
|
||||
read(SECTION_PWA_LIST_PROVIDERS, [&](auto const& a) {
|
||||
policy.providers.push_back(a);
|
||||
});
|
||||
|
||||
auto& path = setup.pwaListsFile;
|
||||
if (config.exists(SECTION_PWA_LIST_FILE))
|
||||
{
|
||||
path = config.section(SECTION_PWA_LIST_FILE).legacy();
|
||||
if (path.empty())
|
||||
fail("Invalid path in [" SECTION_PWA_LIST_FILE "]");
|
||||
if (path.is_relative() && !config.CONFIG_DIR.empty())
|
||||
path = config.CONFIG_DIR / path;
|
||||
// Catch a mistyped path now rather than at the first edit.
|
||||
boost::system::error_code ec;
|
||||
auto const dir = boost::filesystem::absolute(path).parent_path();
|
||||
if (!boost::filesystem::is_directory(dir, ec))
|
||||
fail(
|
||||
"[" SECTION_PWA_LIST_FILE "]: no such directory: " +
|
||||
dir.string());
|
||||
if (auto const why = readPWAListsFile(path, policy.file))
|
||||
fail(*why);
|
||||
}
|
||||
|
||||
if (auto const both = policy.conflicts(); !both.empty())
|
||||
fail(
|
||||
"Account " + toBase58(*both.begin()) +
|
||||
" is in both [" SECTION_PWA_ALLOWLIST
|
||||
"] and [" SECTION_PWA_BLOCKLIST "]");
|
||||
}
|
||||
|
||||
ServerHandler::Setup
|
||||
setup_ServerHandler(Config const& config, std::ostream&& log)
|
||||
{
|
||||
ServerHandler::Setup setup;
|
||||
setup.ports = parse_Ports(config, log);
|
||||
setup_PWAPolicy(config, setup, log);
|
||||
|
||||
if (config.UDP_HIGHWAY)
|
||||
{
|
||||
|
||||
@@ -117,6 +117,8 @@ doPing(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPWAInfo(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPWAList(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPWAValidate(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPrint(RPC::JsonContext&);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** pwa_list: the PWA allowlist and blocklist. Admin only.
|
||||
|
||||
"allow" and "block" put r-addresses on that list, "unallow" and
|
||||
"unblock" take them off it; each is an r-address or an array of them.
|
||||
Removals apply first, so {"unblock": a, "allow": a} moves a in one step,
|
||||
and the request applies whole or not at all. Changes are written to the
|
||||
lists file ([pwa_list_file]) before they take effect, starting from the
|
||||
file as it is now so hand edits are kept; {"reload": true} just rereads
|
||||
it. Entries from the cfg file are fixed.
|
||||
|
||||
Returns the lists file's "allowlist" and "blocklist", the cfg file's
|
||||
"cfg_allowlist" and "cfg_blocklist" when non-empty, the "file" path and
|
||||
whether anything "changed". Every pwa port applies the union.
|
||||
*/
|
||||
Json::Value
|
||||
doPWAList(RPC::JsonContext& context)
|
||||
{
|
||||
auto const& params = context.params;
|
||||
PWALists add, remove;
|
||||
std::pair<char const*, std::set<AccountID>*> const fields[] = {
|
||||
{"allow", &add.allow},
|
||||
{"block", &add.block},
|
||||
{"unallow", &remove.allow},
|
||||
{"unblock", &remove.block}};
|
||||
for (auto const& [key, out] : fields)
|
||||
{
|
||||
if (!params.isMember(key))
|
||||
continue;
|
||||
Json::Value accounts = params[key];
|
||||
if (accounts.isString())
|
||||
{
|
||||
accounts = Json::arrayValue;
|
||||
accounts.append(params[key]);
|
||||
}
|
||||
if (!accounts.isArray())
|
||||
return RPC::expected_field_error(key, "r-address or array");
|
||||
for (auto const& v : accounts)
|
||||
{
|
||||
auto const a = v.isString() ? parseBase58<AccountID>(v.asString())
|
||||
: std::nullopt;
|
||||
if (!a || a->isZero())
|
||||
return RPC::invalid_field_error(key);
|
||||
out->insert(*a);
|
||||
}
|
||||
}
|
||||
|
||||
bool reload = false;
|
||||
if (params.isMember("reload"))
|
||||
{
|
||||
if (!params["reload"].isBool())
|
||||
return RPC::expected_field_error("reload", "bool");
|
||||
reload = params["reload"].asBool();
|
||||
}
|
||||
return context.app.getServerHandler().pwaListsEdit(add, remove, reload);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
Reference in new issue
Block a user