monero-lws/src/rest_server.cpp
2024-06-26 14:14:57 -04:00

1250 lines
46 KiB
C++

// Copyright (c) 2018-2020, 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 "rest_server.h"
#include <algorithm>
#include <boost/range/counting_range.hpp>
#include <boost/thread/tss.hpp>
#include <boost/utility/string_ref.hpp>
#include <cstring>
#include <limits>
#include <string>
#include <utility>
#include "common/error.h" // monero/src
#include "common/expect.h" // monero/src
#include "crypto/crypto.h" // monero/src
#include "cryptonote_config.h" // monero/src
#include "db/data.h"
#include "db/storage.h"
#include "db/string.h"
#include "error.h"
#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 "net/net_ssl.h" // monero/contrib/epee/include
#include "net/zmq.h" // monero/src
#include "rpc/admin.h"
#include "rpc/client.h"
#include "rpc/daemon_messages.h" // monero/src
#include "rpc/light_wallet.h"
#include "rpc/rates.h"
#include "rpc/webhook.h"
#include "util/http_server.h"
#include "util/gamma_picker.h"
#include "util/random_outputs.h"
#include "util/source_location.h"
#include "wire/adapted/crypto.h"
#include "wire/json.h"
namespace lws
{
namespace
{
namespace http = epee::net_utils::http;
constexpr const std::chrono::seconds reconnect_backoff{10};
expect<rpc::client*> thread_client(const rpc::client& gclient, const bool reset = false)
{
struct tclient
{
rpc::client client;
std::chrono::steady_clock::time_point last_connect;
explicit tclient() noexcept
: client(), last_connect(std::chrono::seconds{0})
{}
};
static boost::thread_specific_ptr<tclient> global;
tclient* thread_ptr = global.get();
if (!thread_ptr)
{
thread_ptr = new tclient;
global.reset(thread_ptr);
}
if (reset || !thread_ptr->client)
{
// This reduces ZMQ internal errors with lack of file descriptors
const auto now = std::chrono::steady_clock::now();
if (now - thread_ptr->last_connect < reconnect_backoff)
return {error::daemon_timeout};
// careful, gclient and thread_ptr->client could be aliased
expect<rpc::client> new_client = gclient.clone();
thread_ptr->client = rpc::client{};
thread_ptr->last_connect = now;
if (!new_client)
return new_client.error();
thread_ptr->client = std::move(*new_client);
}
return {std::addressof(thread_ptr->client)};
}
expect<void> send_with_retry(rpc::client& tclient, epee::byte_slice message, const std::chrono::seconds timeout)
{
expect<void> resp{common_error::kInvalidArgument};
for (unsigned i = 0; i < 2; ++i)
{
resp = tclient.send(message.clone(), timeout);
if (resp || resp != net::zmq::make_error_code(EFSM))
break;
// fix state machine by reading+discarding previously timed out response
auto read = tclient.get_message(timeout);
if (!read)
{
// message could've been delivered, then dropped in process failure
thread_client(tclient, true);
return read.error();
}
}
return resp;
}
struct context : epee::net_utils::connection_context_base
{
context()
: epee::net_utils::connection_context_base()
{}
};
bool is_locked(std::uint64_t unlock_time, db::block_id last) noexcept
{
if (unlock_time > CRYPTONOTE_MAX_BLOCK_NUMBER)
return std::chrono::seconds{unlock_time} > std::chrono::system_clock::now().time_since_epoch();
return db::block_id(unlock_time) > last;
}
std::vector<db::output::spend_meta_>::const_iterator
find_metadata(std::vector<db::output::spend_meta_> const& metas, db::output_id id)
{
struct by_output_id
{
bool operator()(db::output::spend_meta_ const& left, db::output_id right) const noexcept
{
return left.id < right;
}
bool operator()(db::output_id left, db::output::spend_meta_ const& right) const noexcept
{
return left < right.id;
}
};
return std::lower_bound(metas.begin(), metas.end(), id, by_output_id{});
}
bool is_hidden(db::account_status status) noexcept
{
switch (status)
{
case db::account_status::active:
case db::account_status::inactive:
return false;
default:
case db::account_status::hidden:
break;
}
return true;
}
bool key_check(const rpc::account_credentials& creds)
{
crypto::public_key verify{};
if (!crypto::secret_key_to_public_key(creds.key, verify))
return false;
if (verify != creds.address.view_public)
return false;
return true;
}
//! \return Account info from the DB, iff key matches address AND address is NOT hidden.
expect<std::pair<db::account, db::storage_reader>> open_account(const rpc::account_credentials& creds, db::storage disk)
{
if (!key_check(creds))
return {lws::error::bad_view_key};
auto reader = disk.start_read();
if (!reader)
return reader.error();
const auto user = reader->get_account(creds.address);
if (!user)
return user.error();
if (is_hidden(user->first))
return {lws::error::account_not_found};
return {std::make_pair(user->second, std::move(*reader))};
}
std::atomic_flag rates_error_once = ATOMIC_FLAG_INIT;
struct runtime_options
{
std::uint32_t max_subaddresses;
epee::net_utils::ssl_verification_t webhook_verify;
bool disable_admin_auth;
bool auto_accept_creation;
};
struct get_address_info
{
using request = rpc::account_credentials;
using response = rpc::get_address_info_response;
static expect<response> handle(const request& req, db::storage disk, rpc::client const& client, runtime_options const&)
{
auto user = open_account(req, std::move(disk));
if (!user)
return user.error();
response resp{};
auto outputs = user->second.get_outputs(user->first.id);
if (!outputs)
return outputs.error();
auto spends = user->second.get_spends(user->first.id);
if (!spends)
return spends.error();
const expect<db::block_info> last = user->second.get_last_block();
if (!last)
return last.error();
resp.blockchain_height = std::uint64_t(last->id);
resp.transaction_height = resp.blockchain_height;
resp.scanned_height = std::uint64_t(user->first.scan_height);
resp.scanned_block_height = resp.scanned_height;
resp.start_height = std::uint64_t(user->first.start_height);
std::vector<db::output::spend_meta_> metas{};
metas.reserve(outputs->count());
for (auto output = outputs->make_iterator(); !output.is_end(); ++output)
{
const db::output::spend_meta_ meta =
output.get_value<MONERO_FIELD(db::output, spend_meta)>();
// these outputs will usually be in correct order post ringct
if (metas.empty() || metas.back().id < meta.id)
metas.push_back(meta);
else
metas.insert(find_metadata(metas, meta.id), meta);
resp.total_received = rpc::safe_uint64(std::uint64_t(resp.total_received) + meta.amount);
if (is_locked(output.get_value<MONERO_FIELD(db::output, unlock_time)>(), last->id))
resp.locked_funds = rpc::safe_uint64(std::uint64_t(resp.locked_funds) + meta.amount);
}
resp.spent_outputs.reserve(spends->count());
for (auto const& spend : spends->make_range())
{
const auto meta = find_metadata(metas, spend.source);
if (meta == metas.end() || meta->id != spend.source)
{
throw std::logic_error{
"Serious database error, no receive for spend"
};
}
resp.spent_outputs.push_back({*meta, spend});
resp.total_sent = rpc::safe_uint64(std::uint64_t(resp.total_sent) + meta->amount);
}
resp.rates = client.get_rates();
if (!resp.rates && !rates_error_once.test_and_set(std::memory_order_relaxed))
MWARNING("Unable to retrieve exchange rates: " << resp.rates.error().message());
return resp;
}
};
struct get_address_txs
{
using request = rpc::account_credentials;
using response = rpc::get_address_txs_response;
static expect<response> handle(const request& req, db::storage disk, rpc::client const&, runtime_options const&)
{
auto user = open_account(req, std::move(disk));
if (!user)
return user.error();
auto outputs = user->second.get_outputs(user->first.id);
if (!outputs)
return outputs.error();
auto spends = user->second.get_spends(user->first.id);
if (!spends)
return spends.error();
const expect<db::block_info> last = user->second.get_last_block();
if (!last)
return last.error();
response resp{};
resp.scanned_height = std::uint64_t(user->first.scan_height);
resp.scanned_block_height = resp.scanned_height;
resp.start_height = std::uint64_t(user->first.start_height);
resp.blockchain_height = std::uint64_t(last->id);
resp.transaction_height = resp.blockchain_height;
// merge input and output info into a single set of txes.
auto output = outputs->make_iterator();
auto spend = spends->make_iterator();
std::vector<db::output::spend_meta_> metas{};
resp.transactions.reserve(outputs->count());
metas.reserve(resp.transactions.capacity());
db::transaction_link next_output{};
db::transaction_link next_spend{};
if (!output.is_end())
next_output = output.get_value<MONERO_FIELD(db::output, link)>();
if (!spend.is_end())
next_spend = spend.get_value<MONERO_FIELD(db::spend, link)>();
while (!output.is_end() || !spend.is_end())
{
if (!resp.transactions.empty())
{
db::transaction_link const& last = resp.transactions.back().info.link;
if ((!output.is_end() && next_output < last) || (!spend.is_end() && next_spend < last))
{
throw std::logic_error{"DB has unexpected sort order"};
}
}
if (spend.is_end() || (!output.is_end() && next_output <= next_spend))
{
std::uint64_t amount = 0;
if (resp.transactions.empty() || resp.transactions.back().info.link.tx_hash != next_output.tx_hash)
{
resp.transactions.push_back({*output});
amount = resp.transactions.back().info.spend_meta.amount;
}
else
{
amount = output.get_value<MONERO_FIELD(db::output, spend_meta.amount)>();
resp.transactions.back().info.spend_meta.amount += amount;
}
const db::output::spend_meta_ meta = output.get_value<MONERO_FIELD(db::output, spend_meta)>();
if (metas.empty() || metas.back().id < meta.id)
metas.push_back(meta);
else
metas.insert(find_metadata(metas, meta.id), meta);
resp.total_received = rpc::safe_uint64(std::uint64_t(resp.total_received) + amount);
++output;
if (!output.is_end())
next_output = output.get_value<MONERO_FIELD(db::output, link)>();
}
else if (output.is_end() || (next_spend < next_output))
{
const db::output_id source_id = spend.get_value<MONERO_FIELD(db::spend, source)>();
const auto meta = find_metadata(metas, source_id);
if (meta == metas.end() || meta->id != source_id)
{
throw std::logic_error{
"Serious database error, no receive for spend"
};
}
if (resp.transactions.empty() || resp.transactions.back().info.link.tx_hash != next_spend.tx_hash)
{
resp.transactions.push_back({});
resp.transactions.back().spends.push_back({*meta, *spend});
resp.transactions.back().info.link.height = resp.transactions.back().spends.back().possible_spend.link.height;
resp.transactions.back().info.link.tx_hash = resp.transactions.back().spends.back().possible_spend.link.tx_hash;
resp.transactions.back().info.spend_meta.mixin_count =
resp.transactions.back().spends.back().possible_spend.mixin_count;
resp.transactions.back().info.timestamp = resp.transactions.back().spends.back().possible_spend.timestamp;
resp.transactions.back().info.unlock_time = resp.transactions.back().spends.back().possible_spend.unlock_time;
}
else
resp.transactions.back().spends.push_back({*meta, *spend});
resp.transactions.back().spent += meta->amount;
++spend;
if (!spend.is_end())
next_spend = spend.get_value<MONERO_FIELD(db::spend, link)>();
}
}
return resp;
}
};
struct get_random_outs
{
using request = rpc::get_random_outs_request;
using response = rpc::get_random_outs_response;
static expect<response> handle(request req, const db::storage&, rpc::client const& gclient, runtime_options const&)
{
using distribution_rpc = cryptonote::rpc::GetOutputDistribution;
using histogram_rpc = cryptonote::rpc::GetOutputHistogram;
using distribution_rpc = cryptonote::rpc::GetOutputDistribution;
std::vector<std::uint64_t> amounts = std::move(req.amounts.values);
if (50 < req.count || 20 < amounts.size())
return {lws::error::exceeded_rest_request_limit};
const expect<rpc::client*> tclient = thread_client(gclient);
if (!tclient)
return tclient.error();
if (*tclient == nullptr)
throw std::logic_error{"Unexpected rpc::client nullptr"};
const std::greater<std::uint64_t> rsort{};
std::sort(amounts.begin(), amounts.end(), rsort);
const std::size_t ringct_count =
amounts.end() - std::lower_bound(amounts.begin(), amounts.end(), 0, rsort);
std::vector<lws::histogram> histograms{};
if (ringct_count < amounts.size())
{
// reuse allocated vector memory
amounts.resize(amounts.size() - ringct_count);
histogram_rpc::Request histogram_req{};
histogram_req.amounts = std::move(amounts);
histogram_req.min_count = 0;
histogram_req.max_count = 0;
histogram_req.unlocked = true;
histogram_req.recent_cutoff = 0;
epee::byte_slice msg = rpc::client::make_message("get_output_histogram", histogram_req);
MONERO_CHECK(send_with_retry(**tclient, std::move(msg), std::chrono::seconds{10}));
auto histogram_resp = (*tclient)->receive<histogram_rpc::Response>(std::chrono::minutes{3}, MLWS_CURRENT_LOCATION);
if (!histogram_resp)
return histogram_resp.error();
if (histogram_resp->histogram.size() != histogram_req.amounts.size())
return {lws::error::bad_daemon_response};
histograms = std::move(histogram_resp->histogram);
amounts = std::move(histogram_req.amounts);
amounts.insert(amounts.end(), ringct_count, 0);
}
std::vector<std::uint64_t> distributions{};
if (ringct_count)
{
distribution_rpc::Request distribution_req{};
if (ringct_count == amounts.size())
distribution_req.amounts = std::move(amounts);
distribution_req.amounts.resize(1);
distribution_req.from_height = 0;
distribution_req.to_height = 0;
distribution_req.cumulative = true;
epee::byte_slice msg =
rpc::client::make_message("get_output_distribution", distribution_req);
MONERO_CHECK(send_with_retry(**tclient, std::move(msg), std::chrono::seconds{10}));
auto distribution_resp =
(*tclient)->receive<distribution_rpc::Response>(std::chrono::minutes{3}, MLWS_CURRENT_LOCATION);
if (!distribution_resp)
return distribution_resp.error();
if (distribution_resp->distributions.size() != 1)
return {lws::error::bad_daemon_response};
if (distribution_resp->distributions[0].amount != 0)
return {lws::error::bad_daemon_response};
distributions = std::move(distribution_resp->distributions[0].data.distribution);
if (amounts.empty())
{
amounts = std::move(distribution_req.amounts);
amounts.insert(amounts.end(), ringct_count - 1, 0);
}
}
class zmq_fetch_keys
{
/* `std::function` needs a copyable functor. The functor was made
const and copied in the function instead of using a reference to
make the callback in `std::function` thread-safe. This shouldn't
be a problem now, but this is just-in-case of a future refactor. */
rpc::client* tclient;
public:
zmq_fetch_keys(rpc::client* src) noexcept
: tclient(src)
{}
zmq_fetch_keys(zmq_fetch_keys&&) = default;
zmq_fetch_keys(const zmq_fetch_keys&) = default;
expect<std::vector<output_keys>> operator()(std::vector<lws::output_ref> ids) const
{
using get_keys_rpc = cryptonote::rpc::GetOutputKeys;
if (tclient == nullptr)
throw std::logic_error{"Unexpected nullptr in zmq_fetch_keys"};
get_keys_rpc::Request keys_req{};
keys_req.outputs = std::move(ids);
epee::byte_slice msg = rpc::client::make_message("get_output_keys", keys_req);
MONERO_CHECK(send_with_retry(*tclient, std::move(msg), std::chrono::seconds{10}));
auto keys_resp = tclient->receive<get_keys_rpc::Response>(std::chrono::seconds{10}, MLWS_CURRENT_LOCATION);
if (!keys_resp)
return keys_resp.error();
return {std::move(keys_resp->keys)};
}
};
lws::gamma_picker pick_rct{std::move(distributions)};
auto rings = pick_random_outputs(
req.count,
epee::to_span(amounts),
pick_rct,
epee::to_mut_span(histograms),
zmq_fetch_keys{*tclient}
);
if (!rings)
return rings.error();
return response{std::move(*rings)};
}
};
struct get_subaddrs
{
using request = rpc::account_credentials;
using response = rpc::get_subaddrs_response;
static expect<response> handle(request const& req, db::storage disk, rpc::client const&, runtime_options const& options)
{
auto user = open_account(req, std::move(disk));
if (!user)
return user.error();
auto subaddrs = user->second.get_subaddresses(user->first.id);
if (!subaddrs)
return subaddrs.error();
return response{std::move(*subaddrs)};
}
};
struct get_unspent_outs
{
using request = rpc::get_unspent_outs_request;
using response = rpc::get_unspent_outs_response;
static expect<response> handle(request req, db::storage disk, rpc::client const& gclient, runtime_options const&)
{
using rpc_command = cryptonote::rpc::GetFeeEstimate;
auto user = open_account(req.creds, std::move(disk));
if (!user)
return user.error();
const expect<rpc::client*> tclient = thread_client(gclient);
if (!tclient)
return tclient.error();
if (*tclient == nullptr)
throw std::logic_error{"Unexpected rpc::client nullptr"};
{
rpc_command::Request req{};
req.num_grace_blocks = 10;
epee::byte_slice msg = rpc::client::make_message("get_dynamic_fee_estimate", req);
MONERO_CHECK(send_with_retry(**tclient, std::move(msg), std::chrono::seconds{10}));
}
if ((req.use_dust && *req.use_dust) || !req.dust_threshold)
req.dust_threshold = rpc::safe_uint64(0);
if (!req.mixin)
req.mixin = 0;
auto outputs = user->second.get_outputs(user->first.id);
if (!outputs)
return outputs.error();
std::uint64_t received = 0;
std::vector<std::pair<db::output, std::vector<crypto::key_image>>> unspent;
unspent.reserve(outputs->count());
for (db::output const& out : outputs->make_range())
{
if (out.spend_meta.amount < std::uint64_t(*req.dust_threshold) || out.spend_meta.mixin_count < *req.mixin)
continue;
received += out.spend_meta.amount;
unspent.push_back({out, {}});
auto images = user->second.get_images(out.spend_meta.id);
if (!images)
return images.error();
unspent.back().second.reserve(images->count());
auto range = images->make_range<MONERO_FIELD(db::key_image, value)>();
std::copy(range.begin(), range.end(), std::back_inserter(unspent.back().second));
}
if (received < std::uint64_t(req.amount))
return {lws::error::account_not_found};
const auto resp = (*tclient)->receive<rpc_command::Response>(std::chrono::seconds{20}, MLWS_CURRENT_LOCATION);
if (!resp)
return resp.error();
if (resp->size_scale == 0 || 1024 < resp->size_scale || resp->fee_mask == 0)
return {lws::error::bad_daemon_response};
const std::uint64_t per_byte_fee =
resp->estimated_base_fee / resp->size_scale;
return response{per_byte_fee, resp->fee_mask, rpc::safe_uint64(received), std::move(unspent), std::move(req.creds.key)};
}
};
struct import_request
{
using request = rpc::account_credentials;
using response = rpc::import_response;
static expect<response> handle(request req, db::storage disk, rpc::client const&, runtime_options const&)
{
bool new_request = false;
bool fulfilled = false;
{
auto user = open_account(req, disk.clone());
if (!user)
return user.error();
if (user->first.start_height == db::block_id(0))
fulfilled = true;
else
{
const expect<db::request_info> info =
user->second.get_request(db::request::import_scan, req.address);
if (!info)
{
if (info != lmdb::error(MDB_NOTFOUND))
return info.error();
new_request = true;
}
}
} // close reader
if (new_request)
MONERO_CHECK(disk.import_request(req.address, db::block_id(0)));
const char* status = new_request ?
"Accepted, waiting for approval" : (fulfilled ? "Approved" : "Waiting for Approval");
return response{rpc::safe_uint64(0), status, new_request, fulfilled};
}
};
struct login
{
using request = rpc::login_request;
using response = rpc::login_response;
static expect<response> handle(request req, db::storage disk, rpc::client const& gclient, runtime_options const& options)
{
if (!key_check(req.creds))
return {lws::error::bad_view_key};
{
auto reader = disk.start_read();
if (!reader)
return reader.error();
const auto account = reader->get_account(req.creds.address);
reader->finish_read();
if (account)
{
if (is_hidden(account->first))
return {lws::error::account_not_found};
// Do not count a request for account creation as login
return response{false, bool(account->second.flags & db::account_generated_locally)};
}
else if (!req.create_account || account != lws::error::account_not_found)
return account.error();
}
const auto flags = req.generated_locally ? db::account_generated_locally : db::default_account;
const auto hooks = disk.creation_request(req.creds.address, req.creds.key, flags);
if (!hooks)
return hooks.error();
if (options.auto_accept_creation)
{
const auto accepted = disk.accept_requests(db::request::create, {std::addressof(req.creds.address), 1});
if (!accepted)
MERROR("Failed to move account " << db::address_string(req.creds.address) << " to available state: " << accepted.error());
}
if (!hooks->empty())
{
const expect<rpc::client*> tclient = thread_client(gclient);
if (!tclient)
return tclient.error();
if (*tclient == nullptr)
throw std::logic_error{"Unexpected rpc::client nullptr"};
rpc::send_webhook(
**tclient, epee::to_span(*hooks), "json-full-new_account_hook:", "msgpack-full-new_account_hook:", std::chrono::seconds{5}, options.webhook_verify
);
}
return response{true, req.generated_locally};
}
};
struct provision_subaddrs
{
using request = rpc::provision_subaddrs_request;
using response = rpc::new_subaddrs_response;
static expect<response> handle(request req, db::storage disk, rpc::client const&, runtime_options const& options)
{
if (!req.maj_i && !req.min_i && !req.n_min && !req.n_maj)
return {lws::error::invalid_range};
db::account_id id = db::account_id::invalid;
{
auto user = open_account(req.creds, disk.clone());
if (!user)
return user.error();
id = user->first.id;
}
const std::uint32_t major_i = req.maj_i.value_or(0);
const std::uint32_t minor_i = req.min_i.value_or(0);
const std::uint32_t n_major = req.n_maj.value_or(50);
const std::uint32_t n_minor = req.n_min.value_or(500);
const bool get_all = req.get_all.value_or(true);
std::vector<db::subaddress_dict> new_ranges;
std::vector<db::subaddress_dict> all_ranges;
if (n_major && n_minor)
{
if (std::numeric_limits<std::uint32_t>::max() / n_major < n_minor)
return {lws::error::max_subaddresses};
if (options.max_subaddresses < n_major * n_minor)
return {lws::error::max_subaddresses};
std::vector<db::subaddress_dict> ranges;
ranges.reserve(n_major);
for (std::uint64_t elem : boost::counting_range(std::uint64_t(major_i), std::uint64_t(major_i) + n_major))
{
ranges.emplace_back(
db::major_index(elem), db::index_ranges{{db::index_range{db::minor_index(minor_i), db::minor_index(minor_i + n_minor - 1)}}}
);
}
auto upserted = disk.upsert_subaddresses(id, req.creds.address, req.creds.key, ranges, options.max_subaddresses);
if (!upserted)
return upserted.error();
new_ranges = std::move(*upserted);
}
if (get_all)
{
// must start a new read after the last write
auto reader = disk.start_read();
if (!reader)
return reader.error();
auto rc = reader->get_subaddresses(id);
if (!rc)
return rc.error();
all_ranges = std::move(*rc);
}
return response{std::move(new_ranges), std::move(all_ranges)};
}
};
struct submit_raw_tx
{
using request = rpc::submit_raw_tx_request;
using response = rpc::submit_raw_tx_response;
static expect<response> handle(request req, const db::storage& disk, const rpc::client& gclient, const runtime_options&)
{
using transaction_rpc = cryptonote::rpc::SendRawTxHex;
const expect<rpc::client*> tclient = thread_client(gclient);
if (!tclient)
return tclient.error();
if (*tclient == nullptr)
throw std::logic_error{"Unexpected rpc::client nullptr"};
transaction_rpc::Request daemon_req{};
daemon_req.relay = true;
daemon_req.tx_as_hex = std::move(req.tx);
epee::byte_slice message = rpc::client::make_message("send_raw_tx_hex", daemon_req);
MONERO_CHECK(send_with_retry(**tclient, std::move(message), std::chrono::seconds{10}));
const auto daemon_resp = (*tclient)->receive<transaction_rpc::Response>(std::chrono::seconds{20}, MLWS_CURRENT_LOCATION);
if (!daemon_resp)
return daemon_resp.error();
if (!daemon_resp->relayed)
return {lws::error::tx_relay_failed};
return response{"OK"};
}
};
struct upsert_subaddrs
{
using request = rpc::upsert_subaddrs_request;
using response = rpc::new_subaddrs_response;
static expect<response> handle(request req, db::storage disk, rpc::client const&, runtime_options const& options)
{
if (!options.max_subaddresses)
return {lws::error::max_subaddresses};
db::account_id id = db::account_id::invalid;
{
auto user = open_account(req.creds, disk.clone());
if (!user)
return user.error();
id = user->first.id;
}
const bool get_all = req.get_all.value_or(true);
std::vector<db::subaddress_dict> all_ranges;
auto new_ranges =
disk.upsert_subaddresses(id, req.creds.address, req.creds.key, req.subaddrs, options.max_subaddresses);
if (!new_ranges)
return new_ranges.error();
if (get_all)
{
auto reader = disk.start_read();
if (!reader)
return reader.error();
auto rc = reader->get_subaddresses(id);
if (!rc)
return rc.error();
all_ranges = std::move(*rc);
}
return response{std::move(*new_ranges), std::move(all_ranges)};
}
};
template<typename E>
expect<epee::byte_slice> call(std::string&& root, db::storage disk, const rpc::client& gclient, const runtime_options& options)
{
using request = typename E::request;
using response = typename E::response;
request req{};
std::error_code error = wire::json::from_bytes(std::move(root), req);
if (error)
return error;
expect<response> resp = E::handle(std::move(req), std::move(disk), gclient, options);
if (!resp)
return resp.error();
epee::byte_slice out{};
if ((error = wire::json::to_bytes(out, *resp)))
return error;
return {std::move(out)};
}
template<typename T>
struct admin
{
T params;
boost::optional<crypto::secret_key> auth;
};
template<typename T>
void read_bytes(wire::json_reader& source, admin<T>& self)
{
wire::object(source, WIRE_OPTIONAL_FIELD(auth), WIRE_FIELD(params));
}
void read_bytes(wire::json_reader& source, admin<expect<void>>& self)
{
// params optional
wire::object(source, WIRE_OPTIONAL_FIELD(auth));
}
template<typename E>
expect<epee::byte_slice> call_admin(std::string&& root, db::storage disk, const rpc::client&, const runtime_options& options)
{
using request = typename E::request;
admin<request> req{};
{
const std::error_code error = wire::json::from_bytes(std::move(root), req);
if (error)
return error;
}
if (!options.disable_admin_auth)
{
if (!req.auth)
return {error::account_not_found};
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), std::move(req.params)));
return epee::byte_slice{dest.take_sink()};
}
struct endpoint
{
char const* const name;
expect<epee::byte_slice> (*const run)(std::string&&, db::storage, rpc::client const&, const runtime_options&);
const unsigned max_size;
};
constexpr const endpoint endpoints[] =
{
{"/get_address_info", call<get_address_info>, 2 * 1024},
{"/get_address_txs", call<get_address_txs>, 2 * 1024},
{"/get_random_outs", call<get_random_outs>, 2 * 1024},
{"/get_subaddrs", call<get_subaddrs>, 2 * 1024},
{"/get_txt_records", nullptr, 0 },
{"/get_unspent_outs", call<get_unspent_outs>, 2 * 1024},
{"/import_wallet_request", call<import_request>, 2 * 1024},
{"/login", call<login>, 2 * 1024},
{"/provision_subaddrs", call<provision_subaddrs>, 2 * 1024},
{"/submit_raw_tx", call<submit_raw_tx>, 50 * 1024},
{"/upsert_subaddrs", call<upsert_subaddrs>, 10 * 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},
{"/validate", call_admin<rpc::validate_>, 50 * 1024},
{"/webhook_add", call_admin<rpc::webhook_add_>, 50 * 1024},
{"/webhook_delete", call_admin<rpc::webhook_delete_>, 50 * 1024},
{"/webhook_delete_uuid", call_admin<rpc::webhook_del_uuid_>,50 * 1024},
{"/webhook_list", call_admin<rpc::webhook_list_>, 100}
};
struct by_name_
{
bool operator()(endpoint const& left, endpoint const& right) const noexcept
{
if (left.name && right.name)
return std::strcmp(left.name, right.name) < 0;
return false;
}
bool operator()(const boost::string_ref left, endpoint const& right) const noexcept
{
if (right.name)
return left < right.name;
return false;
}
bool operator()(endpoint const& left, const boost::string_ref right) const noexcept
{
if (left.name)
return left.name < right;
return false;
}
};
constexpr const by_name_ by_name{};
} // anonymous
struct rest_server::internal final : public lws::http_server_impl_base<rest_server::internal, context>
{
db::storage disk;
rpc::client client;
boost::optional<std::string> prefix;
boost::optional<std::string> admin_prefix;
runtime_options options;
explicit internal(boost::asio::io_service& io_service, lws::db::storage disk, rpc::client client, runtime_options options)
: lws::http_server_impl_base<rest_server::internal, context>(io_service)
, disk(std::move(disk))
, client(std::move(client))
, prefix()
, admin_prefix()
, options(std::move(options))
{
assert(std::is_sorted(std::begin(endpoints), std::end(endpoints), by_name));
}
const endpoint* get_endpoint(boost::string_ref uri) const
{
using span = epee::span<const endpoint>;
span handlers = nullptr;
if (admin_prefix && uri.starts_with(*admin_prefix))
{
uri.remove_prefix(admin_prefix->size());
handlers = span{admin_endpoints};
}
else if (prefix && uri.starts_with(*prefix))
{
uri.remove_prefix(prefix->size());
handlers = span{endpoints};
}
else
return nullptr;
const auto handler = std::lower_bound(
std::begin(handlers), std::end(handlers), uri, by_name
);
if (handler == std::end(handlers) || handler->name != uri)
return nullptr;
return handler;
}
virtual bool
handle_http_request(const http::http_request_info& query, http::http_response_info& response, context& ctx)
override final
{
endpoint const* const handler = get_endpoint(query.m_URI);
if (!handler)
{
response.m_response_code = 404;
response.m_response_comment = "Not Found";
return true;
}
if (handler->run == nullptr)
{
response.m_response_code = 501;
response.m_response_comment = "Not Implemented";
return true;
}
if (handler->max_size < query.m_body.size())
{
MINFO("Client exceeded maximum body size (" << handler->max_size << " bytes)");
response.m_response_code = 400;
response.m_response_comment = "Bad Request";
return true;
}
if (query.m_http_method != http::http_method_post)
{
response.m_response_code = 405;
response.m_response_comment = "Method Not Allowed";
return true;
}
// \TODO remove copy of json string here :/
auto body = handler->run(std::string{query.m_body}, disk.clone(), client, options);
if (!body)
{
MINFO(body.error().message() << " from " << ctx.m_remote_address.str() << " on " << handler->name);
if (body.error().category() == wire::error::rapidjson_category() || body == lws::error::invalid_range)
{
response.m_response_code = 400;
response.m_response_comment = "Bad Request";
}
else if (body == lws::error::account_not_found || body == lws::error::duplicate_request)
{
response.m_response_code = 403;
response.m_response_comment = "Forbidden";
}
else if (body == lws::error::max_subaddresses)
{
response.m_response_code = 409;
response.m_response_comment = "Conflict";
}
else if (body.matches(std::errc::timed_out) || body.matches(std::errc::no_lock_available))
{
response.m_response_code = 503;
response.m_response_comment = "Service Unavailable";
}
else
{
response.m_response_code = 500;
response.m_response_comment = "Internal Server Error";
}
return true;
}
response.m_response_code = 200;
response.m_response_comment = "OK";
response.m_mime_tipe = "application/json";
response.m_header_info.m_content_type = "application/json";
response.m_body.assign(reinterpret_cast<const char*>(body->data()), body->size()); // \TODO Remove copy here too!s
return true;
}
};
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_()
{
if (addresses.empty())
MONERO_THROW(common_error::kInvalidArgument, "REST server requires 1 or more addresses");
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");
const bool https = url.schema == "https";
if (!https && url.schema != "http")
MONERO_THROW(lws::error::configuration, "Unsupported scheme, only http or https supported");
if (std::numeric_limits<std::uint16_t>::max() < url.port)
MONERO_THROW(lws::error::configuration, "Specified port for REST server is out of range");
if (!url.uri.empty() && url.uri.front() != '/')
MONERO_THROW(lws::error::configuration, "First path prefix character must be '/'");
if (!https)
{
boost::system::error_code error{};
const boost::asio::ip::address ip_host =
ip_host.from_string(url.host, error);
if (error)
MONERO_THROW(lws::error::configuration, "Invalid IP address for REST server");
if (!ip_host.is_loopback() && !config.allow_external)
MONERO_THROW(lws::error::configuration, "Binding to external interface with http - consider using https or secure tunnel (ssh, etc). Use --confirm-external-bind to override");
}
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;
ssl_options.verification = epee::net_utils::ssl_verification_t::none; // clients verified with view key
ssl_options.auth = std::move(config.auth);
if (!port.init(std::to_string(url.port), std::move(url.host), std::move(config.access_controls), std::move(ssl_options)))
MONERO_THROW(lws::error::http_server, "REST server failed to initialize");
return https;
};
bool any_ssl = false;
const runtime_options options{config.max_subaddresses, config.webhook_verify, config.disable_admin_auth, config.auto_accept_creation};
for (const std::string& address : addresses)
{
ports_.emplace_back(io_service_, disk.clone(), MONERO_UNWRAP(client.clone()), options);
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()), options);
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;
if (!any_ssl && expect_ssl)
MONERO_THROW(lws::error::configuration, "Specified SSL key/cert without specifying https capable REST server");
if (!ports_.front().run(threads, false))
MONERO_THROW(lws::error::http_server, "REST server failed to run");
}
rest_server::~rest_server() noexcept
{}
} // lws