Merge bitcoin/bitcoin#34342: cli: Replace libevent usage with simple http client

d61053d97b build: Drop libevent from bitcoin-cli link libraries (Fabian Jahr)
798d051c80 cli: Remove libevent usage (Fabian Jahr)
376e7ef07c util: Expose IOErrorIsPermanent in sock header (Fabian Jahr)
5d562430de netbase: Add timeout parameter to ConnectDirectly (Fabian Jahr)
a988ac592f cli: Add HTTPResponseHeaders class for parsing response headers (Fabian Jahr)
c471c5085b common: Add unused UrlEncode function (Fabian Jahr)
9687ef1bd9 ci: 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-ACK d61053d97b
  theStack:
    re-ACK d61053d97b
  w0xlt:
    ACK d61053d97b

Tree-SHA512: a3580a45faf540ee844aac8cb1dc056a89e8e11b45781d2807baa4736d5c0934284c6066206101b6984111a48a186d67845545d07639b623cb35ccc2d85d3ab2
This commit is contained in:
merge-script
2026-05-22 10:04:33 +01:00
13 changed files with 520 additions and 160 deletions

View File

@@ -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:

View File

@@ -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()

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 {};
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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.
*/

View File

@@ -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:

View File

@@ -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):