mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-05-27 22:34:18 +02:00
Merge bitcoin/bitcoin#34342: cli: Replace libevent usage with simple http client
d61053d97bbuild: Drop libevent from bitcoin-cli link libraries (Fabian Jahr)798d051c80cli: Remove libevent usage (Fabian Jahr)376e7ef07cutil: Expose IOErrorIsPermanent in sock header (Fabian Jahr)5d562430denetbase: Add timeout parameter to ConnectDirectly (Fabian Jahr)a988ac592fcli: Add HTTPResponseHeaders class for parsing response headers (Fabian Jahr)c471c5085bcommon: Add unused UrlEncode function (Fabian Jahr)9687ef1bd9ci: Tolerate unused free functions in intermediate commits (Fabian Jahr) Pull request description: Part of the effort to remove the libevent dependency altogether, see #31194 This takes the parsing logic from the [`HTTPHeaders` class](d549f01caa) from #32061 and puts it into `bitcoin-cli` as a small `HTTPResponseHeaders` class with a comment to revisit potentially sharing this code somehow. This decoupled the two pulls which seems like the most sensible way to deal with this since the actual overlap is very small compared to the impact of each of the pulls which should ideally not block each other. Otherwise the change itself replaces the libevent-based HTTP client with a simple synchronous implementation which uses the `Sock` class directly. ACKs for top commit: hodlinator: re-ACKd61053d97btheStack: re-ACKd61053d97bw0xlt: ACKd61053d97bTree-SHA512: a3580a45faf540ee844aac8cb1dc056a89e8e11b45781d2807baa4736d5c0934284c6066206101b6984111a48a186d67845545d07639b623cb35ccc2d85d3ab2
This commit is contained in:
4
.github/ci-test-each-commit-exec.py
vendored
4
.github/ci-test-each-commit-exec.py
vendored
@@ -40,8 +40,8 @@ def main():
|
||||
"-DCMAKE_BUILD_TYPE=Debug",
|
||||
"-DCMAKE_COMPILE_WARNING_AS_ERROR=ON",
|
||||
"--preset=dev-mode",
|
||||
# Tolerate unused member functions in intermediate commits in a pull request
|
||||
"-DCMAKE_CXX_FLAGS=-Wno-error=unused-member-function",
|
||||
# Tolerate unused (member) functions in intermediate commits in a pull request
|
||||
"-DCMAKE_CXX_FLAGS=-Wno-error=unused-member-function -Wno-error=unused-function",
|
||||
])
|
||||
|
||||
if run(["cmake", "--build", build_dir, "-j", str(num_procs)], check=False).returncode != 0:
|
||||
|
||||
@@ -363,8 +363,6 @@ if(BUILD_CLI)
|
||||
bitcoin_common
|
||||
bitcoin_ipc
|
||||
bitcoin_util
|
||||
libevent::core
|
||||
libevent::extra
|
||||
)
|
||||
install_binary_component(bitcoin-cli HAS_MANPAGE)
|
||||
endif()
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
#include <common/args.h>
|
||||
#include <common/license_info.h>
|
||||
#include <common/system.h>
|
||||
#include <common/url.h>
|
||||
#include <compat/compat.h>
|
||||
#include <compat/stdin.h>
|
||||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
#include <interfaces/rpc.h>
|
||||
#include <netbase.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <rpc/client.h>
|
||||
#include <rpc/mining.h>
|
||||
@@ -24,7 +26,9 @@
|
||||
#include <univalue.h>
|
||||
#include <util/chaintype.h>
|
||||
#include <util/exception.h>
|
||||
#include <util/sock.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
#include <util/time.h>
|
||||
#include <util/translation.h>
|
||||
|
||||
@@ -33,19 +37,19 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
#include <support/events.h>
|
||||
|
||||
using util::Join;
|
||||
using util::ToString;
|
||||
|
||||
@@ -75,6 +79,65 @@ static const std::string DEFAULT_NBLOCKS = "1";
|
||||
/** Default -color setting. */
|
||||
static const std::string DEFAULT_COLOR_SETTING{"auto"};
|
||||
|
||||
struct HTTPError : std::runtime_error {
|
||||
explicit inline HTTPError(const std::string& msg) : std::runtime_error(msg) {}
|
||||
};
|
||||
|
||||
/** Parses the headers of an HTTP response.
|
||||
*
|
||||
* May be replaced by the corresponding methods in HTTPHeaders from
|
||||
* https://github.com/bitcoin/bitcoin/pull/35182 once that class is in a
|
||||
* shared location.
|
||||
*/
|
||||
class HTTPResponseHeaders
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> m_headers;
|
||||
|
||||
public:
|
||||
void Read(util::LineReader& reader);
|
||||
std::optional<std::string> FindFirst(std::string_view key) const;
|
||||
};
|
||||
|
||||
// Named Read() in HTTPHeaders (see PR #35182).
|
||||
void HTTPResponseHeaders::Read(util::LineReader& reader)
|
||||
{
|
||||
// Headers https://httpwg.org/specs/rfc9110.html#rfc.section.6.3
|
||||
// A sequence of Field Lines https://httpwg.org/specs/rfc9110.html#rfc.section.5.2
|
||||
while (auto maybe_line = reader.ReadLine()) {
|
||||
const std::string& line = *maybe_line;
|
||||
|
||||
// An empty line indicates end of the headers section https://www.rfc-editor.org/rfc/rfc2616#section-4
|
||||
if (line.empty()) return;
|
||||
|
||||
// Header line must have at least one ":"
|
||||
// keys are not allowed to have delimiters like ":" but values are
|
||||
// https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.2
|
||||
const size_t pos{line.find(':')};
|
||||
if (pos == std::string::npos) throw HTTPError{"Header missing colon (:)"};
|
||||
|
||||
// Whitespace is optional
|
||||
std::string key = util::TrimString(std::string_view(line).substr(0, pos));
|
||||
std::string value = util::TrimString(std::string_view(line).substr(pos + 1));
|
||||
|
||||
// Header keys are Field Names: https://httpwg.org/specs/rfc9110.html#fields.names
|
||||
// which consist of "tokens": https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.2
|
||||
// that can not be empty.
|
||||
if (key.empty()) throw HTTPError{"Empty header name"};
|
||||
|
||||
m_headers.emplace_back(std::move(key), std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> HTTPResponseHeaders::FindFirst(std::string_view key) const
|
||||
{
|
||||
for (const auto& item : m_headers) {
|
||||
if (CaseInsensitiveEqual(key, item.first)) {
|
||||
return item.second;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static void SetupCliArgs(ArgsManager& argsman)
|
||||
{
|
||||
SetupHelpOptions(argsman);
|
||||
@@ -124,15 +187,6 @@ std::optional<std::string> RpcWalletName(const ArgsManager& args)
|
||||
return args.GetArg("-rpcwallet");
|
||||
}
|
||||
|
||||
/** libevent event log callback */
|
||||
static void libevent_log_cb(int severity, const char *msg)
|
||||
{
|
||||
// Ignore everything other than errors
|
||||
if (severity >= EVENT_LOG_ERR) {
|
||||
throw std::runtime_error(strprintf("libevent error: %s", msg));
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Exception thrown on connection error. This error is used to determine
|
||||
// when to wait if -rpcwait is given.
|
||||
@@ -200,68 +254,12 @@ static int AppInitRPC(int argc, char* argv[])
|
||||
return CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
|
||||
/** Reply structure for request_done to fill in */
|
||||
struct HTTPReply
|
||||
struct HTTPResponse
|
||||
{
|
||||
HTTPReply() = default;
|
||||
|
||||
int status{0};
|
||||
int error{-1};
|
||||
std::string body;
|
||||
};
|
||||
|
||||
static std::string http_errorstring(int code)
|
||||
{
|
||||
switch(code) {
|
||||
case EVREQ_HTTP_TIMEOUT:
|
||||
return "timeout reached";
|
||||
case EVREQ_HTTP_EOF:
|
||||
return "EOF reached";
|
||||
case EVREQ_HTTP_INVALID_HEADER:
|
||||
return "error while reading header, or invalid header";
|
||||
case EVREQ_HTTP_BUFFER_ERROR:
|
||||
return "error encountered while reading or writing";
|
||||
case EVREQ_HTTP_REQUEST_CANCEL:
|
||||
return "request was canceled";
|
||||
case EVREQ_HTTP_DATA_TOO_LONG:
|
||||
return "response body is larger than allowed";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void http_request_done(struct evhttp_request *req, void *ctx)
|
||||
{
|
||||
HTTPReply *reply = static_cast<HTTPReply*>(ctx);
|
||||
|
||||
if (req == nullptr) {
|
||||
/* If req is nullptr, it means an error occurred while connecting: the
|
||||
* error code will have been passed to http_error_cb.
|
||||
*/
|
||||
reply->status = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
reply->status = evhttp_request_get_response_code(req);
|
||||
|
||||
struct evbuffer *buf = evhttp_request_get_input_buffer(req);
|
||||
if (buf)
|
||||
{
|
||||
size_t size = evbuffer_get_length(buf);
|
||||
const char *data = (const char*)evbuffer_pullup(buf, size);
|
||||
if (data)
|
||||
reply->body = std::string(data, size);
|
||||
evbuffer_drain(buf, size);
|
||||
}
|
||||
}
|
||||
|
||||
static void http_error_cb(enum evhttp_request_error err, void *ctx)
|
||||
{
|
||||
HTTPReply *reply = static_cast<HTTPReply*>(ctx);
|
||||
reply->error = err;
|
||||
}
|
||||
|
||||
static int8_t NetworkStringToId(const std::string& str)
|
||||
{
|
||||
for (size_t i = 0; i < NETWORKS.size(); ++i) {
|
||||
@@ -832,6 +830,328 @@ static std::optional<UniValue> CallIPC(BaseRequestHandler* rh, const std::string
|
||||
return rh->ProcessReply(reply);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple synchronous HTTP client using Sock class.
|
||||
*/
|
||||
class HTTPClient
|
||||
{
|
||||
public:
|
||||
static HTTPClient Connect(const std::string& host, uint16_t port, std::chrono::seconds timeout);
|
||||
|
||||
HTTPResponse Post(const std::string& endpoint,
|
||||
std::span<const std::pair<std::string, std::string>> headers,
|
||||
const std::string& body);
|
||||
|
||||
private:
|
||||
// Signal that the peer closed the connection cleanly. Used in the read-until-close fallback.
|
||||
struct RecvEOF : CConnectionFailed { using CConnectionFailed::CConnectionFailed; };
|
||||
|
||||
std::unique_ptr<Sock> m_socket;
|
||||
std::string m_host;
|
||||
std::chrono::seconds m_timeout;
|
||||
|
||||
HTTPClient(std::unique_ptr<Sock>&& socket, const std::string& host, std::chrono::seconds timeout)
|
||||
: m_socket(std::move(socket)), m_host(host), m_timeout(timeout) {}
|
||||
bool SendRequest(std::string_view request);
|
||||
HTTPResponse ReadResponse();
|
||||
std::optional<std::string> Recv(std::chrono::time_point<std::chrono::steady_clock> deadline);
|
||||
};
|
||||
|
||||
HTTPClient HTTPClient::Connect(const std::string& host, uint16_t port, std::chrono::seconds timeout)
|
||||
{
|
||||
std::vector<CService> services = Lookup(host, port, /*fAllowLookup=*/true, /*nMaxSolutions=*/256);
|
||||
if (services.empty()) {
|
||||
throw CConnectionFailed(strprintf("Could not resolve host: %s", host));
|
||||
}
|
||||
|
||||
const auto deadline{std::chrono::steady_clock::now() + timeout};
|
||||
for (const CService& service : services) {
|
||||
const auto time_left{std::chrono::duration_cast<std::chrono::milliseconds>(deadline - std::chrono::steady_clock::now())};
|
||||
if (time_left.count() <= 0) break;
|
||||
|
||||
auto sock = ConnectDirectly(service, /*manual_connection=*/true, time_left);
|
||||
if (sock) return HTTPClient{std::move(sock), host, timeout};
|
||||
}
|
||||
|
||||
throw CConnectionFailed{"Could not connect to the server"};
|
||||
}
|
||||
|
||||
HTTPResponse HTTPClient::Post(const std::string& endpoint,
|
||||
std::span<const std::pair<std::string, std::string>> headers,
|
||||
const std::string& body)
|
||||
{
|
||||
try {
|
||||
// Build HTTP request
|
||||
std::string request = strprintf("POST %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Length: %d\r\n",
|
||||
endpoint, m_host, body.size());
|
||||
|
||||
for (const auto& [name, value] : headers) {
|
||||
request += strprintf("%s: %s\r\n", name, value);
|
||||
}
|
||||
request += "\r\n";
|
||||
request += body;
|
||||
|
||||
if (!SendRequest(request)) {
|
||||
throw CConnectionFailed("Failed to send HTTP request");
|
||||
}
|
||||
|
||||
return ReadResponse();
|
||||
} catch (const HTTPError& e) {
|
||||
throw CConnectionFailed(strprintf("HTTP error: %s", e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
bool HTTPClient::SendRequest(std::string_view request)
|
||||
{
|
||||
const auto deadline{std::chrono::steady_clock::now() + m_timeout};
|
||||
|
||||
while (!request.empty()) {
|
||||
Sock::Event event{0};
|
||||
auto time_left = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
deadline - std::chrono::steady_clock::now());
|
||||
if (time_left.count() <= 0 || !m_socket->Wait(time_left, Sock::SEND, &event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(event & Sock::SEND)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ssize_t sent = m_socket->Send(request.data(), request.size(), MSG_NOSIGNAL);
|
||||
if (sent < 0) {
|
||||
int err = WSAGetLastError();
|
||||
if (!IOErrorIsPermanent(err)) {
|
||||
std::this_thread::yield();
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
request.remove_prefix(sent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
HTTPResponse HTTPClient::ReadResponse()
|
||||
{
|
||||
HTTPResponse response;
|
||||
std::string buffer;
|
||||
const auto deadline{std::chrono::steady_clock::now() + m_timeout};
|
||||
|
||||
// Read data until we have complete headers
|
||||
size_t headers_end = 0;
|
||||
|
||||
while (headers_end == 0) {
|
||||
if (auto result{Recv(deadline)}) {
|
||||
buffer.append(*result);
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for header terminator
|
||||
size_t pos = buffer.find("\r\n\r\n");
|
||||
if (pos != std::string::npos) {
|
||||
headers_end = pos + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse http status
|
||||
util::LineReader reader(std::string_view{buffer.data(), headers_end}, headers_end);
|
||||
auto status_line = reader.ReadLine();
|
||||
if (!status_line) {
|
||||
throw HTTPError{"Failed to read status line"};
|
||||
}
|
||||
|
||||
const std::string& status_str = *status_line;
|
||||
// Minimum status line is "HTTP/X.Y NNN" (e.g. "HTTP/1.1 200"), 12 characters.
|
||||
if (status_str.size() < 12 || !status_str.starts_with("HTTP/")) {
|
||||
throw HTTPError{"Invalid status line"};
|
||||
}
|
||||
|
||||
size_t space1 = status_str.find(' ');
|
||||
if (space1 == std::string::npos || space1 + 4 > status_str.size()) {
|
||||
throw HTTPError{"Invalid status line format"};
|
||||
}
|
||||
|
||||
std::string status_code_str = status_str.substr(space1 + 1, 3);
|
||||
auto status_code = ToIntegral<int>(status_code_str);
|
||||
if (!status_code) {
|
||||
throw HTTPError{"Invalid status code"};
|
||||
}
|
||||
response.status = *status_code;
|
||||
|
||||
HTTPResponseHeaders headers;
|
||||
headers.Read(reader);
|
||||
|
||||
// Determine body length
|
||||
size_t content_length = 0;
|
||||
bool chunked = false;
|
||||
|
||||
// RFC 9112 §6.3 says responses with both Transfer-Encoding and Content-Length
|
||||
// must be rejected. We are more lenient: Transfer-Encoding takes precedence
|
||||
// and Content-Length is ignored.
|
||||
auto transfer_encoding = headers.FindFirst("transfer-encoding");
|
||||
if (transfer_encoding && ToLower(*transfer_encoding).find("chunked") != std::string::npos) {
|
||||
chunked = true;
|
||||
} else {
|
||||
auto content_length_header = headers.FindFirst("content-length");
|
||||
if (content_length_header) {
|
||||
auto maybe_len = ToIntegral<size_t>(*content_length_header);
|
||||
if (!maybe_len) {
|
||||
throw HTTPError{"Invalid Content-Length"};
|
||||
}
|
||||
content_length = *maybe_len;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove headers data from buffer, so only initial body data remains
|
||||
buffer.erase(0, headers_end);
|
||||
|
||||
// Read remaining body
|
||||
if (chunked) {
|
||||
// Handle chunked transfer encoding
|
||||
std::string body;
|
||||
|
||||
while (true) {
|
||||
// Try to parse a chunk from current buffer
|
||||
std::string_view chunk_data{buffer};
|
||||
size_t line_end = chunk_data.find("\r\n");
|
||||
|
||||
if (line_end != std::string::npos) {
|
||||
// Parse chunk size
|
||||
std::string_view size_str = chunk_data.substr(0, line_end);
|
||||
// Ignore chunk extensions
|
||||
size_t semi = size_str.find(';');
|
||||
if (semi != std::string::npos) {
|
||||
size_str = size_str.substr(0, semi);
|
||||
}
|
||||
|
||||
const auto chunk_size{ToIntegral<uint64_t>(util::TrimStringView(size_str), /*base=*/16)};
|
||||
if (!chunk_size) {
|
||||
throw HTTPError{"Invalid chunk size"};
|
||||
}
|
||||
|
||||
if (*chunk_size == 0) {
|
||||
// Allow (but ignore) Chunked Trailer section, by
|
||||
// reading CRLF-terminated lines until we read an empty line,
|
||||
// which indicates the end of this response.
|
||||
// See https://httpwg.org/specs/rfc9112.html#rfc.section.7.1.2
|
||||
buffer.erase(0, line_end + 2);
|
||||
while (true) {
|
||||
size_t crlf_pos = buffer.find("\r\n");
|
||||
if (crlf_pos == std::string::npos) {
|
||||
// Need more data
|
||||
if (auto result{Recv(deadline)}) {
|
||||
buffer.append(*result);
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
buffer.erase(0, crlf_pos + 2);
|
||||
if (crlf_pos == 0) break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we have the full chunk
|
||||
size_t chunk_start = line_end + 2;
|
||||
if (*chunk_size > std::numeric_limits<size_t>::max() - chunk_start - 2) {
|
||||
throw HTTPError{"Chunk size too large"};
|
||||
}
|
||||
size_t chunk_end = chunk_start + *chunk_size + 2; // +2 for trailing CRLF
|
||||
|
||||
if (buffer.size() >= chunk_end) {
|
||||
// Extract chunk data
|
||||
body.append(buffer, chunk_start, *chunk_size);
|
||||
|
||||
// Remove processed data
|
||||
buffer.erase(0, chunk_end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Need more data
|
||||
while (true) {
|
||||
if (auto result{Recv(deadline)}) {
|
||||
buffer.append(*result);
|
||||
break;
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.body = std::move(body);
|
||||
} else if (content_length > 0) {
|
||||
// Fixed content length
|
||||
while (buffer.size() < content_length) {
|
||||
if (auto result{Recv(deadline)}) {
|
||||
buffer.append(*result);
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
// Possibly shrink buffer in case we got a larger response than
|
||||
// originally specified.
|
||||
buffer.resize(content_length);
|
||||
response.body = std::move(buffer);
|
||||
} else {
|
||||
// No Content-Length and not chunked: read until the peer closes the
|
||||
// connection (RFC 9112 §6.3, HTTP/1.0 fallback).
|
||||
try {
|
||||
while (true) {
|
||||
if (auto result{Recv(deadline)}) {
|
||||
buffer.append(*result);
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
} catch (const RecvEOF&) {}
|
||||
response.body = std::move(buffer);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
std::optional<std::string> HTTPClient::Recv(const std::chrono::time_point<std::chrono::steady_clock> deadline)
|
||||
{
|
||||
auto wait_for_readable{[this](std::chrono::milliseconds timeout) -> bool {
|
||||
Sock::Event event{0};
|
||||
if (!m_socket->Wait(timeout, Sock::RECV, &event)) {
|
||||
return false;
|
||||
}
|
||||
return (event & Sock::RECV) != 0;
|
||||
}};
|
||||
|
||||
auto time_left = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
deadline - std::chrono::steady_clock::now());
|
||||
if (time_left.count() <= 0 || !wait_for_readable(time_left)) {
|
||||
throw CConnectionFailed{"timeout"};
|
||||
}
|
||||
|
||||
char recv_buf[4096];
|
||||
ssize_t nrecv = m_socket->Recv(recv_buf, sizeof(recv_buf), /*flags=*/0);
|
||||
|
||||
if (nrecv < 0) {
|
||||
int err = WSAGetLastError();
|
||||
if (!IOErrorIsPermanent(err)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
throw CConnectionFailed{strprintf("Read error: %s", NetworkErrorString(err))};
|
||||
}
|
||||
|
||||
if (nrecv == 0) {
|
||||
throw RecvEOF{"EOF"};
|
||||
}
|
||||
|
||||
return std::string{recv_buf, static_cast<size_t>(nrecv)};
|
||||
}
|
||||
|
||||
static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const std::string& endpoint, const std::string& username)
|
||||
{
|
||||
std::string host;
|
||||
@@ -876,34 +1196,16 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain event base
|
||||
raii_event_base base = obtain_event_base();
|
||||
|
||||
// Synchronously look up hostname
|
||||
raii_evhttp_connection evcon = obtain_evhttp_connection_base(base.get(), host, port);
|
||||
|
||||
// Set connection timeout
|
||||
{
|
||||
const int timeout = gArgs.GetIntArg("-rpcclienttimeout", DEFAULT_HTTP_CLIENT_TIMEOUT);
|
||||
if (timeout > 0) {
|
||||
evhttp_connection_set_timeout(evcon.get(), timeout);
|
||||
} else {
|
||||
// Indefinite request timeouts are not possible in libevent-http, so we
|
||||
// set the timeout to a very long time period instead.
|
||||
|
||||
constexpr int YEAR_IN_SECONDS = 31556952; // Average length of year in Gregorian calendar
|
||||
evhttp_connection_set_timeout(evcon.get(), 5 * YEAR_IN_SECONDS);
|
||||
}
|
||||
const int timeout = gArgs.GetIntArg("-rpcclienttimeout", DEFAULT_HTTP_CLIENT_TIMEOUT);
|
||||
std::chrono::seconds timeout_duration;
|
||||
if (timeout > 0) {
|
||||
timeout_duration = std::chrono::seconds(timeout);
|
||||
} else {
|
||||
// Use 5 year timeout for "indefinite"
|
||||
timeout_duration = std::chrono::years(5);
|
||||
}
|
||||
|
||||
HTTPReply response;
|
||||
raii_evhttp_request req = obtain_evhttp_request(http_request_done, (void*)&response);
|
||||
if (req == nullptr) {
|
||||
throw std::runtime_error("create http request failed");
|
||||
}
|
||||
|
||||
evhttp_request_set_error_cb(req.get(), http_error_cb);
|
||||
|
||||
// Get credentials
|
||||
std::string rpc_credentials;
|
||||
std::optional<AuthCookieResult> auth_cookie_result;
|
||||
@@ -914,36 +1216,25 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
|
||||
rpc_credentials = username + ":" + gArgs.GetArg("-rpcpassword", "");
|
||||
}
|
||||
|
||||
struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get());
|
||||
assert(output_headers);
|
||||
evhttp_add_header(output_headers, "Host", host.c_str());
|
||||
evhttp_add_header(output_headers, "Connection", "close");
|
||||
evhttp_add_header(output_headers, "Content-Type", "application/json");
|
||||
evhttp_add_header(output_headers, "Authorization", (std::string("Basic ") + EncodeBase64(rpc_credentials)).c_str());
|
||||
|
||||
// Attach request data
|
||||
const std::pair<std::string, std::string> headers[]{
|
||||
{"Content-Type", "application/json"},
|
||||
{"Authorization", "Basic " + EncodeBase64(rpc_credentials)},
|
||||
};
|
||||
std::string strRequest = rh->PrepareRequest(strMethod, args).write() + "\n";
|
||||
struct evbuffer* output_buffer = evhttp_request_get_output_buffer(req.get());
|
||||
assert(output_buffer);
|
||||
evbuffer_add(output_buffer, strRequest.data(), strRequest.size());
|
||||
|
||||
int r = evhttp_make_request(evcon.get(), req.release(), EVHTTP_REQ_POST, endpoint.c_str());
|
||||
if (r != 0) {
|
||||
throw CConnectionFailed("send http request failed");
|
||||
}
|
||||
|
||||
event_base_dispatch(base.get());
|
||||
|
||||
if (response.status == 0) {
|
||||
std::string responseErrorMessage;
|
||||
if (response.error != -1) {
|
||||
responseErrorMessage = strprintf(" (error code %d - \"%s\")", response.error, http_errorstring(response.error));
|
||||
}
|
||||
throw CConnectionFailed(strprintf("Could not connect to the server %s:%d%s\n\n"
|
||||
HTTPResponse response;
|
||||
try {
|
||||
HTTPClient client{HTTPClient::Connect(host, port, timeout_duration)};
|
||||
response = client.Post(endpoint, headers, strRequest);
|
||||
} catch (const CConnectionFailed& e) {
|
||||
const std::string formatted_error{*e.what() ? strprintf(" (%s)", e.what()) : ""};
|
||||
throw CConnectionFailed(strprintf("Error while attempting to communicate with server %s:%d%s\n\n"
|
||||
"Make sure the bitcoind server is running and that you are connecting to the correct RPC port.\n"
|
||||
"Use \"bitcoin-cli -help\" for more info.",
|
||||
host, port, responseErrorMessage));
|
||||
} else if (response.status == HTTP_UNAUTHORIZED) {
|
||||
host, port, formatted_error));
|
||||
}
|
||||
|
||||
if (response.status == HTTP_UNAUTHORIZED) {
|
||||
std::string error{"Authorization failed: "};
|
||||
if (auth_cookie_result.has_value()) {
|
||||
switch (*auth_cookie_result) {
|
||||
@@ -1000,13 +1291,7 @@ static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& str
|
||||
// check if we should use a special wallet endpoint
|
||||
std::string endpoint = "/";
|
||||
if (rpcwallet) {
|
||||
char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false);
|
||||
if (encodedURI) {
|
||||
endpoint = "/wallet/" + std::string(encodedURI);
|
||||
free(encodedURI);
|
||||
} else {
|
||||
throw CConnectionFailed("uri-encode failed");
|
||||
}
|
||||
endpoint = "/wallet/" + UrlEncode(*rpcwallet);
|
||||
}
|
||||
|
||||
std::string username{gArgs.GetArg("-rpcuser", "")};
|
||||
@@ -1027,8 +1312,10 @@ static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& str
|
||||
} catch (const CConnectionFailed& e) {
|
||||
if (fWait && (timeout <= 0 || std::chrono::steady_clock::now() < deadline)) {
|
||||
UninterruptibleSleep(1s);
|
||||
} else {
|
||||
} else if (fWait) {
|
||||
throw CConnectionFailed(strprintf("timeout on transient error: %s", e.what()));
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
} while (fWait);
|
||||
@@ -1387,7 +1674,6 @@ MAIN_FUNCTION
|
||||
tfm::format(std::cerr, "Error: Initializing networking failed\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
event_set_log_callback(&libevent_log_cb);
|
||||
|
||||
try {
|
||||
int ret = AppInitRPC(argc, argv);
|
||||
|
||||
@@ -37,3 +37,25 @@ std::string UrlDecode(std::string_view url_encoded)
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string UrlEncode(std::string_view str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size() * 3); // worst case: every char needs encoding
|
||||
|
||||
for (char ch : str) {
|
||||
auto c = static_cast<unsigned char>(ch);
|
||||
// Unreserved characters per RFC 3986, Section 2.3
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
res += ch;
|
||||
} else {
|
||||
// Percent-encode all other characters
|
||||
res += '%';
|
||||
constexpr char hex_chars[] = "0123456789ABCDEF";
|
||||
res += hex_chars[(c >> 4) & 0xF];
|
||||
res += hex_chars[c & 0xF];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,7 @@
|
||||
*/
|
||||
std::string UrlDecode(std::string_view url_encoded);
|
||||
|
||||
/* Encode a URL. */
|
||||
std::string UrlEncode(std::string_view str);
|
||||
|
||||
#endif // BITCOIN_COMMON_URL_H
|
||||
|
||||
@@ -587,7 +587,12 @@ static void LogConnectFailure(bool manual_connection, util::ConstevalFormatStrin
|
||||
}
|
||||
}
|
||||
|
||||
static bool ConnectToSocket(const Sock& sock, struct sockaddr* sockaddr, socklen_t len, const std::string& dest_str, bool manual_connection)
|
||||
static bool ConnectToSocket(const Sock& sock,
|
||||
struct sockaddr* sockaddr,
|
||||
socklen_t len,
|
||||
const std::string& dest_str,
|
||||
bool manual_connection,
|
||||
std::chrono::milliseconds timeout)
|
||||
{
|
||||
// Connect to `sockaddr` using `sock`.
|
||||
if (sock.Connect(sockaddr, len) == SOCKET_ERROR) {
|
||||
@@ -600,7 +605,7 @@ static bool ConnectToSocket(const Sock& sock, struct sockaddr* sockaddr, socklen
|
||||
// synchronously to check for successful connection with a timeout.
|
||||
const Sock::Event requested = Sock::RECV | Sock::SEND;
|
||||
Sock::Event occurred;
|
||||
if (!sock.Wait(std::chrono::milliseconds{nConnectTimeout}, requested, &occurred)) {
|
||||
if (!sock.Wait(timeout, requested, &occurred)) {
|
||||
LogInfo("wait for connect to %s failed: %s\n",
|
||||
dest_str,
|
||||
NetworkErrorString(WSAGetLastError()));
|
||||
@@ -643,6 +648,13 @@ static bool ConnectToSocket(const Sock& sock, struct sockaddr* sockaddr, socklen
|
||||
}
|
||||
|
||||
std::unique_ptr<Sock> ConnectDirectly(const CService& dest, bool manual_connection)
|
||||
{
|
||||
return ConnectDirectly(dest, manual_connection, std::chrono::milliseconds{nConnectTimeout});
|
||||
}
|
||||
|
||||
std::unique_ptr<Sock> ConnectDirectly(const CService& dest,
|
||||
bool manual_connection,
|
||||
std::chrono::milliseconds timeout)
|
||||
{
|
||||
auto sock = CreateSock(dest.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP);
|
||||
if (!sock) {
|
||||
@@ -658,7 +670,7 @@ std::unique_ptr<Sock> ConnectDirectly(const CService& dest, bool manual_connecti
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!ConnectToSocket(*sock, (struct sockaddr*)&sockaddr, len, dest.ToStringAddrPort(), manual_connection)) {
|
||||
if (!ConnectToSocket(*sock, (struct sockaddr*)&sockaddr, len, dest.ToStringAddrPort(), manual_connection, timeout)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -687,7 +699,12 @@ std::unique_ptr<Sock> Proxy::Connect() const
|
||||
memcpy(addrun.sun_path, path.c_str(), std::min(sizeof(addrun.sun_path) - 1, path.length()));
|
||||
socklen_t len = sizeof(addrun);
|
||||
|
||||
if(!ConnectToSocket(*sock, (struct sockaddr*)&addrun, len, path, /*manual_connection=*/true)) {
|
||||
if (!ConnectToSocket(*sock,
|
||||
(struct sockaddr*)&addrun,
|
||||
len,
|
||||
path,
|
||||
/*manual_connection=*/true,
|
||||
std::chrono::milliseconds{nConnectTimeout})) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <util/sock.h>
|
||||
#include <util/threadinterrupt.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -305,6 +306,11 @@ extern std::function<std::unique_ptr<Sock>(int, int, int)> CreateSock;
|
||||
*/
|
||||
std::unique_ptr<Sock> ConnectDirectly(const CService& dest, bool manual_connection);
|
||||
|
||||
/** Create a socket and try to connect to the specified service, using the provided timeout. */
|
||||
std::unique_ptr<Sock> ConnectDirectly(const CService& dest,
|
||||
bool manual_connection,
|
||||
std::chrono::milliseconds timeout);
|
||||
|
||||
/**
|
||||
* Connect to a specified destination service through a SOCKS5 proxy by first
|
||||
* connecting to the SOCKS5 proxy.
|
||||
|
||||
@@ -24,8 +24,6 @@ typedef std::unique_ptr<struct type, type##_deleter> raii_##type
|
||||
MAKE_RAII(event_base);
|
||||
MAKE_RAII(event);
|
||||
MAKE_RAII(evhttp);
|
||||
MAKE_RAII(evhttp_request);
|
||||
MAKE_RAII(evhttp_connection);
|
||||
|
||||
inline raii_event_base obtain_event_base() {
|
||||
auto result = raii_event_base(event_base_new());
|
||||
@@ -42,15 +40,4 @@ inline raii_evhttp obtain_evhttp(struct event_base* base) {
|
||||
return raii_evhttp(evhttp_new(base));
|
||||
}
|
||||
|
||||
inline raii_evhttp_request obtain_evhttp_request(void(*cb)(struct evhttp_request *, void *), void *arg) {
|
||||
return raii_evhttp_request(evhttp_request_new(cb, arg));
|
||||
}
|
||||
|
||||
inline raii_evhttp_connection obtain_evhttp_connection_base(struct event_base* base, std::string host, uint16_t port) {
|
||||
auto result = raii_evhttp_connection(evhttp_connection_base_new(base, nullptr, host.c_str(), port));
|
||||
if (!result.get())
|
||||
throw std::runtime_error("create connection failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif // BITCOIN_SUPPORT_EVENTS_H
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <common/url.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
@@ -72,4 +73,44 @@ BOOST_AUTO_TEST_CASE(decode_internal_nulls_test) {
|
||||
BOOST_CHECK_EQUAL(UrlDecode("abc%00%00"), result2);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(url_encode) {
|
||||
auto to_encode = std::string_view{
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
|
||||
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
|
||||
"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
|
||||
"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
|
||||
"\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
|
||||
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
|
||||
"\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
|
||||
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
|
||||
"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
|
||||
"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
|
||||
"\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
|
||||
"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
|
||||
"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
|
||||
"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
|
||||
"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff",
|
||||
256};
|
||||
auto expected_encoded =
|
||||
"%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F"
|
||||
"%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F"
|
||||
"%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F"
|
||||
"0123456789%3A%3B%3C%3D%3E%3F"
|
||||
"%40ABCDEFGHIJKLMNO"
|
||||
"PQRSTUVWXYZ%5B%5C%5D%5E_"
|
||||
"%60abcdefghijklmno"
|
||||
"pqrstuvwxyz%7B%7C%7D~%7F"
|
||||
"%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F"
|
||||
"%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F"
|
||||
"%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF"
|
||||
"%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF"
|
||||
"%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF"
|
||||
"%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF"
|
||||
"%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF"
|
||||
"%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF";
|
||||
BOOST_CHECK_EQUAL(UrlEncode(to_encode), expected_encoded);
|
||||
BOOST_CHECK_EQUAL(UrlDecode(expected_encoded), to_encode);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -26,11 +26,6 @@
|
||||
#include <poll.h>
|
||||
#endif
|
||||
|
||||
static inline bool IOErrorIsPermanent(int err)
|
||||
{
|
||||
return err != WSAEAGAIN && err != WSAEINTR && err != WSAEWOULDBLOCK && err != WSAEINPROGRESS;
|
||||
}
|
||||
|
||||
Sock::Sock(SOCKET s) : m_socket(s) {}
|
||||
|
||||
Sock::Sock(Sock&& other)
|
||||
|
||||
@@ -23,6 +23,11 @@ class CThreadInterrupt;
|
||||
*/
|
||||
static constexpr auto MAX_WAIT_FOR_IO = 1s;
|
||||
|
||||
inline bool IOErrorIsPermanent(int err)
|
||||
{
|
||||
return err != WSAEAGAIN && err != WSAEINTR && err != WSAEWOULDBLOCK && err != WSAEINPROGRESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* RAII helper class that manages a socket and closes it automatically when it goes out of scope.
|
||||
*/
|
||||
|
||||
@@ -177,9 +177,9 @@ class TestBitcoinCli(BitcoinTestFramework):
|
||||
conf_rpcport = "rpcport=" + str(node_rpc_port)
|
||||
self.nodes[0].replace_in_config([(conf_rpcport, "#" + conf_rpcport)])
|
||||
# prefer rpcport over rpcconnect
|
||||
assert_raises_process_error(1, "Could not connect to the server 127.0.0.1:1", self.nodes[0].cli(f"-rpcconnect=127.0.0.1:{node_rpc_port}", "-rpcport=1").echo)
|
||||
assert_raises_process_error(1, "Error while attempting to communicate with server 127.0.0.1:1 (Could not connect to the server)", self.nodes[0].cli(f"-rpcconnect=127.0.0.1:{node_rpc_port}", "-rpcport=1").echo)
|
||||
if have_ipv6:
|
||||
assert_raises_process_error(1, "Could not connect to the server ::1:1", self.nodes[0].cli(f"-rpcconnect=[::1]:{node_rpc_port}", "-rpcport=1").echo)
|
||||
assert_raises_process_error(1, "Error while attempting to communicate with server ::1:1 (Could not connect to the server)", self.nodes[0].cli(f"-rpcconnect=[::1]:{node_rpc_port}", "-rpcport=1").echo)
|
||||
|
||||
assert_equal(BLOCKS, self.nodes[0].cli("-rpcconnect=127.0.0.1:18999", f'-rpcport={node_rpc_port}').getblockcount())
|
||||
if have_ipv6:
|
||||
|
||||
@@ -41,8 +41,8 @@ class TestBitcoinIpcCli(BitcoinTestFramework):
|
||||
self.log.info("Skipping a few checks because temporary directory path is too long")
|
||||
|
||||
http_auth_error = "error: Authorization failed: Incorrect rpcuser or rpcpassword were specified."
|
||||
http_connect_error = f"error: timeout on transient error: Could not connect to the server 127.0.0.1:{rpc_port(node.index)}\n\nMake sure the bitcoind server is running and that you are connecting to the correct RPC port.\nUse \"bitcoin-cli -help\" for more info.\n"
|
||||
ipc_connect_error = "error: timeout on transient error: Connection refused\n\nProbably bitcoin-node is not running or not listening on a unix socket. Can be started with:\n\n bitcoin-node -chain=regtest -ipcbind=unix\n"
|
||||
http_connect_error = f"error: Error while attempting to communicate with server 127.0.0.1:{rpc_port(node.index)} (Could not connect to the server)\n\nMake sure the bitcoind server is running and that you are connecting to the correct RPC port.\nUse \"bitcoin-cli -help\" for more info.\n"
|
||||
ipc_connect_error = "error: Connection refused\n\nProbably bitcoin-node is not running or not listening on a unix socket. Can be started with:\n\n bitcoin-node -chain=regtest -ipcbind=unix\n"
|
||||
ipc_http_conflict = "error: -rpcconnect and -ipcconnect options cannot both be enabled\n"
|
||||
|
||||
for started in (True, False):
|
||||
|
||||
Reference in New Issue
Block a user