Add support for admin REST server (#62)

This commit is contained in:
Lee *!* Clagett 2023-02-10 07:18:15 -05:00 committed by Lee *!* Clagett
parent c958ac7963
commit 7b1e1e58f8
13 changed files with 659 additions and 118 deletions

View file

@ -108,7 +108,7 @@ load_cache(${MONERO_BUILD_DIR} READ_WITH_PREFIX monero_
HIDAPI_LIBRARY
LMDB_INCLUDE
monero_SOURCE_DIR
OPENSSL_INCLUDE_PATH
OPENSSL_INCLUDE_DIR
OPENSSL_CRYPTO_LIBRARY
OPENSSL_SSL_LIBRARY
SODIUM_LIBRARY
@ -217,7 +217,7 @@ set_property(TARGET monero::libraries PROPERTY
INTERFACE_INCLUDE_DIRECTORIES
${Boost_INCLUDE_DIR}
${monero_HIDAPI_INCLUDE_DIRS}
${monero_OPENSSL_INCLUDE_PATH}
${monero_OPENSSL_INCLUDE_DIR}
"${MONERO_BUILD_DIR}/generated_include"
"${MONERO_SOURCE_DIR}/contrib/epee/include"
"${MONERO_SOURCE_DIR}/external/easylogging++"

View file

@ -1,28 +1,91 @@
# Using monero-lws-admin
The `monero-lws-admin` executable is used to administer the database used by
`monero-lws-daemon`. Any number of `monero-lws-admin` instances can run
# monero-lws Administration
The `monero-lws-admin` executable or `--admin-rest-server` option in the
`monero-lws-daemon` executable can be used to administer the database
used by `monero-lws-daemon`. Any number of `monero-lws-admin` instances can run
concurrently with a single `monero-lws-daemon` instance on the same database.
Administration is necessary to authorize new accounts and rescan requests
submitted from the REST API. The admin executable can also be used to list
the contents of the LMDB file for debugging purposes.
# Basics
# monero-lws-admin
The `monero-lws-admin` utility is structured around command-line arguments with
JSON responses printed to `stdout`. Each administration command takes arguments
by position - the design makes it potentially compatible with a JSON or MsgPack
array (as used in JSON-RPC, etc). Every available administration command and
required+optional arguments are listed when the `--help` flag is given to the
executable.
by position. Every available administration command and required+optional
arguments are listed when the `--help` flag is given to the executable.
The [`jq`](https://stedolan.github.io/jq/) utility is recommended if using
`monero-lws-admin` in a shell environment. The `jq` program can be used for
indenting the output to make it more readable, and can be used to
search+filter the JSON output from the command.
# Admin REST API
The `monero-lws-daemon` can be started with 1+ `--admin-rest-server` parameters
that specify a listening location for admin REST clients. By default, there is
no admin REST server and no available admin accounts.
An admin REST server can be merged with a regular REST server if path prefixes
are specified, such as
`--rest-server https://0.0.0.0:8443/basic --admin-rest-server https://0.0.0.0:8443/admin`.
This will start a server listening on one port, 8443, and requires clients to
specify `/basic/command` or `/admin/admin_command` when making a
request.
An admin account account can be created via `monero-lws-admin create_admin`
_only_ (this command is not available via REST for security purposes). The
`key` value returned in the `create_admin` JSON object becomes the `auth`
parameter in the admin REST API. A new admin account is put into the
`hidden` state - the account is _not_ scanned for transactions and is _not_
available to the normal REST API, but is available to the admin REST API.
Running `monero-lws-admin list_admin` will display all current admin
accounts, and their current state ("active", "inactive", or "hidden"). If
an admin account needs to be revoked, use the `modify_account` command
to put the account into the "inactive" state. Deleting accounts is not
currently supported.
Every admin REST request must be a `POST` that contains a JSON object with
an `auth` field and an optional `params` field:
```json
{
"auth":"...",
"params":{...}
}
```
where the `params` object is specified below.
## Commands
A subset of admin commands are available via admin REST API - the remainder
are initially omitted for security purposes. The commands available via REST
are:
* **accept_requests**: `{"type": "import"|"create", "addresses":[...]}`
* **add_account**: `{"address": ..., "key": ...}`
* **list_accounts**: `{}`
* **list_requests**: `{}`
* **modify_account_status**: `{"status": "active"|"hidden"|"inactive", "addresses":[...]}`
* **reject_requests**: `{"type": "import"|"create", "addresses":[...]}`
* **rescan**: `{"height":..., "addresses":[...]}`
where the listed object must be the `params` field above.
# Examples
## Admin REST API
```json
{
"auth":"6d732245002a9499b3842c0a7f9fc6b2d657c77bd612dbefa4f7f9357d08530a",
"params":{
"status": "inactive",
"addresses": ["9sAejnQ9EBR1111111111111111111111111111111111AdYmVTw2Tv6L9KYkHjJ2wd737ov8ZL5QU7CJ4zV6basGP9fyno"]
}
}
```
will put the listed address into the "inactive" state.
## monero-lws-admin
**List every active Monero address on a newline:**
```bash
monero-lws-admin list_accounts | jq -r '.active | .[] | .address'

View file

@ -68,6 +68,7 @@ target_link_libraries(monero-lws-admin
monero::libraries
monero-lws-common
monero-lws-db
monero-lws-rpc
monero-lws-wire-json
${Boost_PROGRAM_OPTIONS_LIBRARY}
Threads::Threads

View file

@ -30,6 +30,7 @@
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/variables_map.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <cassert>
#include <cstring>
#include <iostream>
@ -47,63 +48,45 @@
#include "db/string.h"
#include "options.h"
#include "misc_log_ex.h" // monero/contrib/epee/include
#include "rpc/admin.h"
#include "span.h" // monero/contrib/epee/include
#include "string_tools.h" // monero/contrib/epee/include
#include "wire/crypto.h"
#include "wire/filters.h"
#include "wire/json/write.h"
namespace
{
// Do not output "full" debug data provided by `db::data.h` header; truncate output
// wrapper for custom output for admin accounts
template<typename T>
struct truncated
struct admin_display
{
T value;
};
void write_bytes(wire::json_writer& dest, const truncated<lws::db::account>& self)
void write_bytes(wire::json_writer& dest, const admin_display<lws::db::account>& source)
{
wire::object(dest,
wire::field("address", lws::db::address_string(self.value.address)),
wire::field("scan_height", self.value.scan_height),
wire::field("access_time", self.value.access)
);
};
void write_bytes(wire::json_writer& dest, const truncated<lws::db::request_info>& self)
{
wire::object(dest,
wire::field("address", lws::db::address_string(self.value.address)),
wire::field("start_height", self.value.start_height)
wire::field("address", lws::db::address_string(source.value.address)),
wire::field("key", std::cref(source.value.key))
);
}
template<typename V>
void write_bytes(wire::json_writer& dest, const truncated<boost::iterator_range<lmdb::value_iterator<V>>> self)
void write_bytes(wire::json_writer& dest, admin_display<boost::iterator_range<lmdb::value_iterator<lws::db::account>>> source)
{
const auto truncate = [] (V src) { return truncated<V>{std::move(src)}; };
wire::array(dest, std::move(self.value), truncate);
const auto filter = [](const lws::db::account& src)
{ return bool(src.flags & lws::db::account_flags::admin_account); };
const auto transform = [] (lws::db::account src)
{ return admin_display<lws::db::account>{std::move(src)}; };
wire::array(dest, (source.value | boost::adaptors::filtered(filter)), transform);
}
template<typename K, typename V>
void stream_json_object(std::ostream& dest, boost::iterator_range<lmdb::key_iterator<K, V>> self)
template<typename F, typename... T>
void run_command(F f, std::ostream& dest, T&&... args)
{
using value_range = boost::iterator_range<lmdb::value_iterator<V>>;
const auto truncate = [] (value_range src) -> truncated<value_range>
{
return {std::move(src)};
};
wire::json_stream_writer json{dest};
wire::dynamic_object(json, std::move(self), wire::enum_as_string, truncate);
json.finish();
}
void write_json_addresses(std::ostream& dest, epee::span<const lws::db::account_address> self)
{
// writes an array of monero base58 address strings
wire::json_stream_writer stream{dest};
wire::object(stream, wire::field("updated", wire::as_array(self, lws::db::address_string)));
MONERO_UNWRAP(f(stream, std::forward<T>(args)...));
stream.finish();
}
@ -151,6 +134,7 @@ namespace
arguments.remove_prefix(1);
std::vector<lws::db::account_address> addresses{};
addresses.reserve(arguments.size());
for (std::string const& address : arguments)
addresses.push_back(lws::db::address_string(address).value());
return addresses;
@ -161,15 +145,11 @@ namespace
if (prog.arguments.size() < 2)
throw std::runtime_error{"accept_requests requires 2 or more arguments"};
const lws::db::request req =
MONERO_UNWRAP(lws::db::request_from_string(prog.arguments[0]));
std::vector<lws::db::account_address> addresses =
get_addresses(epee::to_span(prog.arguments));
const std::vector<lws::db::account_address> updated =
prog.disk.accept_requests(req, epee::to_span(addresses)).value();
write_json_addresses(out, epee::to_span(updated));
lws::rpc::address_requests req{
get_addresses(epee::to_span(prog.arguments)),
MONERO_UNWRAP(lws::db::request_from_string(prog.arguments[0]))
};
run_command(lws::rpc::accept_requests, out, std::move(prog.disk), std::move(req));
}
void add_account(program prog, std::ostream& out)
@ -177,13 +157,31 @@ namespace
if (prog.arguments.size() != 2)
throw std::runtime_error{"add_account needs exactly two arguments"};
const lws::db::account_address address[1] = {
lws::db::address_string(prog.arguments[0]).value()
lws::rpc::add_account_req req{
lws::db::address_string(prog.arguments[0]).value(),
get_key(prog.arguments[1])
};
const crypto::secret_key key{get_key(prog.arguments[1])};
run_command(lws::rpc::add_account, out, std::move(prog.disk), std::move(req));
}
MONERO_UNWRAP(prog.disk.add_account(address[0], key));
write_json_addresses(out, address);
void create_admin(program prog, std::ostream& out)
{
if (!prog.arguments.empty())
throw std::runtime_error{"create_admin takes zero arguments"};
admin_display<lws::db::account> account{};
{
crypto::secret_key auth{};
crypto::generate_keys(account.value.address.view_public, auth);
MONERO_UNWRAP(prog.disk.add_account(account.value.address, auth, lws::db::account_flags::admin_account));
static_assert(sizeof(auth) == sizeof(account.value.key), "bad memcpy");
std::memcpy(std::addressof(account.value.key), std::addressof(auth), sizeof(auth));
}
wire::json_stream_writer json{out};
write_bytes(json, account);
json.finish();
}
void debug_database(program prog, std::ostream& out)
@ -199,20 +197,31 @@ namespace
{
if (!prog.arguments.empty())
throw std::runtime_error{"list_accounts takes zero arguments"};
run_command(lws::rpc::list_accounts, out, std::move(prog.disk));
}
auto reader = prog.disk.start_read().value();
auto stream = reader.get_accounts().value();
stream_json_object(out, stream.make_range());
void list_admin(program prog, std::ostream& out)
{
if (!prog.arguments.empty())
throw std::runtime_error{"list_admin takes zero arguments"};
using value_range = boost::iterator_range<lmdb::value_iterator<lws::db::account>>;
const auto transform = [] (value_range user)
{ return admin_display<value_range>{std::move(user)}; };
auto reader = MONERO_UNWRAP(prog.disk.start_read());
wire::json_stream_writer json{out};
wire::dynamic_object(
json, reader.get_accounts().value().make_range(), wire::enum_as_string, transform
);
json.finish();
}
void list_requests(program prog, std::ostream& out)
{
if (!prog.arguments.empty())
throw std::runtime_error{"list_requests takes zero arguments"};
auto reader = prog.disk.start_read().value();
auto stream = reader.get_requests().value();
stream_json_object(out, stream.make_range());
run_command(lws::rpc::list_requests, out, std::move(prog.disk));
}
void modify_account(program prog, std::ostream& out)
@ -220,15 +229,11 @@ namespace
if (prog.arguments.size() < 2)
throw std::runtime_error{"modify_account_status requires 2 or more arguments"};
const lws::db::account_status status =
lws::db::account_status_from_string(prog.arguments[0]).value();
std::vector<lws::db::account_address> addresses =
get_addresses(epee::to_span(prog.arguments));
const std::vector<lws::db::account_address> updated =
prog.disk.change_status(status, epee::to_span(addresses)).value();
write_json_addresses(out, epee::to_span(updated));
lws::rpc::modify_account_req req{
get_addresses(epee::to_span(prog.arguments)),
lws::db::account_status_from_string(prog.arguments[0]).value()
};
run_command(lws::rpc::modify_account, out, std::move(prog.disk), std::move(req));
}
void reject_requests(program prog, std::ostream& out)
@ -236,12 +241,11 @@ namespace
if (prog.arguments.size() < 2)
MONERO_THROW(common_error::kInvalidArgument, "reject_requests requires 2 or more arguments");
const lws::db::request req =
lws::db::request_from_string(prog.arguments[0]).value();
std::vector<lws::db::account_address> addresses =
get_addresses(epee::to_span(prog.arguments));
MONERO_UNWRAP(prog.disk.reject_requests(req, epee::to_span(addresses)));
lws::rpc::address_requests req{
get_addresses(epee::to_span(prog.arguments)),
lws::db::request_from_string(prog.arguments[0]).value()
};
run_command(lws::rpc::reject_requests, out, std::move(prog.disk), std::move(req));
}
void rescan(program prog, std::ostream& out)
@ -249,14 +253,11 @@ namespace
if (prog.arguments.size() < 2)
throw std::runtime_error{"rescan requires 2 or more arguments"};
const auto height = lws::db::block_id(std::stoull(prog.arguments[0]));
const std::vector<lws::db::account_address> addresses =
get_addresses(epee::to_span(prog.arguments));
const std::vector<lws::db::account_address> updated =
prog.disk.rescan(height, epee::to_span(addresses)).value();
write_json_addresses(out, epee::to_span(updated));
lws::rpc::rescan_req req{
get_addresses(epee::to_span(prog.arguments)),
lws::db::block_id(std::stoull(prog.arguments[0]))
};
run_command(lws::rpc::rescan, out, std::move(prog.disk), std::move(req));
}
void rollback(program prog, std::ostream& out)
@ -277,14 +278,16 @@ namespace
char const* const name;
void (*const handler)(program, std::ostream&);
char const* const parameters;
};
};
static constexpr const command commands[] =
{
{"accept_requests", &accept_requests, "<\"create\"|\"import\"> <base58 address> [base 58 address]..."},
{"add_account", &add_account, "<base58 address> <view key hex>"},
{"create_admin", &create_admin, ""},
{"debug_database", &debug_database, ""},
{"list_accounts", &list_accounts, ""},
{"list_admin", &list_admin, ""},
{"list_requests", &list_requests, ""},
{"modify_account_status", &modify_account, "<\"active\"|\"inactive\"|\"hidden\"> <base58 address> [base 58 address]..."},
{"reject_requests", &reject_requests, "<\"create\"|\"import\"> <base58 address> [base 58 address]..."},

View file

@ -81,7 +81,7 @@ namespace db
enum account_flags : std::uint8_t
{
default_account = 0,
admin_account = 1, //!< Not currently used, for future extensions
admin_account = 1, //!< Indicates `key` can be used for admin requests
account_generated_locally = 2 //!< Flag sent by client on initial login request
};

View file

@ -1211,9 +1211,10 @@ namespace db
return {lws::error::bad_view_key};
}
const account_by_address by_address{
user.address, {user.id, account_status::active}
};
const account_status status =
user.flags == account_flags::admin_account ?
account_status::hidden : account_status::active;
const account_by_address by_address{user.address, {user.id, status}};
MDB_val key = lmdb::to_val(by_address_version);
MDB_val value = lmdb::to_val(by_address);
@ -1239,10 +1240,10 @@ namespace db
}
} // anonymous
expect<void> storage::add_account(account_address const& address, crypto::secret_key const& key) noexcept
expect<void> storage::add_account(account_address const& address, crypto::secret_key const& key, const account_flags flags) noexcept
{
MONERO_PRECOND(db != nullptr);
return db->try_write([this, &address, &key] (MDB_txn& txn) -> expect<void>
return db->try_write([this, &address, &key, flags] (MDB_txn& txn) -> expect<void>
{
const expect<db::account_time> current_time = get_account_time();
if (!current_time)
@ -1283,7 +1284,7 @@ namespace db
user.scan_height = *height;
user.access = *current_time;
user.creation = *current_time;
// ... leave flags set to zero ...
user.flags = flags;
return do_add_account(
*accounts_cur, *accounts_ba_cur, *accounts_bh_cur, user

View file

@ -194,7 +194,7 @@ namespace db
//! Add an account, for immediate inclusion in the active list.
expect<void> add_account(account_address const& address, crypto::secret_key const& key) noexcept;
expect<void> add_account(account_address const& address, crypto::secret_key const& key, account_flags flags = account_flags::default_account) noexcept;
//! Reset `addresses` to `height` for scanning.
expect<std::vector<account_address>>

View file

@ -43,6 +43,7 @@
#include "lmdb/util.h" // monero/src
#include "net/http_base.h" // monero/contrib/epee/include
#include "net/net_parse_helpers.h" // monero/contrib/epee/include
#include "rpc/admin.h"
#include "rpc/client.h"
#include "rpc/daemon_messages.h" // monero/src
#include "rpc/light_wallet.h"
@ -51,6 +52,7 @@
#include "util/gamma_picker.h"
#include "util/random_outputs.h"
#include "util/source_location.h"
#include "wire/crypto.h"
#include "wire/json.h"
namespace lws
@ -669,6 +671,56 @@ namespace lws
return wire::json::to_bytes<response>(*resp);
}
template<typename T>
struct admin
{
T params;
crypto::secret_key auth;
};
template<typename T>
void read_bytes(wire::json_reader& source, admin<T>& self)
{
wire::object(
source, wire::field("auth", std::ref(unwrap(unwrap(self.auth)))), WIRE_FIELD(params)
);
}
void read_bytes(wire::json_reader& source, admin<expect<void>>& self)
{
// params optional
wire::object(source, wire::field("auth", std::ref(unwrap(unwrap(self.auth)))));
}
template<typename E>
expect<epee::byte_slice> call_admin(std::string&& root, db::storage disk, const rpc::client&)
{
using request = typename E::request;
const expect<admin<request>> req = wire::json::from_bytes<admin<request>>(std::move(root));
if (!req)
return req.error();
{
db::account_address address{};
if (!crypto::secret_key_to_public_key(req->auth, address.view_public))
return {error::crypto_failure};
auto reader = disk.start_read();
if (!reader)
return reader.error();
const auto account = reader->get_account(address);
if (!account)
return account.error();
if (account->first == db::account_status::inactive)
return {error::account_not_found};
if (!(account->second.flags & db::account_flags::admin_account))
return {error::account_not_found};
}
wire::json_slice_writer dest{};
MONERO_CHECK(E{}(dest, std::move(disk), req->params));
return dest.take_bytes();
}
struct endpoint
{
char const* const name;
@ -688,6 +740,17 @@ namespace lws
{"/submit_raw_tx", call<submit_raw_tx>, 50 * 1024}
};
constexpr const endpoint admin_endpoints[] =
{
{"/accept_requests", call_admin<rpc::accept_requests_>, 50 * 1024},
{"/add_account", call_admin<rpc::add_account_>, 50 * 1024},
{"/list_accounts", call_admin<rpc::list_accounts_>, 100},
{"/list_requests", call_admin<rpc::list_requests_>, 100},
{"/modify_account_status", call_admin<rpc::modify_account_>, 50 * 1024},
{"/reject_requests", call_admin<rpc::reject_requests_>, 50 * 1024},
{"/rescan", call_admin<rpc::rescan_>, 50 * 1024}
};
struct by_name_
{
bool operator()(endpoint const& left, endpoint const& right) const noexcept
@ -716,23 +779,51 @@ namespace lws
{
db::storage disk;
rpc::client client;
boost::optional<std::string> prefix;
boost::optional<std::string> admin_prefix;
explicit internal(boost::asio::io_service& io_service, lws::db::storage disk, rpc::client client)
: lws::http_server_impl_base<rest_server::internal, context>(io_service)
, disk(std::move(disk))
, client(std::move(client))
, prefix()
, admin_prefix()
{
assert(std::is_sorted(std::begin(endpoints), std::end(endpoints), by_name));
}
const endpoint* get_endpoint(boost::string_ref uri) const
{
using span = epee::span<const endpoint>;
span handlers = nullptr;
if (admin_prefix && uri.starts_with(*admin_prefix))
{
uri.remove_prefix(admin_prefix->size());
handlers = span{admin_endpoints};
}
else if (prefix && uri.starts_with(*prefix))
{
uri.remove_prefix(prefix->size());
handlers = span{endpoints};
}
else
return nullptr;
const auto handler = std::lower_bound(
std::begin(handlers), std::end(handlers), uri, by_name
);
if (handler == std::end(handlers) || handler->name != uri)
return nullptr;
return handler;
}
virtual bool
handle_http_request(const http::http_request_info& query, http::http_response_info& response, context& ctx)
override final
{
const auto handler = std::lower_bound(
std::begin(endpoints), std::end(endpoints), query.m_URI, by_name
);
if (handler == std::end(endpoints) || handler->name != query.m_URI)
endpoint const* const handler = get_endpoint(query.m_URI);
if (!handler)
{
response.m_response_code = 404;
response.m_response_comment = "Not Found";
@ -799,26 +890,28 @@ namespace lws
}
};
rest_server::rest_server(epee::span<const std::string> addresses, db::storage disk, rpc::client client, configuration config)
rest_server::rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, configuration config)
: io_service_(), ports_()
{
ports_.emplace_back(io_service_, std::move(disk), std::move(client));
if (addresses.empty())
MONERO_THROW(common_error::kInvalidArgument, "REST server requires 1 or more addresses");
const auto init_port = [] (internal& port, const std::string& address, configuration config) -> bool
std::sort(admin.begin(), admin.end());
const auto init_port = [&admin] (internal& port, const std::string& address, configuration config, const bool is_admin) -> bool
{
epee::net_utils::http::url_content url{};
if (!epee::net_utils::parse_url(address, url))
MONERO_THROW(lws::error::configuration, "REST Server URL/address is invalid");
MONERO_THROW(lws::error::configuration, "REST server URL/address is invalid");
const bool https = url.schema == "https";
if (!https && url.schema != "http")
MONERO_THROW(lws::error::configuration, "Unsupported scheme, only http or https supported");
if (std::numeric_limits<std::uint16_t>::max() < url.port)
MONERO_THROW(lws::error::configuration, "Specified port for rest server is out of range");
MONERO_THROW(lws::error::configuration, "Specified port for REST server is out of range");
if (!url.uri.empty() && url.uri.front() != '/')
MONERO_THROW(lws::error::configuration, "First path prefix character must be '/'");
if (!https)
{
@ -834,6 +927,49 @@ namespace lws
if (url.port == 0)
url.port = https ? 8443 : 8080;
if (!is_admin)
{
epee::net_utils::http::url_content admin_url{};
const boost::string_ref start{address.c_str(), address.rfind(url.uri)};
while (true) // try to merge 1+ admin prefixes
{
const auto mergeable = std::lower_bound(admin.begin(), admin.end(), start);
if (mergeable == admin.end())
break;
if (!epee::net_utils::parse_url(*mergeable, admin_url))
MONERO_THROW(lws::error::configuration, "Admin REST URL/address is invalid");
if (admin_url.port == 0)
admin_url.port = https ? 8443 : 8080;
if (url.host != admin_url.host || url.port != admin_url.port)
break; // nothing is mergeable
if (port.admin_prefix)
MONERO_THROW(lws::error::configuration, "Two admin REST servers cannot be merged onto one REST server");
if (url.uri.size() < 2 || admin_url.uri.size() < 2)
MONERO_THROW(lws::error::configuration, "Cannot merge REST server and admin REST server - a prefix must be specified for both");
if (admin_url.uri.front() != '/')
MONERO_THROW(lws::error::configuration, "Admin REST first path prefix character must be '/'");
if (admin_url.uri != admin_url.m_uri_content.m_path)
MONERO_THROW(lws::error::configuration, "Admin REST server must have path only prefix");
MINFO("Merging admin and non-admin REST servers: " << address << " + " << *mergeable);
port.admin_prefix = admin_url.m_uri_content.m_path;
admin.erase(mergeable);
} // while multiple mergable admins
}
if (url.uri != url.m_uri_content.m_path)
MONERO_THROW(lws::error::configuration, "REST server must have path only prefix");
if (url.uri.size() < 2)
url.m_uri_content.m_path.clear();
if (is_admin)
port.admin_prefix = url.m_uri_content.m_path;
else
port.prefix = url.m_uri_content.m_path;
epee::net_utils::ssl_options_t ssl_options = https ?
epee::net_utils::ssl_support_t::e_ssl_support_enabled :
epee::net_utils::ssl_support_t::e_ssl_support_disabled;
@ -846,15 +982,20 @@ namespace lws
};
bool any_ssl = false;
for (std::size_t index = 1; index < addresses.size(); ++index)
for (const std::string& address : addresses)
{
ports_.emplace_back(io_service_, ports_.front().disk.clone(), MONERO_UNWRAP(ports_.front().client.clone()));
any_ssl |= init_port(ports_.back(), addresses[index], config);
ports_.emplace_back(io_service_, disk.clone(), MONERO_UNWRAP(client.clone()));
any_ssl |= init_port(ports_.back(), address, config, false);
}
for (const std::string& address : admin)
{
ports_.emplace_back(io_service_, disk.clone(), MONERO_UNWRAP(client.clone()));
any_ssl |= init_port(ports_.back(), address, config, true);
}
const bool expect_ssl = !config.auth.private_key_path.empty();
const std::size_t threads = config.threads;
any_ssl |= init_port(ports_.front(), addresses[0], std::move(config));
if (!any_ssl && expect_ssl)
MONERO_THROW(lws::error::configuration, "Specified SSL key/cert without specifying https capable REST server");

View file

@ -56,7 +56,7 @@ namespace lws
bool allow_external;
};
explicit rest_server(epee::span<const std::string> addresses, db::storage disk, rpc::client client, configuration config);
explicit rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, configuration config);
rest_server(rest_server&&) = delete;
rest_server(rest_server const&) = delete;

View file

@ -26,8 +26,8 @@
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
set(monero-lws-rpc_sources client.cpp daemon_pub.cpp daemon_zmq.cpp light_wallet.cpp rates.cpp)
set(monero-lws-rpc_headers client.h daemon_pub.h daemon_zmq.h fwd.h json.h light_wallet.h rates.h)
set(monero-lws-rpc_sources admin.cpp client.cpp daemon_pub.cpp daemon_zmq.cpp light_wallet.cpp rates.cpp)
set(monero-lws-rpc_headers admin.h client.h daemon_pub.h daemon_zmq.h fwd.h json.h light_wallet.h rates.h)
add_library(monero-lws-rpc ${monero-lws-rpc_sources} ${monero-lws-rpc_headers})
target_link_libraries(monero-lws-rpc monero::libraries monero-lws-wire-json)

198
src/rpc/admin.cpp Normal file
View file

@ -0,0 +1,198 @@
// Copyright (c) 2023, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "admin.h"
#include <boost/range/iterator_range.hpp>
#include <functional>
#include <utility>
#include "db/string.h"
#include "error.h"
#include "span.h" // monero/contrib/epee/include
#include "wire.h"
#include "wire/crypto.h"
#include "wire/error.h"
#include "wire/json/write.h"
#include "wire/traits.h"
#include "wire/vector.h"
namespace
{
// Do not output "full" debug data provided by `db::data.h` header; truncate output
template<typename T>
struct truncated
{
T value;
};
lws::db::account_address wire_unwrap(const boost::string_ref source)
{
const expect<lws::db::account_address> address = lws::db::address_string(source);
if (!address)
WIRE_DLOG_THROW(wire::error::schema::string, "Bad string to address conversion: " << address.error().message());
return *address;
}
using base58_address = truncated<lws::db::account_address&>;
void read_bytes(wire::reader& source, base58_address& dest)
{
dest.value = wire_unwrap(source.string());
}
void write_bytes(wire::writer& dest, const truncated<lws::db::account>& self)
{
wire::object(dest,
wire::field("address", lws::db::address_string(self.value.address)),
wire::field("scan_height", self.value.scan_height),
wire::field("access_time", self.value.access)
);
}
void write_bytes(wire::writer& dest, const truncated<lws::db::request_info>& self)
{
wire::object(dest,
wire::field("address", lws::db::address_string(self.value.address)),
wire::field("start_height", self.value.start_height)
);
}
template<typename V>
void write_bytes(wire::json_writer& dest, const truncated<boost::iterator_range<lmdb::value_iterator<V>>> self)
{
const auto truncate = [] (V src) { return truncated<V>{std::move(src)}; };
wire::array(dest, std::move(self.value), truncate);
}
template<typename K, typename V, typename C>
expect<void> stream_object(wire::json_writer& dest, expect<lmdb::key_stream<K, V, C>> self)
{
using value_range = boost::iterator_range<lmdb::value_iterator<V>>;
const auto truncate = [] (value_range src) -> truncated<value_range>
{
return {std::move(src)};
};
if (!self)
return self.error();
wire::dynamic_object(dest, self->make_range(), wire::enum_as_string, truncate);
return success();
}
template<typename T, typename U>
void read_addresses(wire::reader& source, T& self, U field)
{
std::vector<std::string> addresses;
wire::object(source, wire::field("addresses", std::ref(addresses)), std::move(field));
self.addresses.reserve(addresses.size());
for (const auto& elem : addresses)
self.addresses.emplace_back(wire_unwrap(elem));
}
void write_addresses(wire::writer& dest, epee::span<const lws::db::account_address> self)
{
// writes an array of monero base58 address strings
wire::object(dest, wire::field("updated", wire::as_array(self, lws::db::address_string)));
}
expect<void> write_addresses(wire::writer& dest, const expect<std::vector<lws::db::account_address>>& self)
{
if (!self)
return self.error();
write_addresses(dest, epee::to_span(*self));
return success();
}
} // anonymous
namespace lws { namespace rpc
{
void read_bytes(wire::reader& source, add_account_req& self)
{
wire::object(source,
wire::field("address", base58_address{self.address}),
wire::field("key", std::ref(unwrap(unwrap(self.key))))
);
}
void read_bytes(wire::reader& source, address_requests& self)
{
read_addresses(source, self, WIRE_FIELD(type));
}
void read_bytes(wire::reader& source, modify_account_req& self)
{
read_addresses(source, self, WIRE_FIELD(status));
}
void read_bytes(wire::reader& source, rescan_req& self)
{
read_addresses(source, self, WIRE_FIELD(height));
}
expect<void> accept_requests_::operator()(wire::writer& dest, db::storage disk, const request& req) const
{
return write_addresses(dest, disk.accept_requests(req.type, epee::to_span(req.addresses)));
}
expect<void> add_account_::operator()(wire::writer& out, db::storage disk, const request& req) const
{
using span = epee::span<const lws::db::account_address>;
MONERO_CHECK(disk.add_account(req.address, req.key));
write_addresses(out, span{std::addressof(req.address), 1});
return success();
}
expect<void> list_accounts_::operator()(wire::json_writer& dest, db::storage disk) const
{
auto reader = disk.start_read();
if (!reader)
return reader.error();
return stream_object(dest, reader->get_accounts());
}
expect<void> list_requests_::operator()(wire::json_writer& dest, db::storage disk) const
{
auto reader = disk.start_read();
if (!reader)
return reader.error();
return stream_object(dest, reader->get_requests());
}
expect<void> modify_account_::operator()(wire::writer& dest, db::storage disk, const request& req) const
{
return write_addresses(dest, disk.change_status(req.status, epee::to_span(req.addresses)));
}
expect<void> reject_requests_::operator()(wire::writer& dest, db::storage disk, const request& req) const
{
return write_addresses(dest, disk.reject_requests(req.type, epee::to_span(req.addresses)));
}
expect<void> rescan_::operator()(wire::writer& dest, db::storage disk, const request& req) const
{
return write_addresses(dest, disk.rescan(req.height, epee::to_span(req.addresses)));
}
}} // lws // rpc

125
src/rpc/admin.h Normal file
View file

@ -0,0 +1,125 @@
// Copyright (c) 2023, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <string>
#include <vector>
#include "common/expect.h" // monero/src
#include "db/data.h"
#include "db/storage.h"
#include "wire/fwd.h"
#include "wire/json/fwd.h"
namespace lws
{
namespace rpc
{
struct add_account_req
{
db::account_address address;
crypto::secret_key key;
};
void read_bytes(wire::reader&, add_account_req&);
//! Request object for `accept_requests` and `reject_requests` endpoints.
struct address_requests
{
std::vector<db::account_address> addresses;
db::request type;
};
void read_bytes(wire::reader&, address_requests&);
struct modify_account_req
{
std::vector<db::account_address> addresses;
db::account_status status;
};
void read_bytes(wire::reader&, modify_account_req&);
struct rescan_req
{
std::vector<db::account_address> addresses;
db::block_id height;
};
void read_bytes(wire::reader&, rescan_req&);
struct accept_requests_
{
using request = address_requests;
expect<void> operator()(wire::writer& dest, db::storage disk, const request& req) const;
};
constexpr const accept_requests_ accept_requests{};
struct add_account_
{
using request = add_account_req;
expect<void> operator()(wire::writer& dest, db::storage disk, const request& req) const;
};
constexpr const add_account_ add_account{};
struct list_accounts_
{
using request = expect<void>;
expect<void> operator()(wire::json_writer& dest, db::storage disk) const;
expect<void> operator()(wire::json_writer& dest, db::storage disk, const request&) const
{ return (*this)(dest, std::move(disk)); }
};
constexpr const list_accounts_ list_accounts{};
struct list_requests_
{
using request = expect<void>;
expect<void> operator()(wire::json_writer& dest, db::storage disk) const;
expect<void> operator()(wire::json_writer& dest, db::storage disk, const request&) const
{ return (*this)(dest, std::move(disk)); }
};
constexpr const list_requests_ list_requests{};
struct modify_account_
{
using request = modify_account_req;
expect<void> operator()(wire::writer& dest, db::storage disk, const request& req) const;
};
constexpr const modify_account_ modify_account{};
struct reject_requests_
{
using request = address_requests;
expect<void> operator()(wire::writer& dest, db::storage disk, const request& req) const;
};
constexpr const reject_requests_ reject_requests{};
struct rescan_
{
using request = rescan_req;
expect<void> operator()(wire::writer& dest, db::storage disk, const request& req) const;
};
constexpr const rescan_ rescan{};
}} // lws // rpc

View file

@ -57,6 +57,7 @@ namespace
const command_line::arg_descriptor<std::string> daemon_rpc;
const command_line::arg_descriptor<std::string> daemon_sub;
const command_line::arg_descriptor<std::vector<std::string>> rest_servers;
const command_line::arg_descriptor<std::vector<std::string>> admin_rest_servers;
const command_line::arg_descriptor<std::string> rest_ssl_key;
const command_line::arg_descriptor<std::string> rest_ssl_cert;
const command_line::arg_descriptor<std::size_t> rest_threads;
@ -87,7 +88,8 @@ namespace
: lws::options()
, daemon_rpc{"daemon", "<protocol>://<address>:<port> of a monerod ZMQ RPC", get_default_zmq()}
, daemon_sub{"sub", "tcp://address:port or ipc://path of a monerod ZMQ Pub", ""}
, rest_servers{"rest-server", "[(https|http)://<address>:]<port> for incoming connections, multiple declarations allowed"}
, rest_servers{"rest-server", "[(https|http)://<address>:]<port>[/<prefix>] for incoming connections, multiple declarations allowed"}
, admin_rest_servers{"admin-rest-server", "[(https|http])://<address>:]<port>[/<prefix>] for incoming admin connections, multiple declarations allowed"}
, rest_ssl_key{"rest-ssl-key", "<path> to PEM formatted SSL key for https REST server", ""}
, rest_ssl_cert{"rest-ssl-certificate", "<path> to PEM formatted SSL certificate (chains supported) for https REST server", ""}
, rest_threads{"rest-threads", "Number of threads to process REST connections", 1}
@ -107,6 +109,7 @@ namespace
command_line::add_arg(description, daemon_rpc);
command_line::add_arg(description, daemon_sub);
description.add_options()(rest_servers.name, boost::program_options::value<std::vector<std::string>>()->default_value({rest_default}, rest_default), rest_servers.description);
command_line::add_arg(description, admin_rest_servers);
command_line::add_arg(description, rest_ssl_key);
command_line::add_arg(description, rest_ssl_cert);
command_line::add_arg(description, rest_threads);
@ -123,6 +126,7 @@ namespace
{
std::string db_path;
std::vector<std::string> rest_servers;
std::vector<std::string> admin_rest_servers;
lws::rest_server::configuration rest_config;
std::string daemon_rpc;
std::string daemon_sub;
@ -168,6 +172,7 @@ namespace
program prog{
command_line::get_arg(args, opts.db_path),
command_line::get_arg(args, opts.rest_servers),
command_line::get_arg(args, opts.admin_rest_servers),
lws::rest_server::configuration{
{command_line::get_arg(args, opts.rest_ssl_key), command_line::get_arg(args, opts.rest_ssl_cert)},
command_line::get_arg(args, opts.access_controls),
@ -201,9 +206,13 @@ namespace
MINFO("Using monerod ZMQ RPC at " << ctx.daemon_address());
auto client = lws::scanner::sync(disk.clone(), ctx.connect().value()).value();
lws::rest_server server{epee::to_span(prog.rest_servers), disk.clone(), std::move(client), std::move(prog.rest_config)};
lws::rest_server server{
epee::to_span(prog.rest_servers), prog.admin_rest_servers, disk.clone(), std::move(client), std::move(prog.rest_config)
};
for (const std::string& address : prog.rest_servers)
MINFO("Listening for REST clients at " << address);
for (const std::string& address : prog.admin_rest_servers)
MINFO("Listening for REST admin clients at " << address);
// blocks until SIGINT
lws::scanner::run(std::move(disk), std::move(ctx), prog.scan_threads);