From 8cf09765572ced01daee054ab027d5dffd33dccf Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Wed, 26 Nov 2025 18:57:36 -0500 Subject: [PATCH] Add /get_version, based on openmonero with a few extra additions (#209) --- src/CMakeLists.txt | 29 +++++++++++++++++++++ src/error.cpp | 2 ++ src/error.h | 1 + src/lws_version.h.in | 47 ++++++++++++++++++++++++++++++++++ src/rest_server.cpp | 55 +++++++++++++++++++++++++++++++++++----- src/rpc/light_wallet.cpp | 32 +++++++++++++++++++++++ src/rpc/light_wallet.h | 29 +++++++++++++++++++++ 7 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 src/lws_version.h.in diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1ddc24c..92d5401 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -28,6 +28,35 @@ include_directories(.) +find_package(Git) + +set(MLWS_COMMIT_BRANCH "") +set(MLWS_COMMIT_DATE "") +set(MLWS_COMMIT_HASH "") +if (GIT_FOUND) + execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + OUTPUT_VARIABLE MLWS_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse --abbrev-ref HEAD + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + OUTPUT_VARIABLE MLWS_COMMIT_BRANCH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + execute_process( + COMMAND "${GIT_EXECUTABLE}" log -1 "--date=format:\"%Y/%m/%d %T\"" "--format=\"%ad\"" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + OUTPUT_VARIABLE MLWS_COMMIT_DATE + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif () + +configure_file(lws_version.h.in "${CMAKE_BINARY_DIR}/generated_include/lws_version.h") +include_directories("${CMAKE_BINARY_DIR}/generated_include") + add_subdirectory(lmdb) add_subdirectory(wire) add_subdirectory(db) diff --git a/src/error.cpp b/src/error.cpp index 39f8fd6..a4db6c9 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -61,6 +61,8 @@ namespace lws return "Invalid blockchain height"; case error::bad_url: return "Invlaid URL"; + case error::bad_verb: + return "Incorrect HTTP verb provided"; case error::bad_webhook: return "Invalid webhook request"; case error::blockchain_reorg: diff --git a/src/error.h b/src/error.h index fcc9cc1..ca67de3 100644 --- a/src/error.h +++ b/src/error.h @@ -44,6 +44,7 @@ namespace lws bad_daemon_response, //!< RPC Response from daemon was invalid bad_height, //!< Invalid blockchain height bad_url, //!< Invalid URL + bad_verb, //!< Bad HTTP verb for endpoint bad_webhook, //!< Invalid webhook request blockchain_reorg, //!< Blockchain reorg after fetching/scanning block(s) configuration, //!< Process configuration invalid diff --git a/src/lws_version.h.in b/src/lws_version.h.in new file mode 100644 index 0000000..e5f0146 --- /dev/null +++ b/src/lws_version.h.in @@ -0,0 +1,47 @@ +// Copyright (c) 2025, 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 + +namespace lws { namespace version +{ + constexpr const char branch[] = "@MLWS_COMMIT_BRANCH@"; + constexpr const char commit[] = "@MLWS_COMMIT_HASH@"; + constexpr const char date[] = "@MLWS_COMMIT_DATE@"; + constexpr const char id[] = "0.4-alpha"; + constexpr const char name[] = "monero-lws"; + + // openmonero is currently on 1.6 and we have multiple additions since then + namespace api + { + constexpr const std::uint16_t major = 1; + constexpr const std::uint16_t minor = 7; + constexpr const std::uint32_t combined = std::uint32_t(major) << 16 | minor; + } +}} // lws // version + diff --git a/src/rest_server.cpp b/src/rest_server.cpp index 859a9cc..f27f9b2 100644 --- a/src/rest_server.cpp +++ b/src/rest_server.cpp @@ -159,10 +159,11 @@ namespace lws struct connection_data { rest_server_data* const global; //!< Valid for lifetime of server + boost::beast::http::verb last_verb; bool passed_login; //!< True iff a login via viewkey was successful explicit connection_data(rest_server_data* global) noexcept - : global(global), passed_login(false) + : global(global), last_verb(boost::beast::http::verb::unknown), passed_login(false) {} //! \return Next request timeout, based on login status @@ -300,7 +301,7 @@ namespace lws using response = epee::byte_slice; // sometimes async using async_response = rpc::daemon_status_response; - static expect handle(const request&, const connection_data& data, std::function&& resume) + static expect handle(request, const connection_data& data, std::function&& resume) { using info_rpc = cryptonote::rpc::GetInfo; @@ -1303,6 +1304,33 @@ namespace lws } }; + struct get_version + { + using request = rpc::get_version_request; + using response = rpc::get_version_response; + + static expect handle(request, const connection_data& data, std::function&&) + { + lws::db::block_id height{}; + { + auto reader = data.global->disk.start_read(); + if (reader) + { + auto db_height = reader->get_last_block(); + if (db_height) + height = db_height->id; + else + MWARNING("Failed to get DB height: " << db_height.error().message()); + } + else + MWARNING("Failed to start db reader: " << reader.error().message()); + } + + // response constructor fills remaining fields + return response{height, data.global->options.max_subaddresses}; + } + }; + struct import_request { using request = rpc::import_request; @@ -1710,11 +1738,16 @@ namespace lws throw std::logic_error{"async REST handler not setup properly"}; if (std::is_same() && !resume) throw std::logic_error{"async REST handler not setup properly"}; - + request req{}; - std::error_code error = wire::json::from_bytes(std::move(root), req); - if (error) - return error; + if (!std::is_empty()) + { + if (data.last_verb != boost::beast::http::verb::post) + return {error::bad_verb}; + std::error_code error = wire::json::from_bytes(std::move(root), req); + if (error) + return error; + } expect resp = E::handle(std::move(req), data, std::move(resume)); if (!resp) @@ -1744,6 +1777,9 @@ namespace lws expect call_admin(std::string&& root, connection_data& data, std::function&&) { using request = typename E::request; + + if (data.last_verb != boost::beast::http::verb::post) + return {error::bad_verb}; admin req{}; { @@ -1804,6 +1840,7 @@ namespace lws {"/get_subaddrs", call, 2 * 1024, false}, {"/get_txt_records", nullptr, 0, false}, {"/get_unspent_outs", call, 2 * 1024, true}, + {"/get_version", call, 1024, false}, {"/import_wallet_request", call, 2 * 1024, false}, {"/login", call, 2 * 1024, false}, {"/provision_subaddrs", call, 2 * 1024, false}, @@ -2010,6 +2047,8 @@ namespace lws assert(strand_.running_in_this_thread()); if (error.category() == wire::error::rapidjson_category() || error == lws::error::invalid_range || error == lws::error::not_enough_amount) return bad_request(boost::beast::http::status::bad_request, std::forward(resume)); + else if (error == lws::error::bad_verb) + return bad_request(boost::beast::http::status::method_not_allowed, std::forward(resume)); else if (error == lws::error::account_not_found || error == lws::error::duplicate_request) return bad_request(boost::beast::http::status::forbidden, std::forward(resume)); else if (error == lws::error::max_subaddresses) @@ -2119,7 +2158,8 @@ namespace lws return self_->bad_request(boost::beast::http::status::bad_request, std::forward(resume)); } - if (self_->parser_->get().method() != boost::beast::http::verb::post) + const boost::beast::http::verb verb = self_->parser_->get().method(); + if (verb != boost::beast::http::verb::post && verb != boost::beast::http::verb::get) return self_->bad_request(boost::beast::http::status::method_not_allowed, std::forward(resume)); std::function resumer; @@ -2143,6 +2183,7 @@ namespace lws } MDEBUG("Running REST handler " << handler->name << " on " << self_.get()); + self_->data_.last_verb = verb; auto body = handler->run(std::move(self_->parser_->get()).body(), self_->data_, std::move(resumer)); if (!body) return self_->bad_request(body.error(), std::forward(resume)); diff --git a/src/rpc/light_wallet.cpp b/src/rpc/light_wallet.cpp index adc83bc..3ab20c9 100644 --- a/src/rpc/light_wallet.cpp +++ b/src/rpc/light_wallet.cpp @@ -37,10 +37,12 @@ #include "config.h" #include "db/string.h" #include "error.h" +#include "lws_version.h" #include "time_helper.h" // monero/contrib/epee/include #include "ringct/rctOps.h" // monero/src #include "span.h" // monero/contrib/epee/include #include "util/random_outputs.h" +#include "version.h" // monero/src #include "wire.h" #include "wire/adapted/crypto.h" #include "wire/error.h" @@ -362,6 +364,36 @@ namespace lws ); } + rpc::get_version_response::get_version_response(const db::block_id height, const std::uint32_t max_subaddresses) + : server_type(lws::version::name), + server_version(lws::version::id), + last_git_commit_hash(lws::version::commit), + last_git_commit_date(lws::version::date), + git_branch_name(lws::version::branch), + monero_version_full(MONERO_VERSION_FULL), + blockchain_height(height), + api(lws::version::api::combined), + max_subaddresses(max_subaddresses), + network(lws::rpc::network_type(lws::config::network)), + testnet(config::network == cryptonote::TESTNET) + {} + void rpc::write_bytes(wire::json_writer& dest, const get_version_response& self) + { + wire::object(dest, + WIRE_FIELD(server_type), + WIRE_FIELD(server_version), + WIRE_FIELD(last_git_commit_hash), + WIRE_FIELD(last_git_commit_date), + WIRE_FIELD(git_branch_name), + WIRE_FIELD(monero_version_full), + WIRE_FIELD_COPY(blockchain_height), + WIRE_FIELD(api), + WIRE_FIELD_COPY(max_subaddresses), + wire::field("network_type", self.network), + WIRE_FIELD_COPY(testnet) + ); + } + void rpc::read_bytes(wire::json_reader& source, import_request& self) { std::string address; diff --git a/src/rpc/light_wallet.h b/src/rpc/light_wallet.h index dc78199..5d6ffb8 100644 --- a/src/rpc/light_wallet.h +++ b/src/rpc/light_wallet.h @@ -224,6 +224,35 @@ namespace rpc void write_bytes(wire::json_writer&, const get_subaddrs_response&); + struct get_version_request + { + get_version_request() = delete; + }; + inline void read_bytes(const wire::reader&, const get_version_request&) + {} + + struct get_version_response + { + //! Defaults to current network in unavailable state + get_version_response(lws::db::block_id height, std::uint32_t max_subaddresses); + + + const std::string server_type; + const std::string server_version; + const std::string last_git_commit_hash; + const std::string last_git_commit_date; + const std::string git_branch_name; + const std::string monero_version_full; + + const db::block_id blockchain_height; + const std::uint32_t api; + const std::uint32_t max_subaddresses; + const network_type network; + const bool testnet; + }; + void write_bytes(wire::json_writer&, const get_version_response&); + + struct import_request { import_request() = delete;