diff --git a/CMakeLists.txt b/CMakeLists.txt index 4635c84..1782191 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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++" diff --git a/docs/administration.md b/docs/administration.md index 40f2f54..85a06f3 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -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' diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c940cf3..bc2b0d5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/admin_main.cpp b/src/admin_main.cpp index a2812b1..52c78c7 100644 --- a/src/admin_main.cpp +++ b/src/admin_main.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -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 - struct truncated + struct admin_display { T value; }; - void write_bytes(wire::json_writer& dest, const truncated& self) + void write_bytes(wire::json_writer& dest, const admin_display& 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& 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 - void write_bytes(wire::json_writer& dest, const truncated>> self) + void write_bytes(wire::json_writer& dest, admin_display>> source) { - const auto truncate = [] (V src) { return truncated{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{std::move(src)}; }; + + wire::array(dest, (source.value | boost::adaptors::filtered(filter)), transform); } - template - void stream_json_object(std::ostream& dest, boost::iterator_range> self) + template + void run_command(F f, std::ostream& dest, T&&... args) { - using value_range = boost::iterator_range>; - const auto truncate = [] (value_range src) -> truncated - { - 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 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(args)...)); stream.finish(); } @@ -151,6 +134,7 @@ namespace arguments.remove_prefix(1); std::vector 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 addresses = - get_addresses(epee::to_span(prog.arguments)); - - const std::vector 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 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>; + const auto transform = [] (value_range user) + { return admin_display{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 addresses = - get_addresses(epee::to_span(prog.arguments)); - - const std::vector 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 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 addresses = - get_addresses(epee::to_span(prog.arguments)); - - const std::vector 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\"> [base 58 address]..."}, {"add_account", &add_account, " "}, + {"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\"> [base 58 address]..."}, {"reject_requests", &reject_requests, "<\"create\"|\"import\"> [base 58 address]..."}, diff --git a/src/db/data.h b/src/db/data.h index 93d4ded..59c7d3b 100644 --- a/src/db/data.h +++ b/src/db/data.h @@ -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 }; diff --git a/src/db/storage.cpp b/src/db/storage.cpp index 7e90151..97a13ef 100644 --- a/src/db/storage.cpp +++ b/src/db/storage.cpp @@ -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 storage::add_account(account_address const& address, crypto::secret_key const& key) noexcept + expect 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 + return db->try_write([this, &address, &key, flags] (MDB_txn& txn) -> expect { const expect 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 diff --git a/src/db/storage.h b/src/db/storage.h index 522bab0..85147f8 100644 --- a/src/db/storage.h +++ b/src/db/storage.h @@ -194,7 +194,7 @@ namespace db //! Add an account, for immediate inclusion in the active list. - expect add_account(account_address const& address, crypto::secret_key const& key) noexcept; + expect 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> diff --git a/src/rest_server.cpp b/src/rest_server.cpp index ea197f2..9f75b3c 100644 --- a/src/rest_server.cpp +++ b/src/rest_server.cpp @@ -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(*resp); } + template + struct admin + { + T params; + crypto::secret_key auth; + }; + + template + void read_bytes(wire::json_reader& source, admin& self) + { + wire::object( + source, wire::field("auth", std::ref(unwrap(unwrap(self.auth)))), WIRE_FIELD(params) + ); + } + void read_bytes(wire::json_reader& source, admin>& self) + { + // params optional + wire::object(source, wire::field("auth", std::ref(unwrap(unwrap(self.auth))))); + } + + template + expect call_admin(std::string&& root, db::storage disk, const rpc::client&) + { + using request = typename E::request; + const expect> req = wire::json::from_bytes>(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, 50 * 1024} }; + constexpr const endpoint admin_endpoints[] = + { + {"/accept_requests", call_admin, 50 * 1024}, + {"/add_account", call_admin, 50 * 1024}, + {"/list_accounts", call_admin, 100}, + {"/list_requests", call_admin, 100}, + {"/modify_account_status", call_admin, 50 * 1024}, + {"/reject_requests", call_admin, 50 * 1024}, + {"/rescan", call_admin, 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 prefix; + boost::optional admin_prefix; explicit internal(boost::asio::io_service& io_service, lws::db::storage disk, rpc::client client) : lws::http_server_impl_base(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; + 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 addresses, db::storage disk, rpc::client client, configuration config) + rest_server::rest_server(epee::span addresses, std::vector 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::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"); diff --git a/src/rest_server.h b/src/rest_server.h index 25532c2..26633b5 100644 --- a/src/rest_server.h +++ b/src/rest_server.h @@ -56,7 +56,7 @@ namespace lws bool allow_external; }; - explicit rest_server(epee::span addresses, db::storage disk, rpc::client client, configuration config); + explicit rest_server(epee::span addresses, std::vector admin, db::storage disk, rpc::client client, configuration config); rest_server(rest_server&&) = delete; rest_server(rest_server const&) = delete; diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index 934f4be..ebf9fac 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -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) diff --git a/src/rpc/admin.cpp b/src/rpc/admin.cpp new file mode 100644 index 0000000..aa1635a --- /dev/null +++ b/src/rpc/admin.cpp @@ -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 +#include +#include +#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 + struct truncated + { + T value; + }; + + lws::db::account_address wire_unwrap(const boost::string_ref source) + { + const expect 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; + void read_bytes(wire::reader& source, base58_address& dest) + { + dest.value = wire_unwrap(source.string()); + } + + void write_bytes(wire::writer& dest, const truncated& 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& self) + { + wire::object(dest, + wire::field("address", lws::db::address_string(self.value.address)), + wire::field("start_height", self.value.start_height) + ); + } + + template + void write_bytes(wire::json_writer& dest, const truncated>> self) + { + const auto truncate = [] (V src) { return truncated{std::move(src)}; }; + wire::array(dest, std::move(self.value), truncate); + } + + template + expect stream_object(wire::json_writer& dest, expect> self) + { + using value_range = boost::iterator_range>; + const auto truncate = [] (value_range src) -> truncated + { + return {std::move(src)}; + }; + + if (!self) + return self.error(); + + wire::dynamic_object(dest, self->make_range(), wire::enum_as_string, truncate); + return success(); + } + + template + void read_addresses(wire::reader& source, T& self, U field) + { + std::vector 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 self) + { + // writes an array of monero base58 address strings + wire::object(dest, wire::field("updated", wire::as_array(self, lws::db::address_string))); + } + + expect write_addresses(wire::writer& dest, const expect>& 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 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 add_account_::operator()(wire::writer& out, db::storage disk, const request& req) const + { + using span = epee::span; + MONERO_CHECK(disk.add_account(req.address, req.key)); + write_addresses(out, span{std::addressof(req.address), 1}); + return success(); + } + + expect 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 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 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 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 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 diff --git a/src/rpc/admin.h b/src/rpc/admin.h new file mode 100644 index 0000000..2c48745 --- /dev/null +++ b/src/rpc/admin.h @@ -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 +#include +#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 addresses; + db::request type; + }; + void read_bytes(wire::reader&, address_requests&); + + struct modify_account_req + { + std::vector addresses; + db::account_status status; + }; + void read_bytes(wire::reader&, modify_account_req&); + + struct rescan_req + { + std::vector addresses; + db::block_id height; + }; + void read_bytes(wire::reader&, rescan_req&); + + + struct accept_requests_ + { + using request = address_requests; + expect 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 operator()(wire::writer& dest, db::storage disk, const request& req) const; + }; + constexpr const add_account_ add_account{}; + + struct list_accounts_ + { + using request = expect; + expect operator()(wire::json_writer& dest, db::storage disk) const; + expect 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; + expect operator()(wire::json_writer& dest, db::storage disk) const; + expect 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 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 operator()(wire::writer& dest, db::storage disk, const request& req) const; + }; + constexpr const reject_requests_ reject_requests{}; + + struct rescan_ + { + using request = rescan_req; + expect operator()(wire::writer& dest, db::storage disk, const request& req) const; + }; + constexpr const rescan_ rescan{}; + +}} // lws // rpc diff --git a/src/server_main.cpp b/src/server_main.cpp index 546c0e4..f6dbe26 100644 --- a/src/server_main.cpp +++ b/src/server_main.cpp @@ -57,6 +57,7 @@ namespace const command_line::arg_descriptor daemon_rpc; const command_line::arg_descriptor daemon_sub; const command_line::arg_descriptor> rest_servers; + const command_line::arg_descriptor> admin_rest_servers; const command_line::arg_descriptor rest_ssl_key; const command_line::arg_descriptor rest_ssl_cert; const command_line::arg_descriptor rest_threads; @@ -87,7 +88,8 @@ namespace : lws::options() , daemon_rpc{"daemon", "://
: 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)://
:] for incoming connections, multiple declarations allowed"} + , rest_servers{"rest-server", "[(https|http)://
:][/] for incoming connections, multiple declarations allowed"} + , admin_rest_servers{"admin-rest-server", "[(https|http])://
:][/] for incoming admin connections, multiple declarations allowed"} , rest_ssl_key{"rest-ssl-key", " to PEM formatted SSL key for https REST server", ""} , rest_ssl_cert{"rest-ssl-certificate", " 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>()->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 rest_servers; + std::vector 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);