diff options
| -rw-r--r-- | contrib/depends/packages/openssl.mk | 6 | ||||
| -rw-r--r-- | contrib/epee/include/net/http_base.h | 40 | ||||
| -rw-r--r-- | contrib/epee/include/net/http_client.h | 100 | ||||
| -rw-r--r-- | contrib/epee/include/net/http_protocol_handler.inl | 87 | ||||
| -rw-r--r-- | src/cryptonote_basic/miner.cpp | 3 | ||||
| -rw-r--r-- | src/cryptonote_core/blockchain.cpp | 53 | ||||
| -rw-r--r-- | src/cryptonote_core/cryptonote_tx_utils.cpp | 3 | ||||
| -rw-r--r-- | src/cryptonote_protocol/cryptonote_protocol_handler.inl | 19 | ||||
| -rw-r--r-- | src/daemon/rpc_command_executor.cpp | 10 | ||||
| -rw-r--r-- | src/net/zmq.cpp | 6 | ||||
| -rw-r--r-- | src/net/zmq.h | 2 | ||||
| -rw-r--r-- | src/rpc/core_rpc_server_commands_defs.h | 2 | ||||
| -rw-r--r-- | src/rpc/zmq_server.cpp | 4 | ||||
| -rw-r--r-- | src/wallet/wallet2.cpp | 203 | ||||
| -rw-r--r-- | src/wallet/wallet2.h | 7 | ||||
| -rw-r--r-- | src/wallet/wallet_errors.h | 2 | ||||
| -rw-r--r-- | tests/unit_tests/http.cpp | 252 |
17 files changed, 547 insertions, 252 deletions
diff --git a/contrib/depends/packages/openssl.mk b/contrib/depends/packages/openssl.mk index 77d4b2d11..926b3b180 100644 --- a/contrib/depends/packages/openssl.mk +++ b/contrib/depends/packages/openssl.mk @@ -1,8 +1,8 @@ package=openssl -$(package)_version=3.0.19 -$(package)_download_path=https://www.openssl.org/source +$(package)_version=3.0.21 +$(package)_download_path=https://github.com/openssl/openssl/releases/download/openssl-$($(package)_version) $(package)_file_name=$(package)-$($(package)_version).tar.gz -$(package)_sha256_hash=fa5a4143b8aae18be53ef2f3caf29a2e0747430b8bc74d32d88335b94ab63072 +$(package)_sha256_hash=617e29af8e421f46649484a4937e48c685e47f46488167c982f88bc4ec1d522f define $(package)_set_vars $(package)_config_env=AR="$($(package)_ar)" ARFLAGS=$($(package)_arflags) RANLIB="$($(package)_ranlib)" CC="$($(package)_cc)" diff --git a/contrib/epee/include/net/http_base.h b/contrib/epee/include/net/http_base.h index f32fdd9ae..aec0689a5 100644 --- a/contrib/epee/include/net/http_base.h +++ b/contrib/epee/include/net/http_base.h @@ -29,7 +29,7 @@ #pragma once #include "memwipe.h" -#include <boost/utility/string_ref.hpp> +#include <boost/utility/string_view.hpp> #include <string> #include <utility> @@ -70,7 +70,7 @@ namespace net_utils std::string get_value_from_uri_line(const std::string& param_name, const std::string& uri); - static inline void add_field(std::string& out, const boost::string_ref name, const boost::string_ref value) + static inline void add_field(std::string& out, boost::string_view name, boost::string_view value) { out.append(name.data(), name.size()).append(": "); out.append(value.data(), value.size()).append("\r\n"); @@ -80,6 +80,42 @@ namespace net_utils add_field(out, field.first, field.second); } + namespace detail + { + inline bool parse_header_line(boost::string_view line, boost::string_view& name, boost::string_view& value) + { + if(!line.empty() && line.back() == '\r') + line.remove_suffix(1); + if(line.empty()) + return false; + if(line.front() == ' ' || line.front() == '\t') + return false; + + const size_t colon = line.find(':'); + if(colon == boost::string_view::npos || colon == 0) + return false; + + name = line.substr(0, colon); + value = line.substr(colon + 1); + while(!value.empty() && (value.front() == ' ' || value.front() == '\t')) + value.remove_prefix(1); + while(!value.empty() && (value.back() == ' ' || value.back() == '\t')) + value.remove_suffix(1); + + for(char c : name) + { + const bool alnum = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + const bool allowed = alnum || + c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || + c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || + c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; + if(!allowed) + return false; + } + + return true; + } + } struct http_header_info { diff --git a/contrib/epee/include/net/http_client.h b/contrib/epee/include/net/http_client.h index 9ce30b620..949629352 100644 --- a/contrib/epee/include/net/http_client.h +++ b/contrib/epee/include/net/http_client.h @@ -414,7 +414,11 @@ namespace net_utils recv_buff.assign(m_header_cache.begin()+pos+4, m_header_cache.end()); m_header_cache.erase(m_header_cache.begin()+pos+4, m_header_cache.end()); - analize_cached_header_and_invoke_state(); + if(!analize_cached_header_and_invoke_state()) + { + m_state = reciev_machine_state_error; + return false; + } if (!on_header(m_response_info)) { MDEBUG("Connection cancelled by on_header"); @@ -647,64 +651,46 @@ namespace net_utils { MTRACE("http_stream_filter::parse_cached_header(*)"); - const char *ptr = m_cache_to_process.c_str(); - while (ptr[0] != '\r' || ptr[1] != '\n') + size_t cur = 0; + while(cur < m_cache_to_process.size()) { - // optional \n - if (*ptr == '\n') - ++ptr; - // an identifier composed of letters or - - const char *key_pos = ptr; - while (isalnum(*ptr) || *ptr == '_' || *ptr == '-') - ++ptr; - const char *key_end = ptr; - // optional space (not in RFC, but in previous code) - if (*ptr == ' ') - ++ptr; - CHECK_AND_ASSERT_MES(*ptr == ':', true, "http_stream_filter::parse_cached_header() invalid header in: " << m_cache_to_process); - ++ptr; - // optional whitespace, but not newlines - line folding is obsolete, let's ignore it - while (isblank(*ptr)) - ++ptr; - const char *value_pos = ptr; - while (*ptr != '\r' && *ptr != '\n') - ++ptr; - const char *value_end = ptr; - // optional trailing whitespace - while (value_end > value_pos && isblank(*(value_end-1))) - --value_end; - if (*ptr == '\r') - ++ptr; - CHECK_AND_ASSERT_MES(*ptr == '\n', true, "http_stream_filter::parse_cached_header() invalid header in: " << m_cache_to_process); - ++ptr; + const size_t line_end = m_cache_to_process.find('\n', cur); + CHECK_AND_ASSERT_MES(line_end != std::string::npos, false, "http_stream_filter::parse_cached_header() invalid header in: " << m_cache_to_process); - const std::string key = std::string(key_pos, key_end - key_pos); - const std::string value = std::string(value_pos, value_end - value_pos); - if (!key.empty()) - { - if (!string_tools::compare_no_case(key, "Connection")) - body_info.m_connection = value; - else if(!string_tools::compare_no_case(key, "Referrer")) - body_info.m_referer = value; - else if(!string_tools::compare_no_case(key, "Content-Length")) - body_info.m_content_length = value; - else if(!string_tools::compare_no_case(key, "Content-Type")) - body_info.m_content_type = value; - else if(!string_tools::compare_no_case(key, "Transfer-Encoding")) - body_info.m_transfer_encoding = value; - else if(!string_tools::compare_no_case(key, "Content-Encoding")) - body_info.m_content_encoding = value; - else if(!string_tools::compare_no_case(key, "Host")) - body_info.m_host = value; - else if(!string_tools::compare_no_case(key, "Cookie")) - body_info.m_cookie = value; - else if(!string_tools::compare_no_case(key, "User-Agent")) - body_info.m_user_agent = value; - else if(!string_tools::compare_no_case(key, "Origin")) - body_info.m_origin = value; - else - body_info.m_etc_fields.emplace_back(key, value); - } + boost::string_view line(m_cache_to_process.data() + cur, line_end - cur); + cur = line_end + 1; + + if(line == "\r" || line.empty()) + break; + + boost::string_view name; + boost::string_view value; + CHECK_AND_ASSERT_MES(detail::parse_header_line(line, name, value), false, "http_stream_filter::parse_cached_header() invalid header in: " << m_cache_to_process); + + std::string key(name.data(), name.size()); + std::string val(value.data(), value.size()); + if (!string_tools::compare_no_case(key, "Connection")) + body_info.m_connection = std::move(val); + else if(!string_tools::compare_no_case(key, "Referer")) + body_info.m_referer = std::move(val); + else if(!string_tools::compare_no_case(key, "Content-Length")) + body_info.m_content_length = std::move(val); + else if(!string_tools::compare_no_case(key, "Content-Type")) + body_info.m_content_type = std::move(val); + else if(!string_tools::compare_no_case(key, "Transfer-Encoding")) + body_info.m_transfer_encoding = std::move(val); + else if(!string_tools::compare_no_case(key, "Content-Encoding")) + body_info.m_content_encoding = std::move(val); + else if(!string_tools::compare_no_case(key, "Host")) + body_info.m_host = std::move(val); + else if(!string_tools::compare_no_case(key, "Cookie")) + body_info.m_cookie = std::move(val); + else if(!string_tools::compare_no_case(key, "User-Agent")) + body_info.m_user_agent = std::move(val); + else if(!string_tools::compare_no_case(key, "Origin")) + body_info.m_origin = std::move(val); + else + body_info.m_etc_fields.emplace_back(std::move(key), std::move(val)); } return true; } diff --git a/contrib/epee/include/net/http_protocol_handler.inl b/contrib/epee/include/net/http_protocol_handler.inl index 6647d1f15..3e46633b7 100644 --- a/contrib/epee/include/net/http_protocol_handler.inl +++ b/contrib/epee/include/net/http_protocol_handler.inl @@ -25,8 +25,9 @@ // -#include <boost/regex.hpp> +#include <boost/algorithm/string/predicate.hpp> #include <boost/lexical_cast.hpp> +#include <boost/regex.hpp> #include "http_protocol_handler.h" #include "reg_exp_definer.h" #include "string_tools.h" @@ -546,54 +547,52 @@ namespace net_utils template<class t_connection_context> bool simple_http_connection_handler<t_connection_context>::parse_cached_header(http_header_info& body_info, const std::string& m_cache_to_process, size_t pos) { - STATIC_REGEXP_EXPR_1(rexp_mach_field, - "\n?((Connection)|(Referer)|(Content-Length)|(Content-Type)|(Transfer-Encoding)|(Content-Encoding)|(Host)|(Cookie)|(User-Agent)|(Origin)" - // 12 3 4 5 6 7 8 9 10 11 - "|([\\w-]+?)) ?: ?((.*?)(\r?\n))[^\t ]", - //11 1213 14 - boost::regex::icase | boost::regex::normal); - - boost::smatch result; - std::string::const_iterator it_current_bound = m_cache_to_process.begin(); - std::string::const_iterator it_end_bound = m_cache_to_process.begin()+pos; - body_info.clear(); + if(pos > m_cache_to_process.size() || pos > HTTP_MAX_HEADER_LEN) + return false; + + size_t cur = 0; - //lookup all fields and fill well-known fields - while( boost::regex_search( it_current_bound, it_end_bound, result, rexp_mach_field, boost::match_default) && result[0].matched) + while(cur < pos) { - const size_t field_val = 14; - const size_t field_etc_name = 12; + const size_t line_end = m_cache_to_process.find('\n', cur); + if(line_end == std::string::npos || line_end >= pos) + break; - int i = 2; //start position = 2 - if(result[i++].matched)//"Connection" - body_info.m_connection = result[field_val]; - else if(result[i++].matched)//"Referer" - body_info.m_referer = result[field_val]; - else if(result[i++].matched)//"Content-Length" - body_info.m_content_length = result[field_val]; - else if(result[i++].matched)//"Content-Type" - body_info.m_content_type = result[field_val]; - else if(result[i++].matched)//"Transfer-Encoding" - body_info.m_transfer_encoding = result[field_val]; - else if(result[i++].matched)//"Content-Encoding" - body_info.m_content_encoding = result[field_val]; - else if(result[i++].matched)//"Host" - body_info.m_host = result[field_val]; - else if(result[i++].matched)//"Cookie" - body_info.m_cookie = result[field_val]; - else if(result[i++].matched)//"User-Agent" - body_info.m_user_agent = result[field_val]; - else if(result[i++].matched)//"Origin" - body_info.m_origin = result[field_val]; - else if(result[i++].matched)//e.t.c (HAVE TO BE MATCHED!) - body_info.m_etc_fields.push_back(std::pair<std::string, std::string>(result[field_etc_name], result[field_val])); - else - { - LOG_ERROR_CC(m_conn_context, "simple_http_connection_handler<t_connection_context>::parse_cached_header() not matched last entry in:" << m_cache_to_process); - } + boost::string_view line(m_cache_to_process.data() + cur, line_end - cur); + cur = line_end + 1; + + // End of header block. + if(line == "\r" || line.empty()) + break; - it_current_bound = result[(int)result.size()-1]. first; + boost::string_view name; + boost::string_view value; + if(!detail::parse_header_line(line, name, value)) + return false; + + if(boost::iequals(name, "Connection")) + body_info.m_connection = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Referer")) + body_info.m_referer = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Content-Length")) + body_info.m_content_length = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Content-Type")) + body_info.m_content_type = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Transfer-Encoding")) + body_info.m_transfer_encoding = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Content-Encoding")) + body_info.m_content_encoding = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Host")) + body_info.m_host = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Cookie")) + body_info.m_cookie = std::string(value.data(), value.size()); + else if(boost::iequals(name, "User-Agent")) + body_info.m_user_agent = std::string(value.data(), value.size()); + else if(boost::iequals(name, "Origin")) + body_info.m_origin = std::string(value.data(), value.size()); + else + body_info.m_etc_fields.push_back(std::make_pair(std::string(name.data(), name.size()), std::string(value.data(), value.size()))); } return true; } diff --git a/src/cryptonote_basic/miner.cpp b/src/cryptonote_basic/miner.cpp index 71b8f78cc..fd13ff74d 100644 --- a/src/cryptonote_basic/miner.cpp +++ b/src/cryptonote_basic/miner.cpp @@ -578,7 +578,8 @@ namespace cryptonote if ((b.major_version >= RX_BLOCK_VERSION) && !rx_set) { - crypto::rx_set_miner_thread(th_local_index, tools::get_max_concurrency()); + // Must be non-zero value because 0 means "not a miner thread, run with secure JIT" in rx-slow-hash.c + crypto::rx_set_miner_thread(th_local_index + 1, tools::get_max_concurrency()); rx_set = true; } diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 1004215d7..fcc92644a 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -5237,7 +5237,7 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete its = m_scan_table.find(tx_prefix_hash); assert(its != m_scan_table.end()); - // get all amounts from tx.vin(s) + // initialize amount buckets for tx.vin(s) for (const auto &txin : tx.vin) { const txin_to_key &in_to_key = boost::get < txin_to_key > (txin); @@ -5247,22 +5247,8 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete if (it != its->second.end()) SCAN_TABLE_QUIT("Duplicate key_image found from incoming blocks."); - amounts.push_back(in_to_key.amount); - } - - // sort and remove duplicate amounts from amounts list - std::sort(amounts.begin(), amounts.end()); - auto last = std::unique(amounts.begin(), amounts.end()); - amounts.erase(last, amounts.end()); - - // add amount to the offset_map and tx_map - for (const uint64_t &amount : amounts) - { - if (offset_map.find(amount) == offset_map.end()) - offset_map.emplace(amount, std::vector<uint64_t>()); - - if (tx_map.find(amount) == tx_map.end()) - tx_map.emplace(amount, std::vector<output_data_t>()); + offset_map.emplace(in_to_key.amount, std::vector<uint64_t>()); + tx_map.emplace(in_to_key.amount, std::vector<output_data_t>()); } // add new absolute_offsets to offset_map @@ -5279,6 +5265,10 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete ++block_index; } + amounts.reserve(offset_map.size()); + for (const auto &offsets : offset_map) + amounts.push_back(offsets.first); + // sort and remove duplicate absolute_offsets in offset_map for (auto &offsets : offset_map) { @@ -5337,25 +5327,24 @@ bool Blockchain::prepare_handle_incoming_blocks(const std::vector<block_complete const txin_to_key &in_to_key = boost::get < txin_to_key > (txin); auto needed_offsets = relative_output_offsets_to_absolute(in_to_key.key_offsets); + const auto offset_it = offset_map.find(in_to_key.amount); + const auto tx_it = tx_map.find(in_to_key.amount); + if (offset_it == offset_map.end() || tx_it == tx_map.end()) + SCAN_TABLE_QUIT("Amount not found on scan table from incoming blocks."); + + const std::vector<uint64_t> &offsets_found = offset_it->second; + const std::vector<output_data_t> &outputs_found = tx_it->second; + std::vector<output_data_t> outputs; for (const uint64_t & offset_needed : needed_offsets) { - size_t pos = 0; - bool found = false; - - for (const uint64_t &offset_found : offset_map[in_to_key.amount]) - { - if (offset_needed == offset_found) - { - found = true; - break; - } - - ++pos; - } + // offsets_found is sorted above before output_scan_worker populates outputs_found. + const auto found_it = std::lower_bound(offsets_found.begin(), offsets_found.end(), offset_needed); + const size_t pos = found_it - offsets_found.begin(); + const bool found = found_it != offsets_found.end() && *found_it == offset_needed; - if (found && pos < tx_map[in_to_key.amount].size()) - outputs.push_back(tx_map[in_to_key.amount].at(pos)); + if (found && pos < outputs_found.size()) + outputs.push_back(outputs_found[pos]); else break; } diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index c350e24c4..7aea5cf50 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -453,11 +453,12 @@ namespace cryptonote crypto::public_key out_eph_public_key; crypto::view_tag view_tag; - hwdev.generate_output_ephemeral_keys(tx.version,sender_account_keys, txkey_pub, tx_key, + const bool r = hwdev.generate_output_ephemeral_keys(tx.version,sender_account_keys, txkey_pub, tx_key, dst_entr, change_addr, output_index, need_additional_txkeys, additional_tx_keys, additional_tx_public_keys, amount_keys, out_eph_public_key, use_view_tags, view_tag); + CHECK_AND_ASSERT_MES(r, false, "Failed to generate output ephemeral keys"); tx_out out; cryptonote::set_tx_out(dst_entr.amount, out_eph_public_key, use_view_tags, view_tag, out); diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl index 6c36a3b5b..3ad26a3de 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl +++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl @@ -378,6 +378,11 @@ namespace cryptonote cnx.ip = cnx.host; cnx.port = std::to_string(cntxt.m_remote_address.as<epee::net_utils::ipv4_network_address>().port()); } + else if (cntxt.m_remote_address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id()) + { + cnx.ip = cnx.host; + cnx.port = std::to_string(cntxt.m_remote_address.as<epee::net_utils::ipv6_network_address>().port()); + } cnx.rpc_port = cntxt.m_rpc_port; cnx.rpc_credits_per_hash = cntxt.m_rpc_credits_per_hash; @@ -878,17 +883,23 @@ namespace cryptonote int t_cryptonote_protocol_handler<t_core>::handle_notify_new_transactions(int command, NOTIFY_NEW_TRANSACTIONS::request& arg, cryptonote_connection_context& context) { MLOG_P2P_MESSAGE("Received NOTIFY_NEW_TRANSACTIONS (" << arg.txs.size() << " txes)"); - std::unordered_set<blobdata> seen; + std::unordered_set<crypto::hash> seen; + seen.reserve(arg.txs.size()); + for (const auto &blob: arg.txs) { MLOGIF_P2P_MESSAGE(cryptonote::transaction tx; crypto::hash hash; bool ret = cryptonote::parse_and_validate_tx_from_blob(blob, tx, hash);, ret, "Including transaction " << hash); - if (seen.find(blob) != seen.end()) + + crypto::hash digest{}; + if (!blob.empty()) + tools::sha256sum(reinterpret_cast<const uint8_t*>(blob.data()), blob.size(), digest); + + if (!seen.insert(digest).second) { LOG_PRINT_CCONTEXT_L1("Duplicate transaction in notification, dropping connection"); drop_connection(context, false, false); return 1; } - seen.insert(blob); } if(context.m_state != cryptonote_connection_context::state_normal) @@ -2841,6 +2852,8 @@ skip: { MINFO("Target height decreasing from " << previous_target << " to " << target); m_core.set_target_blockchain_height(target); + if (target < m_core.get_current_blockchain_height() + 5) + m_core.safesyncmode(true); if (target == 0 && context.m_state > cryptonote_connection_context::state_before_handshake && !m_stopping) { MCWARNING("global", "monerod is now disconnected from the network"); diff --git a/src/daemon/rpc_command_executor.cpp b/src/daemon/rpc_command_executor.cpp index f0407eece..f6746b8ad 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -644,7 +644,11 @@ bool t_rpc_command_executor::print_connections() { } } - tools::msg_writer() << std::setw(30) << std::left << "Remote Host" + int host_field_width = 15; + for (const auto &conn : res.connections) + host_field_width = std::max(host_field_width, 8 + (int) conn.address.length()); + + tools::msg_writer() << std::setw(host_field_width) << std::left << "Remote Host" << std::setw(8) << "Type" << std::setw(6) << "SSL" << std::setw(20) << "Peer id" @@ -661,11 +665,11 @@ bool t_rpc_command_executor::print_connections() { for (auto & info : res.connections) { std::string address = info.incoming ? "INC " : "OUT "; - address += info.ip + ":" + info.port; + address += info.address; //std::string in_out = info.incoming ? "INC " : "OUT "; tools::msg_writer() //<< std::setw(30) << std::left << in_out - << std::setw(30) << std::left << address + << std::setw(host_field_width) << std::left << address << std::setw(8) << (get_address_type_name((epee::net_utils::address_type)info.address_type)) << std::setw(6) << (info.ssl ? "yes" : "no") << std::setw(20) << info.peer_id diff --git a/src/net/zmq.cpp b/src/net/zmq.cpp index 2b3ca8376..4a9d3e162 100644 --- a/src/net/zmq.cpp +++ b/src/net/zmq.cpp @@ -150,6 +150,12 @@ namespace zmq if ((last = zmq_msg_recv(part.handle(), socket, flags)) < 0) return last; + if (max_message_size < payload.size() || + max_message_size - payload.size() < part.size()) + { + errno = EMSGSIZE; + return -1; + } payload.append(part.data(), part.size()); if (!zmq_msg_more(part.handle())) break; diff --git a/src/net/zmq.h b/src/net/zmq.h index 18bb80c8b..68b6a71be 100644 --- a/src/net/zmq.h +++ b/src/net/zmq.h @@ -64,6 +64,8 @@ namespace net { namespace zmq { + constexpr std::size_t max_message_size = 10 * 1024 * 1024; // 10 MiB + //! \return Category for ZMQ errors. const std::error_category& error_category() noexcept; diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index 46e52d42f..e6c07ce77 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -88,7 +88,7 @@ namespace cryptonote // advance which version they will stop working with // Don't go over 32767 for any of these #define CORE_RPC_VERSION_MAJOR 3 -#define CORE_RPC_VERSION_MINOR 15 +#define CORE_RPC_VERSION_MINOR 16 #define MAKE_CORE_RPC_VERSION(major,minor) (((major)<<16)|(minor)) #define CORE_RPC_VERSION MAKE_CORE_RPC_VERSION(CORE_RPC_VERSION_MAJOR, CORE_RPC_VERSION_MINOR) diff --git a/src/rpc/zmq_server.cpp b/src/rpc/zmq_server.cpp index 7ebb6c49f..3f1f38e21 100644 --- a/src/rpc/zmq_server.cpp +++ b/src/rpc/zmq_server.cpp @@ -46,7 +46,7 @@ namespace cryptonote namespace { constexpr const int num_zmq_threads = 1; - constexpr const std::int64_t max_message_size = 10 * 1024 * 1024; // 10 MiB + constexpr const std::int64_t max_frame_size = net::zmq::max_message_size; constexpr const std::chrono::seconds linger_timeout{2}; // wait period for pending out messages net::zmq::socket init_socket(void* context, int type, epee::span<const std::string> addresses) @@ -62,7 +62,7 @@ namespace return nullptr; } - if (zmq_setsockopt(out.get(), ZMQ_MAXMSGSIZE, std::addressof(max_message_size), sizeof(max_message_size)) != 0) + if (zmq_setsockopt(out.get(), ZMQ_MAXMSGSIZE, std::addressof(max_frame_size), sizeof(max_frame_size)) != 0) { MONERO_LOG_ZMQ_ERROR("Failed to set maximum incoming message size"); return nullptr; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 61f49481e..4c83dd890 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -3105,6 +3105,7 @@ void read_pool_txs(const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request &req, MDEBUG("Reading pool txs"); if (res.txs.size() == req.txs_hashes.size()) { + const std::unordered_set<crypto::hash> txid_set(txids.begin(), txids.end()); for (const auto &tx_entry: res.txs) { if (tx_entry.in_pool) @@ -3115,9 +3116,7 @@ void read_pool_txs(const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request &req, if (get_pruned_tx(tx_entry, tx, tx_hash)) { - const std::vector<crypto::hash>::const_iterator i = std::find_if(txids.begin(), txids.end(), - [tx_hash](const crypto::hash &e) { return e == tx_hash; }); - if (i != txids.end()) + if (txid_set.count(tx_hash) > 0) { txs.push_back(std::make_tuple(tx, tx_hash, tx_entry.double_spend_seen)); } @@ -3362,11 +3361,14 @@ void wallet2::process_parsed_blocks(uint64_t start_height, const std::vector<cry { THROW_WALLET_EXCEPTION_IF(txidx >= tx_cache_data.size(), error::wallet_internal_error, "txidx out of range"); const cryptonote::transaction& tx = parsed_blocks[i].block.miner_tx; - const size_t n_vouts = (m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2) ? 1 : tx.vout.size(); - if (parsed_blocks[i].block.major_version >= hf_version_view_tags) - geniods.push_back(geniod_params{ tx, n_vouts, txidx }); - else - tpool.submit(&waiter, [&, n_vouts, txidx](){ geniod(tx, n_vouts, txidx); }, true); + const size_t n_vouts = (m_refresh_type == RefreshType::RefreshOptimizeCoinbase && tx.version < 2 && !tx.vout.empty()) ? 1 : tx.vout.size(); + if (n_vouts > 0) + { + if (parsed_blocks[i].block.major_version >= hf_version_view_tags) + geniods.push_back(geniod_params{ tx, n_vouts, txidx }); + else + tpool.submit(&waiter, [&, n_vouts, txidx](){ geniod(tx, n_vouts, txidx); }, true); + } } ++txidx; for (size_t j = 0; j < parsed_blocks[i].txes.size(); ++j) @@ -3566,21 +3568,18 @@ void wallet2::pull_and_parse_next_blocks(bool first, bool try_incremental, uint6 void wallet2::remove_obsolete_pool_txs(const std::vector<crypto::hash> &tx_hashes, bool remove_if_found) { + remove_obsolete_pool_txs(std::unordered_set<crypto::hash>(tx_hashes.begin(), tx_hashes.end()), remove_if_found); +} + +void wallet2::remove_obsolete_pool_txs(const std::unordered_set<crypto::hash> &tx_hashes, bool remove_if_found) +{ // remove pool txes to us that aren't in the pool anymore (remove_if_found = false), // or remove pool txes to us that were reported as removed (remove_if_found = true) std::unordered_multimap<crypto::hash, wallet2::pool_payment_details>::iterator uit = m_unconfirmed_payments.begin(); while (uit != m_unconfirmed_payments.end()) { const crypto::hash &txid = uit->second.m_pd.m_tx_hash; - bool found = false; - for (const auto &it2: tx_hashes) - { - if (it2 == txid) - { - found = true; - break; - } - } + const bool found = tx_hashes.count(txid) > 0; auto pit = uit++; if ((!remove_if_found && !found) || (remove_if_found && found)) { @@ -3596,17 +3595,9 @@ void wallet2::remove_obsolete_pool_txs(const std::vector<crypto::hash> &tx_hashe // Code that is common to 'update_pool_state_by_pool_query' and 'update_pool_state_from_pool_data': // Check wether a tx in the pool is worthy of processing because we did not see it // yet or because it is "interesting" out of special circumstances -bool wallet2::accept_pool_tx_for_processing(const crypto::hash &txid) +bool wallet2::accept_pool_tx_for_processing(const crypto::hash &txid, const std::unordered_set<crypto::hash> &payments_tx_hashes) { - bool txid_found_in_up = false; - for (const auto &up: m_unconfirmed_payments) - { - if (up.second.m_pd.m_tx_hash == txid) - { - txid_found_in_up = true; - break; - } - } + const bool txid_found_in_up = payments_tx_hashes.count(txid) > 0; if (m_scanned_pool_txs[0].find(txid) != m_scanned_pool_txs[0].end() || m_scanned_pool_txs[1].find(txid) != m_scanned_pool_txs[1].end()) { // if it's for us, we want to keep track of whether we saw a double spend, so don't bail out @@ -3619,30 +3610,25 @@ bool wallet2::accept_pool_tx_for_processing(const crypto::hash &txid) if (!txid_found_in_up) { LOG_PRINT_L1("Found new pool tx: " << txid); - bool found = false; - for (const auto &i: m_unconfirmed_txs) + const auto i = m_unconfirmed_txs.find(txid); + bool sent_by_us = i != m_unconfirmed_txs.end(); + if (sent_by_us) { - if (i.first == txid) + const unconfirmed_transfer_details& utd = i->second; + for (const auto& dst : utd.m_dests) { - found = true; - // if this is a payment to yourself at a different subaddress account, don't skip it - // so that you can see the incoming pool tx with 'show_transfers' on that receiving subaddress account - const unconfirmed_transfer_details& utd = i.second; - for (const auto& dst : utd.m_dests) + auto subaddr_index = m_subaddresses.find(dst.addr.m_spend_public_key); + if (subaddr_index != m_subaddresses.end() && subaddr_index->second.major != utd.m_subaddr_account) { - auto subaddr_index = m_subaddresses.find(dst.addr.m_spend_public_key); - if (subaddr_index != m_subaddresses.end() && subaddr_index->second.major != utd.m_subaddr_account) - { - found = false; - break; - } + // Payment to ourselves at a different subaddress account: + // process it so the receiving account can show the incoming pool tx. + sent_by_us = false; + break; } - break; } } - if (!found) + if (!sent_by_us) { - // not one of those we sent ourselves return true; } else @@ -3813,19 +3799,14 @@ void wallet2::update_pool_state_by_pool_query(std::vector<std::tuple<cryptonote: // remove any pending tx that's not in the pool const auto now = std::chrono::system_clock::now(); std::unordered_map<crypto::hash, wallet2::unconfirmed_transfer_details>::iterator it = m_unconfirmed_txs.begin(); + + const std::unordered_set<crypto::hash> pool_set(res.tx_hashes.begin(), res.tx_hashes.end()); + while (it != m_unconfirmed_txs.end()) { const crypto::hash &txid = it->first; MDEBUG("Checking m_unconfirmed_txs entry " << txid); - bool found = false; - for (const auto &it2: res.tx_hashes) - { - if (it2 == txid) - { - found = true; - break; - } - } + const bool found = pool_set.count(txid) > 0; auto pit = it++; process_unconfirmed_transfer(false, txid, pit->second, found, now, refreshed); MDEBUG("New state of that entry: " << pit->second.m_state); @@ -3837,15 +3818,21 @@ void wallet2::update_pool_state_by_pool_query(std::vector<std::tuple<cryptonote: // the in transfers list instead (or nowhere if it just // disappeared without being mined) if (refreshed) - remove_obsolete_pool_txs(res.tx_hashes, false); + remove_obsolete_pool_txs(pool_set, false); MTRACE("update_pool_state_by_pool_query done second loop"); + std::unordered_set<crypto::hash> payments_tx_hashes; + payments_tx_hashes.reserve(m_unconfirmed_payments.size()); + for (const auto &p: m_unconfirmed_payments) + payments_tx_hashes.insert(p.second.m_pd.m_tx_hash); + // gather txids of new pool txes to us std::vector<crypto::hash> txids; + txids.reserve(res.tx_hashes.size()); for (const auto &txid: res.tx_hashes) { - if (accept_pool_tx_for_processing(txid)) + if (accept_pool_tx_for_processing(txid, payments_tx_hashes)) txids.push_back(txid); } @@ -3872,6 +3859,11 @@ void wallet2::update_pool_state_from_pool_data(bool incremental, const std::vect m_encrypt_keys_after_refresh.reset(); }); + std::unordered_set<crypto::hash> added_pool_txids; + added_pool_txids.reserve(added_pool_txs.size()); + for (const auto &pool_tx: added_pool_txs) + added_pool_txids.insert(std::get<1>(pool_tx)); + if (refreshed) { if (incremental) @@ -3884,16 +3876,8 @@ void wallet2::update_pool_state_from_pool_data(bool incremental, const std::vect } else { - // Delete from the list of unconfirmed payments what we don't find anymore in the pool; a bit - // unfortunate that we have to build a new vector with ids first, but better than copying and - // modifying the code of 'remove_obsolete_pool_txs' here - std::vector<crypto::hash> txids; - txids.reserve(added_pool_txs.size()); - for (const auto &pool_tx: added_pool_txs) - { - txids.push_back(std::get<1>(pool_tx)); - } - remove_obsolete_pool_txs(txids, false); + // Delete from the list of unconfirmed payments what we don't find anymore in the pool + remove_obsolete_pool_txs(added_pool_txids, false); } } @@ -3904,15 +3888,7 @@ void wallet2::update_pool_state_from_pool_data(bool incremental, const std::vect { const crypto::hash &txid = it->first; MDEBUG("Checking m_unconfirmed_txs entry " << txid); - bool found = false; - for (const auto &pool_tx: added_pool_txs) - { - if (std::get<1>(pool_tx) == txid) - { - found = true; - break; - } - } + const bool found = added_pool_txids.count(txid) > 0; auto pit = it++; process_unconfirmed_transfer(incremental, txid, pit->second, found, now, refreshed); MDEBUG("Resulting state of that entry: " << pit->second.m_state); @@ -3922,9 +3898,13 @@ void wallet2::update_pool_state_from_pool_data(bool incremental, const std::vect // if we work incrementally and thus see only new pool txs since last time we asked it should // be rare that we know already about one of those, but check nevertheless process_txs.clear(); + std::unordered_set<crypto::hash> payments_tx_hashes; + payments_tx_hashes.reserve(m_unconfirmed_payments.size()); + for (const auto &p: m_unconfirmed_payments) + payments_tx_hashes.insert(p.second.m_pd.m_tx_hash); for (const auto &pool_tx: added_pool_txs) { - if (accept_pool_tx_for_processing(std::get<1>(pool_tx))) + if (accept_pool_tx_for_processing(std::get<1>(pool_tx), payments_tx_hashes)) { process_txs.push_back(pool_tx); } @@ -8155,14 +8135,6 @@ std::string wallet2::save_multisig_tx(multisig_tx_set txs) { LOG_PRINT_L0("saving " << txs.m_ptx.size() << " multisig transactions"); - // txes generated, get rid of used k values - for (size_t n = 0; n < txs.m_ptx.size(); ++n) - for (size_t idx: txs.m_ptx[n].construction_data.selected_transfers) - { - memwipe(m_transfers[idx].m_multisig_k.data(), m_transfers[idx].m_multisig_k.size() * sizeof(m_transfers[idx].m_multisig_k[0])); - m_transfers[idx].m_multisig_k.clear(); - } - // zero out some data we don't want to share for (auto &ptx: txs.m_ptx) { @@ -8190,6 +8162,11 @@ std::string wallet2::save_multisig_tx(multisig_tx_set txs) } LOG_PRINT_L2("Saving multisig unsigned tx data: " << oss.str()); std::string ciphertext = encrypt_with_view_secret_key(oss.str()); + + // The transaction creator has already signed, so do not expose the txset + // until the corresponding one-time nonce erasure is stored. + clear_multisig_k_and_store(txs); + return std::string(MULTISIG_UNSIGNED_TX_PREFIX) + ciphertext; } //---------------------------------------------------------------------------------------------------- @@ -8348,8 +8325,12 @@ bool wallet2::load_multisig_tx_from_file(const std::string &filename, multisig_t return true; } //---------------------------------------------------------------------------------------------------- -bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, std::vector<crypto::hash> &txids) +bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs_inout, std::vector<crypto::hash> &txids) { + multisig_tx_set exported_txs = exported_txs_inout; + std::vector<crypto::hash> signed_txids; + std::vector<std::pair<crypto::hash, size_t>> signed_tx_key_indices; + THROW_WALLET_EXCEPTION_IF(exported_txs.m_ptx.empty(), error::wallet_internal_error, "No tx found"); const crypto::public_key local_signer = get_multisig_signer_public_key(); @@ -8363,8 +8344,6 @@ bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, std::vector<crypto THROW_WALLET_EXCEPTION_IF(frozen(exported_txs), error::wallet_internal_error, "Will not sign multisig tx containing frozen outputs") - txids.clear(); - // The 'exported_txs' contains a set of different transactions for the multisig group to try to sign. Each of those // transactions has a set of 'signing attempts' corresponding to all the possible signing groups within the multisig. // - Here, we will partially sign as many of those signing attempts as possible, for each proposed transaction. @@ -8474,25 +8453,26 @@ bool wallet2::sign_multisig_tx(multisig_tx_set &exported_txs, std::vector<crypto "Unable to finalize the transaction: the ignore sets for these tx attempts seem to be malformed."); const crypto::hash txid = get_transaction_hash(ptx.tx); if (store_tx_info()) - { - m_tx_keys[txid] = ptx.tx_key; - m_additional_tx_keys[txid] = ptx.additional_tx_keys; - } - txids.push_back(txid); + signed_tx_key_indices.emplace_back(txid, n); + signed_txids.push_back(txid); } } - // signatures generated, get rid of any unused k values (must do export_multisig() to make more tx attempts with the - // inputs in the transactions worked on here) - for (size_t n = 0; n < exported_txs.m_ptx.size(); ++n) - for (size_t idx: exported_txs.m_ptx[n].construction_data.selected_transfers) - { - memwipe(m_transfers[idx].m_multisig_k.data(), m_transfers[idx].m_multisig_k.size() * sizeof(m_transfers[idx].m_multisig_k[0])); - m_transfers[idx].m_multisig_k.clear(); - } + exported_txs.m_signers.insert(local_signer); - exported_txs.m_signers.insert(get_multisig_signer_public_key()); + // Do not expose signatures until all nonce material for the selected inputs + // has been erased from the wallet cache. + clear_multisig_k_and_store(exported_txs); + for (const auto &entry: signed_tx_key_indices) + { + const auto &ptx = exported_txs.m_ptx[entry.second]; + m_tx_keys[entry.first] = ptx.tx_key; + m_additional_tx_keys[entry.first] = ptx.additional_tx_keys; + } + + exported_txs_inout = std::move(exported_txs); + txids = std::move(signed_txids); return true; } //---------------------------------------------------------------------------------------------------- @@ -9285,6 +9265,8 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>> // check we're clear enough of rct start, to avoid corner cases below THROW_WALLET_EXCEPTION_IF(rct_offsets.size() <= CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE, error::get_output_distribution, "Not enough rct outputs"); + THROW_WALLET_EXCEPTION_IF(!std::is_sorted(rct_offsets.begin(), rct_offsets.end()), + error::get_output_distribution, "Daemon reports non-monotonic rct output distribution"); THROW_WALLET_EXCEPTION_IF(rct_offsets.back() <= max_rct_index, error::get_output_distribution, "Daemon reports suspicious number of rct outputs"); } @@ -15223,6 +15205,27 @@ void wallet2::get_multisig_k(size_t idx, const std::unordered_set<rct::key> &use THROW_WALLET_EXCEPTION(tools::error::multisig_export_needed); } //---------------------------------------------------------------------------------------------------- +void wallet2::clear_multisig_k_and_store(const multisig_tx_set &txs) +{ + // Must succeed before any txset produced with these nonces is exposed. + bool changed = false; + for (const auto &ptx: txs.m_ptx) + { + for (size_t idx: ptx.construction_data.selected_transfers) + { + std::vector<rct::key> &multisig_k = m_transfers[idx].m_multisig_k; + if (multisig_k.empty()) + continue; + memwipe(multisig_k.data(), multisig_k.size() * sizeof(multisig_k[0])); + multisig_k.clear(); + changed = true; + } + } + + if (changed) + store(); +} +//---------------------------------------------------------------------------------------------------- rct::multisig_kLRki wallet2::get_multisig_kLRki(size_t n, const rct::key &k) const { CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad m_transfers index"); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index cedfecbeb..b18e9d8c6 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -31,6 +31,7 @@ #pragma once #include <memory> +#include <unordered_set> #include <boost/program_options/options_description.hpp> #include <boost/program_options/variables_map.hpp> @@ -1211,7 +1212,7 @@ private: bool load_multisig_tx(cryptonote::blobdata blob, multisig_tx_set &exported_txs, std::function<bool(const multisig_tx_set&)> accept_func = NULL); bool load_multisig_tx_from_file(const std::string &filename, multisig_tx_set &exported_txs, std::function<bool(const multisig_tx_set&)> accept_func = NULL); bool sign_multisig_tx_from_file(const std::string &filename, std::vector<crypto::hash> &txids, std::function<bool(const multisig_tx_set&)> accept_func); - bool sign_multisig_tx(multisig_tx_set &exported_txs, std::vector<crypto::hash> &txids); + bool sign_multisig_tx(multisig_tx_set &exported_txs_inout, std::vector<crypto::hash> &txids); bool sign_multisig_tx_to_file(multisig_tx_set &exported_txs, const std::string &filename, std::vector<crypto::hash> &txids); std::vector<pending_tx> create_unmixable_sweep_transactions(); void discard_unmixable_outputs(); @@ -1649,6 +1650,7 @@ private: void update_pool_state(std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> &process_txs, bool refreshed = false, bool try_incremental = false); void process_pool_state(const std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> &txs); void remove_obsolete_pool_txs(const std::vector<crypto::hash> &tx_hashes, bool remove_if_found); + void remove_obsolete_pool_txs(const std::unordered_set<crypto::hash> &tx_hashes, bool remove_if_found); std::string encrypt(const char *plaintext, size_t len, const crypto::secret_key &skey, bool authenticated = true) const; std::string encrypt(const epee::span<char> &span, const crypto::secret_key &skey, bool authenticated = true) const; @@ -1869,7 +1871,7 @@ private: void fast_refresh(uint64_t stop_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, bool force = false); void pull_and_parse_next_blocks(bool first, bool try_incremental, uint64_t start_height, uint64_t &blocks_start_height, std::list<crypto::hash> &short_chain_history, const std::vector<cryptonote::block_complete_entry> &prev_blocks, const std::vector<parsed_block> &prev_parsed_blocks, std::vector<cryptonote::block_complete_entry> &blocks, std::vector<parsed_block> &parsed_blocks, bool &last, bool &error, std::exception_ptr &exception); void process_parsed_blocks(uint64_t start_height, const std::vector<cryptonote::block_complete_entry> &blocks, const std::vector<parsed_block> &parsed_blocks, uint64_t& blocks_added, std::map<std::pair<uint64_t, uint64_t>, size_t> *output_tracker_cache = NULL); - bool accept_pool_tx_for_processing(const crypto::hash &txid); + bool accept_pool_tx_for_processing(const crypto::hash &txid, const std::unordered_set<crypto::hash> &payments_tx_hashes); void process_unconfirmed_transfer(bool incremental, const crypto::hash &txid, wallet2::unconfirmed_transfer_details &tx_details, bool seen_in_pool, std::chrono::system_clock::time_point now, bool refreshed); void process_pool_info_extent(const cryptonote::COMMAND_RPC_GET_BLOCKS_FAST::response &res, std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> &process_txs, bool refreshed); void update_pool_state_by_pool_query(std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> &process_txs, bool refreshed = false); @@ -1908,6 +1910,7 @@ private: rct::multisig_kLRki get_multisig_composite_kLRki(size_t n, const std::unordered_set<crypto::public_key> &ignore_set, std::unordered_set<rct::key> &used_L, std::unordered_set<rct::key> &new_used_L) const; rct::multisig_kLRki get_multisig_kLRki(size_t n, const rct::key &k) const; void get_multisig_k(size_t idx, const std::unordered_set<rct::key> &used_L, rct::key &nonce); + void clear_multisig_k_and_store(const multisig_tx_set &txs); void update_multisig_rescan_info(const std::vector<std::vector<rct::key>> &multisig_k, const std::vector<std::vector<tools::wallet2::multisig_info>> &info, size_t n); bool add_rings(const crypto::chacha_key &key, const cryptonote::transaction_prefix &tx); bool add_rings(const cryptonote::transaction_prefix &tx); diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index c54cd3499..e0a767159 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -440,7 +440,7 @@ namespace tools struct out_of_hashchain_bounds_error : public refresh_error { explicit out_of_hashchain_bounds_error(std::string&& loc) - : refresh_error(std::move(loc), "Index out of bounds of of hashchain") + : refresh_error(std::move(loc), "Index out of bounds of hashchain") { } diff --git a/tests/unit_tests/http.cpp b/tests/unit_tests/http.cpp index 1746ee190..b9f569fda 100644 --- a/tests/unit_tests/http.cpp +++ b/tests/unit_tests/http.cpp @@ -28,6 +28,9 @@ #include "gtest/gtest.h" #include "net/http_auth.h" +#include "net/http_client.h" +#include "syncobj.h" +#include "net/http_protocol_handler.h" #include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/join.hpp> @@ -51,6 +54,7 @@ #include <boost/spirit/include/qi_plus.hpp> #include <boost/spirit/include/qi_sequence.hpp> #include <boost/spirit/include/qi_string.hpp> +#include <chrono> #include <cstdint> #include <iterator> #include <openssl/evp.h> @@ -67,6 +71,111 @@ namespace http = epee::net_utils::http; using fields = std::unordered_map<std::string, std::string>; using auth_responses = std::vector<fields>; +class test_http_endpoint final : public epee::net_utils::i_service_endpoint +{ +public: + bool do_send(epee::byte_slice message) override + { + sent.append(reinterpret_cast<const char*>(message.data()), message.size()); + return true; + } + bool close(const bool) override { return true; } + bool send_done() override { return true; } + bool call_run_once_service_io() override { return true; } + bool request_callback() override { return true; } + boost::asio::io_context& get_io_context() override { return io_context; } + bool add_ref() override { return true; } + bool release() override { return true; } + + boost::asio::io_context io_context; + std::string sent; +}; + +class dummy_client +{ +public: + bool connect(const std::string&, int, std::chrono::milliseconds, bool = false, const std::string& = "0.0.0.0") { return true; } + bool connect(const std::string&, const std::string&, std::chrono::milliseconds, bool = false, const std::string& = "0.0.0.0") { return true; } + bool disconnect() { return true; } + bool send(const boost::string_ref, std::chrono::milliseconds) { return true; } + bool send(const void*, size_t) { return true; } + bool recv(std::string& buff, std::chrono::milliseconds) + { + buff = data; + data.clear(); + return true; + } + void set_ssl(epee::net_utils::ssl_options_t) { } + bool is_connected(bool *ssl = NULL) { return true; } + uint64_t get_bytes_sent() const { return 1; } + uint64_t get_bytes_received() const { return 1; } + + void set_test_data(const std::string& s) { data = s; } + +private: + std::string data; +}; + +class test_http_client final : public http::http_simple_client_template<dummy_client> +{ +public: + bool on_header(const http::http_response_info& headers) override + { + ++headers_seen; + last_headers = headers; + return true; + } + + http::http_response_info last_headers; + unsigned headers_seen = 0; +}; + +class capturing_http_handler final : public http::i_http_server_handler<epee::net_utils::connection_context_base> +{ +public: + bool handle_http_request( + const http::http_request_info& query_info, + http::http_response_info& response, + epee::net_utils::connection_context_base&) override + { + requests.push_back(query_info); + response.m_response_code = 200; + response.m_response_comment = "OK"; + response.m_mime_tipe = "text/plain"; + return true; + } + + std::vector<http::http_request_info> requests; +}; + +struct http_request_capture +{ + std::vector<bool> results; + std::vector<http::http_request_info> requests; + std::string sent; +}; + +http_request_capture feed_http_request(const std::vector<std::string>& chunks) +{ + capturing_http_handler handler; + test_http_endpoint endpoint; + epee::net_utils::connection_context_base context; + http::custum_handler_config<epee::net_utils::connection_context_base> config; + config.m_phandler = &handler; + + http::http_custom_handler<epee::net_utils::connection_context_base> connection(&endpoint, config, context); + std::vector<bool> results; + for (const std::string& chunk : chunks) + results.push_back(connection.handle_recv(chunk.data(), chunk.size())); + + return {results, handler.requests, endpoint.sent}; +} + +http_request_capture feed_http_request(const std::string& request) +{ + return feed_http_request(std::vector<std::string>{request}); +} + void rng(size_t len, uint8_t *ptr) { crypto::rand(len, ptr); @@ -701,6 +810,149 @@ TEST(HTTP_Client_Auth, MD5_auth) } +TEST(HTTP, Parse_Header_Line) +{ + boost::string_view name; + boost::string_view value; + + ASSERT_TRUE(http::detail::parse_header_line("Host: example.com\r", name, value)); + EXPECT_EQ("Host", name); + EXPECT_EQ("example.com", value); + + ASSERT_TRUE(http::detail::parse_header_line("Content-Type:\t application/json \t", name, value)); + EXPECT_EQ("Content-Type", name); + EXPECT_EQ("application/json", value); + + ASSERT_TRUE(http::detail::parse_header_line("Host:example.com", name, value)); + EXPECT_EQ("Host", name); + EXPECT_EQ("example.com", value); + + ASSERT_TRUE(http::detail::parse_header_line("X!#$%&'*+-.^_`|~: value", name, value)); + EXPECT_EQ("X!#$%&'*+-.^_`|~", name); + EXPECT_EQ("value", value); + + EXPECT_FALSE(http::detail::parse_header_line("", name, value)); + EXPECT_FALSE(http::detail::parse_header_line("Host : example.com", name, value)); + EXPECT_FALSE(http::detail::parse_header_line(" Content-Length: 1", name, value)); + EXPECT_FALSE(http::detail::parse_header_line("\tContent-Length: 1", name, value)); + EXPECT_FALSE(http::detail::parse_header_line("Bad Header", name, value)); + EXPECT_FALSE(http::detail::parse_header_line("Host example.com", name, value)); + EXPECT_FALSE(http::detail::parse_header_line("GET / HTTP/1.1", name, value)); +} + +TEST(HTTP, Server_Parses_Content_Length_First_Header) +{ + const std::string body = "0123456789"; + const auto capture = feed_http_request( + "POST /json_rpc HTTP/1.1\r\n" + "Content-Length: 10\r\n" + "Host: example.com\r\n" + "\r\n" + body + ); + + ASSERT_EQ(1u, capture.results.size()); + ASSERT_TRUE(capture.results.front()); + ASSERT_EQ(1u, capture.requests.size()); + EXPECT_STREQ("10", capture.requests.front().m_header_info.m_content_length.c_str()); + EXPECT_EQ(body, capture.requests.front().m_body); +} + +TEST(HTTP, Server_Parses_First_Header_After_Split_Request_Line) +{ + const std::string body = "0123456789"; + const auto capture = feed_http_request({ + "POST /json_rpc HTTP/1.1\r\n", + "Content-Length: 10\r\n" + "Host: example.com\r\n" + "\r\n" + body + }); + + ASSERT_EQ(2u, capture.results.size()); + EXPECT_TRUE(capture.results[0]); + EXPECT_TRUE(capture.results[1]); + ASSERT_EQ(1u, capture.requests.size()); + EXPECT_STREQ("10", capture.requests.front().m_header_info.m_content_length.c_str()); + EXPECT_EQ(body, capture.requests.front().m_body); +} + +TEST(HTTP, Server_Rejects_Malformed_First_Header) +{ + const auto capture = feed_http_request( + "GET / HTTP/1.1\r\n" + "Bad Header Without Colon\r\n" + "Host: example.com\r\n" + "\r\n" + ); + + ASSERT_EQ(1u, capture.results.size()); + EXPECT_FALSE(capture.results.front()); + EXPECT_TRUE(capture.requests.empty()); +} + +TEST(HTTP, Server_Rejects_Malformed_Later_Header) +{ + const auto capture = feed_http_request( + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "Bad Header Without Colon\r\n" + "\r\n" + ); + + ASSERT_EQ(1u, capture.results.size()); + EXPECT_FALSE(capture.results.front()); + EXPECT_TRUE(capture.requests.empty()); +} + +TEST(HTTP, Server_Keeps_Unknown_First_Header) +{ + const auto capture = feed_http_request( + "GET / HTTP/1.1\r\n" + "X-Test: abc\r\n" + "Host: example.com\r\n" + "\r\n" + ); + + ASSERT_EQ(1u, capture.results.size()); + ASSERT_TRUE(capture.results.front()); + ASSERT_EQ(1u, capture.requests.size()); + ASSERT_EQ(1u, capture.requests.front().m_header_info.m_etc_fields.size()); + EXPECT_STREQ("X-Test", capture.requests.front().m_header_info.m_etc_fields.front().first.c_str()); + EXPECT_STREQ("abc", capture.requests.front().m_header_info.m_etc_fields.front().second.c_str()); +} + +TEST(HTTP, Client_Keeps_Unknown_Header) +{ + test_http_client client; + const bool result = client.test( + "HTTP/1.1 200 OK\r\n" + "X-Test: abc\r\n" + "Content-Length: 0\r\n" + "\r\n", + std::chrono::milliseconds(1000) + ); + + ASSERT_TRUE(result); + EXPECT_EQ(1u, client.headers_seen); + ASSERT_EQ(1u, client.last_headers.m_header_info.m_etc_fields.size()); + EXPECT_STREQ("X-Test", client.last_headers.m_header_info.m_etc_fields.front().first.c_str()); + EXPECT_STREQ("abc", client.last_headers.m_header_info.m_etc_fields.front().second.c_str()); +} + +TEST(HTTP, Client_Rejects_Malformed_Response_Header) +{ + test_http_client client; + const bool result = client.test( + "HTTP/1.1 200 OK\r\n" + "Bad Header Without Colon\r\n" + "Content-Length: 0\r\n" + "\r\n", + std::chrono::milliseconds(1000) + ); + + EXPECT_FALSE(result); + EXPECT_EQ(0u, client.headers_seen); +} + TEST(HTTP, Add_Field) { std::string str{"leading text"}; |
