mirror of
https://github.com/vtnerd/monero-lws.git
synced 2024-11-16 17:27:39 +00:00
Add support for admin REST server (#62)
This commit is contained in:
parent
c958ac7963
commit
7b1e1e58f8
13 changed files with 659 additions and 118 deletions
|
@ -108,7 +108,7 @@ load_cache(${MONERO_BUILD_DIR} READ_WITH_PREFIX monero_
|
||||||
HIDAPI_LIBRARY
|
HIDAPI_LIBRARY
|
||||||
LMDB_INCLUDE
|
LMDB_INCLUDE
|
||||||
monero_SOURCE_DIR
|
monero_SOURCE_DIR
|
||||||
OPENSSL_INCLUDE_PATH
|
OPENSSL_INCLUDE_DIR
|
||||||
OPENSSL_CRYPTO_LIBRARY
|
OPENSSL_CRYPTO_LIBRARY
|
||||||
OPENSSL_SSL_LIBRARY
|
OPENSSL_SSL_LIBRARY
|
||||||
SODIUM_LIBRARY
|
SODIUM_LIBRARY
|
||||||
|
@ -217,7 +217,7 @@ set_property(TARGET monero::libraries PROPERTY
|
||||||
INTERFACE_INCLUDE_DIRECTORIES
|
INTERFACE_INCLUDE_DIRECTORIES
|
||||||
${Boost_INCLUDE_DIR}
|
${Boost_INCLUDE_DIR}
|
||||||
${monero_HIDAPI_INCLUDE_DIRS}
|
${monero_HIDAPI_INCLUDE_DIRS}
|
||||||
${monero_OPENSSL_INCLUDE_PATH}
|
${monero_OPENSSL_INCLUDE_DIR}
|
||||||
"${MONERO_BUILD_DIR}/generated_include"
|
"${MONERO_BUILD_DIR}/generated_include"
|
||||||
"${MONERO_SOURCE_DIR}/contrib/epee/include"
|
"${MONERO_SOURCE_DIR}/contrib/epee/include"
|
||||||
"${MONERO_SOURCE_DIR}/external/easylogging++"
|
"${MONERO_SOURCE_DIR}/external/easylogging++"
|
||||||
|
|
|
@ -1,28 +1,91 @@
|
||||||
# Using monero-lws-admin
|
# monero-lws Administration
|
||||||
|
The `monero-lws-admin` executable or `--admin-rest-server` option in the
|
||||||
The `monero-lws-admin` executable is used to administer the database used by
|
`monero-lws-daemon` executable can be used to administer the database
|
||||||
`monero-lws-daemon`. Any number of `monero-lws-admin` instances can run
|
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.
|
concurrently with a single `monero-lws-daemon` instance on the same database.
|
||||||
Administration is necessary to authorize new accounts and rescan requests
|
Administration is necessary to authorize new accounts and rescan requests
|
||||||
submitted from the REST API. The admin executable can also be used to list
|
submitted from the REST API. The admin executable can also be used to list
|
||||||
the contents of the LMDB file for debugging purposes.
|
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
|
The `monero-lws-admin` utility is structured around command-line arguments with
|
||||||
JSON responses printed to `stdout`. Each administration command takes arguments
|
JSON responses printed to `stdout`. Each administration command takes arguments
|
||||||
by position - the design makes it potentially compatible with a JSON or MsgPack
|
by position. Every available administration command and required+optional
|
||||||
array (as used in JSON-RPC, etc). Every available administration command and
|
arguments are listed when the `--help` flag is given to the executable.
|
||||||
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
|
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
|
`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
|
indenting the output to make it more readable, and can be used to
|
||||||
search+filter the JSON output from the command.
|
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
|
# 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:**
|
**List every active Monero address on a newline:**
|
||||||
```bash
|
```bash
|
||||||
monero-lws-admin list_accounts | jq -r '.active | .[] | .address'
|
monero-lws-admin list_accounts | jq -r '.active | .[] | .address'
|
||||||
|
|
|
@ -68,6 +68,7 @@ target_link_libraries(monero-lws-admin
|
||||||
monero::libraries
|
monero::libraries
|
||||||
monero-lws-common
|
monero-lws-common
|
||||||
monero-lws-db
|
monero-lws-db
|
||||||
|
monero-lws-rpc
|
||||||
monero-lws-wire-json
|
monero-lws-wire-json
|
||||||
${Boost_PROGRAM_OPTIONS_LIBRARY}
|
${Boost_PROGRAM_OPTIONS_LIBRARY}
|
||||||
Threads::Threads
|
Threads::Threads
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
#include <boost/program_options/options_description.hpp>
|
#include <boost/program_options/options_description.hpp>
|
||||||
#include <boost/program_options/parsers.hpp>
|
#include <boost/program_options/parsers.hpp>
|
||||||
#include <boost/program_options/variables_map.hpp>
|
#include <boost/program_options/variables_map.hpp>
|
||||||
|
#include <boost/range/adaptor/filtered.hpp>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
@ -47,63 +48,45 @@
|
||||||
#include "db/string.h"
|
#include "db/string.h"
|
||||||
#include "options.h"
|
#include "options.h"
|
||||||
#include "misc_log_ex.h" // monero/contrib/epee/include
|
#include "misc_log_ex.h" // monero/contrib/epee/include
|
||||||
|
#include "rpc/admin.h"
|
||||||
#include "span.h" // monero/contrib/epee/include
|
#include "span.h" // monero/contrib/epee/include
|
||||||
#include "string_tools.h" // monero/contrib/epee/include
|
#include "string_tools.h" // monero/contrib/epee/include
|
||||||
|
#include "wire/crypto.h"
|
||||||
#include "wire/filters.h"
|
#include "wire/filters.h"
|
||||||
#include "wire/json/write.h"
|
#include "wire/json/write.h"
|
||||||
|
|
||||||
namespace
|
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>
|
template<typename T>
|
||||||
struct truncated
|
struct admin_display
|
||||||
{
|
{
|
||||||
T value;
|
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::object(dest,
|
||||||
wire::field("address", lws::db::address_string(self.value.address)),
|
wire::field("address", lws::db::address_string(source.value.address)),
|
||||||
wire::field("scan_height", self.value.scan_height),
|
wire::field("key", std::cref(source.value.key))
|
||||||
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)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename V>
|
void write_bytes(wire::json_writer& dest, admin_display<boost::iterator_range<lmdb::value_iterator<lws::db::account>>> source)
|
||||||
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)}; };
|
const auto filter = [](const lws::db::account& src)
|
||||||
wire::array(dest, std::move(self.value), truncate);
|
{ 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>
|
template<typename F, typename... T>
|
||||||
void stream_json_object(std::ostream& dest, boost::iterator_range<lmdb::key_iterator<K, V>> self)
|
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::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();
|
stream.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +134,7 @@ namespace
|
||||||
arguments.remove_prefix(1);
|
arguments.remove_prefix(1);
|
||||||
|
|
||||||
std::vector<lws::db::account_address> addresses{};
|
std::vector<lws::db::account_address> addresses{};
|
||||||
|
addresses.reserve(arguments.size());
|
||||||
for (std::string const& address : arguments)
|
for (std::string const& address : arguments)
|
||||||
addresses.push_back(lws::db::address_string(address).value());
|
addresses.push_back(lws::db::address_string(address).value());
|
||||||
return addresses;
|
return addresses;
|
||||||
|
@ -161,15 +145,11 @@ namespace
|
||||||
if (prog.arguments.size() < 2)
|
if (prog.arguments.size() < 2)
|
||||||
throw std::runtime_error{"accept_requests requires 2 or more arguments"};
|
throw std::runtime_error{"accept_requests requires 2 or more arguments"};
|
||||||
|
|
||||||
const lws::db::request req =
|
lws::rpc::address_requests req{
|
||||||
MONERO_UNWRAP(lws::db::request_from_string(prog.arguments[0]));
|
get_addresses(epee::to_span(prog.arguments)),
|
||||||
std::vector<lws::db::account_address> addresses =
|
MONERO_UNWRAP(lws::db::request_from_string(prog.arguments[0]))
|
||||||
get_addresses(epee::to_span(prog.arguments));
|
};
|
||||||
|
run_command(lws::rpc::accept_requests, out, std::move(prog.disk), std::move(req));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void add_account(program prog, std::ostream& out)
|
void add_account(program prog, std::ostream& out)
|
||||||
|
@ -177,13 +157,31 @@ namespace
|
||||||
if (prog.arguments.size() != 2)
|
if (prog.arguments.size() != 2)
|
||||||
throw std::runtime_error{"add_account needs exactly two arguments"};
|
throw std::runtime_error{"add_account needs exactly two arguments"};
|
||||||
|
|
||||||
const lws::db::account_address address[1] = {
|
lws::rpc::add_account_req req{
|
||||||
lws::db::address_string(prog.arguments[0]).value()
|
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));
|
void create_admin(program prog, std::ostream& out)
|
||||||
write_json_addresses(out, address);
|
{
|
||||||
|
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)
|
void debug_database(program prog, std::ostream& out)
|
||||||
|
@ -199,20 +197,31 @@ namespace
|
||||||
{
|
{
|
||||||
if (!prog.arguments.empty())
|
if (!prog.arguments.empty())
|
||||||
throw std::runtime_error{"list_accounts takes zero arguments"};
|
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();
|
void list_admin(program prog, std::ostream& out)
|
||||||
auto stream = reader.get_accounts().value();
|
{
|
||||||
stream_json_object(out, stream.make_range());
|
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)
|
void list_requests(program prog, std::ostream& out)
|
||||||
{
|
{
|
||||||
if (!prog.arguments.empty())
|
if (!prog.arguments.empty())
|
||||||
throw std::runtime_error{"list_requests takes zero arguments"};
|
throw std::runtime_error{"list_requests takes zero arguments"};
|
||||||
|
run_command(lws::rpc::list_requests, out, std::move(prog.disk));
|
||||||
auto reader = prog.disk.start_read().value();
|
|
||||||
auto stream = reader.get_requests().value();
|
|
||||||
stream_json_object(out, stream.make_range());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void modify_account(program prog, std::ostream& out)
|
void modify_account(program prog, std::ostream& out)
|
||||||
|
@ -220,15 +229,11 @@ namespace
|
||||||
if (prog.arguments.size() < 2)
|
if (prog.arguments.size() < 2)
|
||||||
throw std::runtime_error{"modify_account_status requires 2 or more arguments"};
|
throw std::runtime_error{"modify_account_status requires 2 or more arguments"};
|
||||||
|
|
||||||
const lws::db::account_status status =
|
lws::rpc::modify_account_req req{
|
||||||
lws::db::account_status_from_string(prog.arguments[0]).value();
|
get_addresses(epee::to_span(prog.arguments)),
|
||||||
std::vector<lws::db::account_address> addresses =
|
lws::db::account_status_from_string(prog.arguments[0]).value()
|
||||||
get_addresses(epee::to_span(prog.arguments));
|
};
|
||||||
|
run_command(lws::rpc::modify_account, out, std::move(prog.disk), std::move(req));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void reject_requests(program prog, std::ostream& out)
|
void reject_requests(program prog, std::ostream& out)
|
||||||
|
@ -236,12 +241,11 @@ namespace
|
||||||
if (prog.arguments.size() < 2)
|
if (prog.arguments.size() < 2)
|
||||||
MONERO_THROW(common_error::kInvalidArgument, "reject_requests requires 2 or more arguments");
|
MONERO_THROW(common_error::kInvalidArgument, "reject_requests requires 2 or more arguments");
|
||||||
|
|
||||||
const lws::db::request req =
|
lws::rpc::address_requests req{
|
||||||
lws::db::request_from_string(prog.arguments[0]).value();
|
get_addresses(epee::to_span(prog.arguments)),
|
||||||
std::vector<lws::db::account_address> addresses =
|
lws::db::request_from_string(prog.arguments[0]).value()
|
||||||
get_addresses(epee::to_span(prog.arguments));
|
};
|
||||||
|
run_command(lws::rpc::reject_requests, out, std::move(prog.disk), std::move(req));
|
||||||
MONERO_UNWRAP(prog.disk.reject_requests(req, epee::to_span(addresses)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void rescan(program prog, std::ostream& out)
|
void rescan(program prog, std::ostream& out)
|
||||||
|
@ -249,14 +253,11 @@ namespace
|
||||||
if (prog.arguments.size() < 2)
|
if (prog.arguments.size() < 2)
|
||||||
throw std::runtime_error{"rescan requires 2 or more arguments"};
|
throw std::runtime_error{"rescan requires 2 or more arguments"};
|
||||||
|
|
||||||
const auto height = lws::db::block_id(std::stoull(prog.arguments[0]));
|
lws::rpc::rescan_req req{
|
||||||
const std::vector<lws::db::account_address> addresses =
|
get_addresses(epee::to_span(prog.arguments)),
|
||||||
get_addresses(epee::to_span(prog.arguments));
|
lws::db::block_id(std::stoull(prog.arguments[0]))
|
||||||
|
};
|
||||||
const std::vector<lws::db::account_address> updated =
|
run_command(lws::rpc::rescan, out, std::move(prog.disk), std::move(req));
|
||||||
prog.disk.rescan(height, epee::to_span(addresses)).value();
|
|
||||||
|
|
||||||
write_json_addresses(out, epee::to_span(updated));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void rollback(program prog, std::ostream& out)
|
void rollback(program prog, std::ostream& out)
|
||||||
|
@ -277,14 +278,16 @@ namespace
|
||||||
char const* const name;
|
char const* const name;
|
||||||
void (*const handler)(program, std::ostream&);
|
void (*const handler)(program, std::ostream&);
|
||||||
char const* const parameters;
|
char const* const parameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr const command commands[] =
|
static constexpr const command commands[] =
|
||||||
{
|
{
|
||||||
{"accept_requests", &accept_requests, "<\"create\"|\"import\"> <base58 address> [base 58 address]..."},
|
{"accept_requests", &accept_requests, "<\"create\"|\"import\"> <base58 address> [base 58 address]..."},
|
||||||
{"add_account", &add_account, "<base58 address> <view key hex>"},
|
{"add_account", &add_account, "<base58 address> <view key hex>"},
|
||||||
|
{"create_admin", &create_admin, ""},
|
||||||
{"debug_database", &debug_database, ""},
|
{"debug_database", &debug_database, ""},
|
||||||
{"list_accounts", &list_accounts, ""},
|
{"list_accounts", &list_accounts, ""},
|
||||||
|
{"list_admin", &list_admin, ""},
|
||||||
{"list_requests", &list_requests, ""},
|
{"list_requests", &list_requests, ""},
|
||||||
{"modify_account_status", &modify_account, "<\"active\"|\"inactive\"|\"hidden\"> <base58 address> [base 58 address]..."},
|
{"modify_account_status", &modify_account, "<\"active\"|\"inactive\"|\"hidden\"> <base58 address> [base 58 address]..."},
|
||||||
{"reject_requests", &reject_requests, "<\"create\"|\"import\"> <base58 address> [base 58 address]..."},
|
{"reject_requests", &reject_requests, "<\"create\"|\"import\"> <base58 address> [base 58 address]..."},
|
||||||
|
|
|
@ -81,7 +81,7 @@ namespace db
|
||||||
enum account_flags : std::uint8_t
|
enum account_flags : std::uint8_t
|
||||||
{
|
{
|
||||||
default_account = 0,
|
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
|
account_generated_locally = 2 //!< Flag sent by client on initial login request
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1211,9 +1211,10 @@ namespace db
|
||||||
return {lws::error::bad_view_key};
|
return {lws::error::bad_view_key};
|
||||||
}
|
}
|
||||||
|
|
||||||
const account_by_address by_address{
|
const account_status status =
|
||||||
user.address, {user.id, account_status::active}
|
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 key = lmdb::to_val(by_address_version);
|
||||||
MDB_val value = lmdb::to_val(by_address);
|
MDB_val value = lmdb::to_val(by_address);
|
||||||
|
@ -1239,10 +1240,10 @@ namespace db
|
||||||
}
|
}
|
||||||
} // anonymous
|
} // 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);
|
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();
|
const expect<db::account_time> current_time = get_account_time();
|
||||||
if (!current_time)
|
if (!current_time)
|
||||||
|
@ -1283,7 +1284,7 @@ namespace db
|
||||||
user.scan_height = *height;
|
user.scan_height = *height;
|
||||||
user.access = *current_time;
|
user.access = *current_time;
|
||||||
user.creation = *current_time;
|
user.creation = *current_time;
|
||||||
// ... leave flags set to zero ...
|
user.flags = flags;
|
||||||
|
|
||||||
return do_add_account(
|
return do_add_account(
|
||||||
*accounts_cur, *accounts_ba_cur, *accounts_bh_cur, user
|
*accounts_cur, *accounts_ba_cur, *accounts_bh_cur, user
|
||||||
|
|
|
@ -194,7 +194,7 @@ namespace db
|
||||||
|
|
||||||
|
|
||||||
//! Add an account, for immediate inclusion in the active list.
|
//! 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.
|
//! Reset `addresses` to `height` for scanning.
|
||||||
expect<std::vector<account_address>>
|
expect<std::vector<account_address>>
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
#include "lmdb/util.h" // monero/src
|
#include "lmdb/util.h" // monero/src
|
||||||
#include "net/http_base.h" // monero/contrib/epee/include
|
#include "net/http_base.h" // monero/contrib/epee/include
|
||||||
#include "net/net_parse_helpers.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/client.h"
|
||||||
#include "rpc/daemon_messages.h" // monero/src
|
#include "rpc/daemon_messages.h" // monero/src
|
||||||
#include "rpc/light_wallet.h"
|
#include "rpc/light_wallet.h"
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
#include "util/gamma_picker.h"
|
#include "util/gamma_picker.h"
|
||||||
#include "util/random_outputs.h"
|
#include "util/random_outputs.h"
|
||||||
#include "util/source_location.h"
|
#include "util/source_location.h"
|
||||||
|
#include "wire/crypto.h"
|
||||||
#include "wire/json.h"
|
#include "wire/json.h"
|
||||||
|
|
||||||
namespace lws
|
namespace lws
|
||||||
|
@ -669,6 +671,56 @@ namespace lws
|
||||||
return wire::json::to_bytes<response>(*resp);
|
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
|
struct endpoint
|
||||||
{
|
{
|
||||||
char const* const name;
|
char const* const name;
|
||||||
|
@ -688,6 +740,17 @@ namespace lws
|
||||||
{"/submit_raw_tx", call<submit_raw_tx>, 50 * 1024}
|
{"/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_
|
struct by_name_
|
||||||
{
|
{
|
||||||
bool operator()(endpoint const& left, endpoint const& right) const noexcept
|
bool operator()(endpoint const& left, endpoint const& right) const noexcept
|
||||||
|
@ -716,23 +779,51 @@ namespace lws
|
||||||
{
|
{
|
||||||
db::storage disk;
|
db::storage disk;
|
||||||
rpc::client client;
|
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)
|
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)
|
: lws::http_server_impl_base<rest_server::internal, context>(io_service)
|
||||||
, disk(std::move(disk))
|
, disk(std::move(disk))
|
||||||
, client(std::move(client))
|
, client(std::move(client))
|
||||||
|
, prefix()
|
||||||
|
, admin_prefix()
|
||||||
{
|
{
|
||||||
assert(std::is_sorted(std::begin(endpoints), std::end(endpoints), by_name));
|
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
|
virtual bool
|
||||||
handle_http_request(const http::http_request_info& query, http::http_response_info& response, context& ctx)
|
handle_http_request(const http::http_request_info& query, http::http_response_info& response, context& ctx)
|
||||||
override final
|
override final
|
||||||
{
|
{
|
||||||
const auto handler = std::lower_bound(
|
endpoint const* const handler = get_endpoint(query.m_URI);
|
||||||
std::begin(endpoints), std::end(endpoints), query.m_URI, by_name
|
if (!handler)
|
||||||
);
|
|
||||||
if (handler == std::end(endpoints) || handler->name != query.m_URI)
|
|
||||||
{
|
{
|
||||||
response.m_response_code = 404;
|
response.m_response_code = 404;
|
||||||
response.m_response_comment = "Not Found";
|
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_()
|
: io_service_(), ports_()
|
||||||
{
|
{
|
||||||
ports_.emplace_back(io_service_, std::move(disk), std::move(client));
|
|
||||||
|
|
||||||
if (addresses.empty())
|
if (addresses.empty())
|
||||||
MONERO_THROW(common_error::kInvalidArgument, "REST server requires 1 or more addresses");
|
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{};
|
epee::net_utils::http::url_content url{};
|
||||||
if (!epee::net_utils::parse_url(address, 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";
|
const bool https = url.schema == "https";
|
||||||
if (!https && url.schema != "http")
|
if (!https && url.schema != "http")
|
||||||
MONERO_THROW(lws::error::configuration, "Unsupported scheme, only http or https supported");
|
MONERO_THROW(lws::error::configuration, "Unsupported scheme, only http or https supported");
|
||||||
|
|
||||||
if (std::numeric_limits<std::uint16_t>::max() < url.port)
|
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)
|
if (!https)
|
||||||
{
|
{
|
||||||
|
@ -834,6 +927,49 @@ namespace lws
|
||||||
if (url.port == 0)
|
if (url.port == 0)
|
||||||
url.port = https ? 8443 : 8080;
|
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_options_t ssl_options = https ?
|
||||||
epee::net_utils::ssl_support_t::e_ssl_support_enabled :
|
epee::net_utils::ssl_support_t::e_ssl_support_enabled :
|
||||||
epee::net_utils::ssl_support_t::e_ssl_support_disabled;
|
epee::net_utils::ssl_support_t::e_ssl_support_disabled;
|
||||||
|
@ -846,15 +982,20 @@ namespace lws
|
||||||
};
|
};
|
||||||
|
|
||||||
bool any_ssl = false;
|
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()));
|
ports_.emplace_back(io_service_, disk.clone(), MONERO_UNWRAP(client.clone()));
|
||||||
any_ssl |= init_port(ports_.back(), addresses[index], config);
|
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 bool expect_ssl = !config.auth.private_key_path.empty();
|
||||||
const std::size_t threads = config.threads;
|
const std::size_t threads = config.threads;
|
||||||
any_ssl |= init_port(ports_.front(), addresses[0], std::move(config));
|
|
||||||
if (!any_ssl && expect_ssl)
|
if (!any_ssl && expect_ssl)
|
||||||
MONERO_THROW(lws::error::configuration, "Specified SSL key/cert without specifying https capable REST server");
|
MONERO_THROW(lws::error::configuration, "Specified SSL key/cert without specifying https capable REST server");
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ namespace lws
|
||||||
bool allow_external;
|
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&&) = delete;
|
||||||
rest_server(rest_server const&) = delete;
|
rest_server(rest_server const&) = delete;
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
# 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.
|
# 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_sources admin.cpp 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_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})
|
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)
|
target_link_libraries(monero-lws-rpc monero::libraries monero-lws-wire-json)
|
||||||
|
|
198
src/rpc/admin.cpp
Normal file
198
src/rpc/admin.cpp
Normal 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
125
src/rpc/admin.h
Normal 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
|
|
@ -57,6 +57,7 @@ namespace
|
||||||
const command_line::arg_descriptor<std::string> daemon_rpc;
|
const command_line::arg_descriptor<std::string> daemon_rpc;
|
||||||
const command_line::arg_descriptor<std::string> daemon_sub;
|
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>> 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_key;
|
||||||
const command_line::arg_descriptor<std::string> rest_ssl_cert;
|
const command_line::arg_descriptor<std::string> rest_ssl_cert;
|
||||||
const command_line::arg_descriptor<std::size_t> rest_threads;
|
const command_line::arg_descriptor<std::size_t> rest_threads;
|
||||||
|
@ -87,7 +88,8 @@ namespace
|
||||||
: lws::options()
|
: lws::options()
|
||||||
, daemon_rpc{"daemon", "<protocol>://<address>:<port> of a monerod ZMQ RPC", get_default_zmq()}
|
, 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", ""}
|
, 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_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_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}
|
, 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_rpc);
|
||||||
command_line::add_arg(description, daemon_sub);
|
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);
|
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_key);
|
||||||
command_line::add_arg(description, rest_ssl_cert);
|
command_line::add_arg(description, rest_ssl_cert);
|
||||||
command_line::add_arg(description, rest_threads);
|
command_line::add_arg(description, rest_threads);
|
||||||
|
@ -123,6 +126,7 @@ namespace
|
||||||
{
|
{
|
||||||
std::string db_path;
|
std::string db_path;
|
||||||
std::vector<std::string> rest_servers;
|
std::vector<std::string> rest_servers;
|
||||||
|
std::vector<std::string> admin_rest_servers;
|
||||||
lws::rest_server::configuration rest_config;
|
lws::rest_server::configuration rest_config;
|
||||||
std::string daemon_rpc;
|
std::string daemon_rpc;
|
||||||
std::string daemon_sub;
|
std::string daemon_sub;
|
||||||
|
@ -168,6 +172,7 @@ namespace
|
||||||
program prog{
|
program prog{
|
||||||
command_line::get_arg(args, opts.db_path),
|
command_line::get_arg(args, opts.db_path),
|
||||||
command_line::get_arg(args, opts.rest_servers),
|
command_line::get_arg(args, opts.rest_servers),
|
||||||
|
command_line::get_arg(args, opts.admin_rest_servers),
|
||||||
lws::rest_server::configuration{
|
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.rest_ssl_key), command_line::get_arg(args, opts.rest_ssl_cert)},
|
||||||
command_line::get_arg(args, opts.access_controls),
|
command_line::get_arg(args, opts.access_controls),
|
||||||
|
@ -201,9 +206,13 @@ namespace
|
||||||
MINFO("Using monerod ZMQ RPC at " << ctx.daemon_address());
|
MINFO("Using monerod ZMQ RPC at " << ctx.daemon_address());
|
||||||
auto client = lws::scanner::sync(disk.clone(), ctx.connect().value()).value();
|
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)
|
for (const std::string& address : prog.rest_servers)
|
||||||
MINFO("Listening for REST clients at " << address);
|
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
|
// blocks until SIGINT
|
||||||
lws::scanner::run(std::move(disk), std::move(ctx), prog.scan_threads);
|
lws::scanner::run(std::move(disk), std::move(ctx), prog.scan_threads);
|
||||||
|
|
Loading…
Reference in a new issue