Drop Boost entirely from triangles-cli: use raw sockets for HTTP

Third time's the charm. After two CI failures chasing boost::asio / libboost_system
linking issues across platforms (Homebrew missing config on macOS, MSYS2 versioned
names on Windows, CMake targets that don't quite work everywhere), rip the whole
Boost dependency out of the CLI and use raw POSIX/Winsock sockets.

- triangles-cli.cpp: replaced boost::asio with raw socket() / connect() / send()
  / recv() / getaddrinfo(). Cross-platform: #ifdef _WIN32 for Winsock + WSAStartup
  / WSACleanup, else POSIX. ~100 lines of clean portable socket code.
- src/CMakeLists.txt: dropped find_package(Boost) entirely. Only links
  json_compat (header-only) + ws2_32 on Windows. No boost libs to find.

Should be the last fix needed for this PR.
This commit is contained in:
Krystie
2026-06-18 19:05:37 -07:00
parent 600b1cf35f
commit 569b541931
2 changed files with 146 additions and 89 deletions
+5 -13
View File
@@ -258,26 +258,18 @@ if(BUILD_CLI)
add_executable(triangles-cli add_executable(triangles-cli
triangles-cli.cpp triangles-cli.cpp
) )
# Boost: header-only asio via Boost::boost target (sets include dirs; # No Boost dependency: uses raw POSIX/Winsock sockets for HTTP. Only links
# available in Boost 1.83+). libboost_system is the small compiled # the json_compat header-only shim and the platform's native socket lib
# library that provides error_code/error_category symbols; link it # (Winsock ws2_32 on Windows; libc on POSIX). Keeps the binary small and
# explicitly by name to avoid needing the per-component CMake config # avoids per-platform Boost linking pain (MSYS2 uses versioned -mt- names;
# file (which Homebrew's boost formula doesn't ship for the system # Homebrew doesn't ship the boost_system CMake config).
# component on macOS).
find_package(Boost REQUIRED)
target_link_libraries(triangles-cli target_link_libraries(triangles-cli
PRIVATE PRIVATE
json_compat json_compat
Boost::boost
) )
# Link libboost_system per-platform by library name
if(UNIX OR WIN32)
target_link_libraries(triangles-cli PRIVATE boost_system)
endif()
if(WIN32) if(WIN32)
set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe") set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe")
# boost::asio needs ws2_32 on Windows
target_link_libraries(triangles-cli PRIVATE ws2_32) target_link_libraries(triangles-cli PRIVATE ws2_32)
endif() endif()
+141 -76
View File
@@ -10,9 +10,11 @@
// Build with -DBUILD_CLI=ON (default ON). // Build with -DBUILD_CLI=ON (default ON).
// //
// Self-contained: does NOT link util.cpp / wallet.cpp / net.cpp / triangles_common. // Self-contained: does NOT link util.cpp / wallet.cpp / net.cpp / triangles_common.
// Only links json_compat (nlohmann/json via json_spirit shim), boost (asio + // Only links json_compat (nlohmann/json via json_spirit shim) and the platform's
// program_options + filesystem + system), and OpenSSL (for base64). // native socket library (Winsock on Windows, libc on POSIX). No Boost dependency
// This keeps the CLI binary small (~600 KB stripped on Linux, ~1.5 MB on Windows). // at all — keeps the binary small and avoids platform-specific link problems
// with boost::asio / libboost_system (MSYS2 names them with -mt- versioned
// suffixes; Homebrew doesn't ship the CMake config for the system component).
// //
// Connection parameters (highest precedence first): // Connection parameters (highest precedence first):
// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport // 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport
@@ -38,9 +40,6 @@
#include "json/json_compat.h" #include "json/json_compat.h"
#include <boost/asio.hpp>
#include <boost/asio/streambuf.hpp>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
@@ -58,9 +57,32 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
// Cross-platform socket includes
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
using socket_t = SOCKET;
#define TRI_CLI_INVALID_SOCKET INVALID_SOCKET
#define TRI_CLI_CLOSE_SOCKET(s) closesocket(s)
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
using socket_t = int;
#define TRI_CLI_INVALID_SOCKET (-1)
#define TRI_CLI_CLOSE_SOCKET(s) close(s)
#endif
using namespace std; using namespace std;
namespace asio = boost::asio;
using boost::asio::ip::tcp;
namespace fs = std::filesystem; namespace fs = std::filesystem;
using namespace json_spirit; using namespace json_spirit;
@@ -92,17 +114,14 @@ static void ReadConfigFile(const string& path)
if (!f.good()) return; if (!f.good()) return;
string line; string line;
while (getline(f, line)) { while (getline(f, line)) {
// Strip CR (Windows) and leading whitespace
if (!line.empty() && line.back() == '\r') line.pop_back(); if (!line.empty() && line.back() == '\r') line.pop_back();
size_t start = line.find_first_not_of(" \t"); size_t start = line.find_first_not_of(" \t");
if (start == string::npos) continue; if (start == string::npos) continue;
if (line[start] == '#') continue; if (line[start] == '#') continue;
// Parse key = value
size_t eq = line.find('=', start); size_t eq = line.find('=', start);
if (eq == string::npos) continue; if (eq == string::npos) continue;
string key = line.substr(start, eq - start); string key = line.substr(start, eq - start);
string value = line.substr(eq + 1); string value = line.substr(eq + 1);
// Trim whitespace on both ends
auto trim = [](string& s) { auto trim = [](string& s) {
size_t a = s.find_first_not_of(" \t"); size_t a = s.find_first_not_of(" \t");
size_t b = s.find_last_not_of(" \t"); size_t b = s.find_last_not_of(" \t");
@@ -111,7 +130,6 @@ static void ReadConfigFile(const string& path)
}; };
trim(key); trim(key);
trim(value); trim(value);
// Strip surrounding quotes
if (value.size() >= 2 && if (value.size() >= 2 &&
((value.front() == '"' && value.back() == '"') || ((value.front() == '"' && value.back() == '"') ||
(value.front() == '\'' && value.back() == '\''))) { (value.front() == '\'' && value.back() == '\''))) {
@@ -125,25 +143,21 @@ static void ReadConfigFile(const string& path)
} }
} }
// Cross-platform default data directory (matches the daemon's path)
static fs::path GetDefaultDataDir() static fs::path GetDefaultDataDir()
{ {
#ifdef WIN32 #ifdef _WIN32
// %APPDATA%/CryptographicTriangles
const char* appdata = getenv("APPDATA"); const char* appdata = getenv("APPDATA");
if (appdata && *appdata) { if (appdata && *appdata) {
return fs::path(appdata) / "CryptographicTriangles"; return fs::path(appdata) / "CryptographicTriangles";
} }
return fs::path("C:/CryptographicTriangles"); return fs::path("C:/CryptographicTriangles");
#elif defined(__APPLE__) #elif defined(__APPLE__)
// ~/Library/Application Support/CryptographicTriangles
const char* home = getenv("HOME"); const char* home = getenv("HOME");
if (home && *home) { if (home && *home) {
return fs::path(home) / "Library/Application Support/CryptographicTriangles"; return fs::path(home) / "Library/Application Support/CryptographicTriangles";
} }
return fs::path("/tmp/CryptographicTriangles"); return fs::path("/tmp/CryptographicTriangles");
#else #else
// ~/.cryptographic-triangles (matches daemon's GetDefaultDataDir)
const char* home = getenv("HOME"); const char* home = getenv("HOME");
if (home && *home) { if (home && *home) {
return fs::path(home) / ".cryptographic-triangles"; return fs::path(home) / ".cryptographic-triangles";
@@ -171,7 +185,6 @@ static void ParseCommandLine(int argc, char* const argv[])
mapMultiArgs.clear(); mapMultiArgs.clear();
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
string str(argv[i]); string str(argv[i]);
// Bare "-" means: read remaining args from stdin
if (str == "-") { if (str == "-") {
mapMultiArgs["-"].push_back("-"); mapMultiArgs["-"].push_back("-");
continue; continue;
@@ -203,9 +216,6 @@ struct RPCConn {
static int AppInitRPCConn(RPCConn& conn) static int AppInitRPCConn(RPCConn& conn)
{ {
// Load conf file FIRST (before pulling creds) so defaults from triangles.conf
// are visible. Command-line flags (already in mapArgs) take precedence because
// ReadConfigFile only inserts when key is absent.
fs::path confPath = GetConfigFilePath(); fs::path confPath = GetConfigFilePath();
if (!confPath.empty()) ReadConfigFile(confPath.string()); if (!confPath.empty()) ReadConfigFile(confPath.string());
@@ -234,7 +244,6 @@ static Value ParseCLIParam(const string& arg)
return Value(string("")); return Value(string(""));
} }
Value v; Value v;
// Try parsing the arg as JSON. If it parses to a non-string literal, keep.
if (read_string(arg, v) && v.type() != str_type) { if (read_string(arg, v) && v.type() != str_type) {
return v; return v;
} }
@@ -247,7 +256,6 @@ static void ParseCommandLineRPCParams(int argc, char* const argv[],
method = Value(string("")); method = Value(string(""));
params.clear(); params.clear();
int i = 1; int i = 1;
// Skip leading flags
static const set<string> valFlags = { static const set<string> valFlags = {
"-conf", "-datadir", "-rpcconnect", "-rpcport", "-conf", "-datadir", "-rpcconnect", "-rpcport",
"-rpcuser", "-rpcpassword" "-rpcuser", "-rpcpassword"
@@ -309,12 +317,37 @@ static string Base64Encode(const string& in)
} }
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// HTTP/1.1 JSON-RPC POST (plaintext) // HTTP/1.1 JSON-RPC POST (plaintext) — using raw sockets (no Boost)
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
namespace {
class SocketInit {
public:
SocketInit() {
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
}
~SocketInit() {
#ifdef _WIN32
WSACleanup();
#endif
}
};
inline void close_socket(socket_t s) {
TRI_CLI_CLOSE_SOCKET(s);
}
} // namespace
static int CallRPC(const RPCConn& conn, const string& strMethod, static int CallRPC(const RPCConn& conn, const string& strMethod,
const Array& params, Value& result) const Array& params, Value& result)
{ {
SocketInit sockInit;
Object req; Object req;
req.push_back(Pair("jsonrpc", Value(string("1.0")))); req.push_back(Pair("jsonrpc", Value(string("1.0"))));
req.push_back(Pair("id", Value(string("triangles-cli")))); req.push_back(Pair("id", Value(string("triangles-cli"))));
@@ -324,81 +357,114 @@ static int CallRPC(const RPCConn& conn, const string& strMethod,
string strAuth = Base64Encode(conn.user + ":" + conn.pass); string strAuth = Base64Encode(conn.user + ":" + conn.pass);
asio::io_context io; // Resolve host:port via getaddrinfo
tcp::resolver resolver(io); struct addrinfo hints;
boost::system::error_code ec; memset(&hints, 0, sizeof(hints));
auto endpoints = resolver.resolve(conn.host, conn.port, ec); hints.ai_family = AF_UNSPEC;
if (ec) { hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
struct addrinfo* addrRes = nullptr;
int rc = getaddrinfo(conn.host.c_str(), conn.port.c_str(), &hints, &addrRes);
if (rc != 0 || addrRes == nullptr) {
cerr << "triangles-cli: resolve " << conn.host << ":" << conn.port cerr << "triangles-cli: resolve " << conn.host << ":" << conn.port
<< " failed: " << ec.message() << "\n"; << " failed: " << gai_strerror(rc) << "\n";
if (addrRes) freeaddrinfo(addrRes);
return 1; return 1;
} }
tcp::socket sock(io); // Try each resolved address until one connects
sock.connect(*endpoints.begin(), ec); socket_t sock = TRI_CLI_INVALID_SOCKET;
if (ec) { for (struct addrinfo* ai = addrRes; ai != nullptr; ai = ai->ai_next) {
sock = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (sock == TRI_CLI_INVALID_SOCKET) {
continue;
}
if (::connect(sock, ai->ai_addr, ai->ai_addrlen) == 0) {
break; // connected
}
close_socket(sock);
sock = TRI_CLI_INVALID_SOCKET;
}
freeaddrinfo(addrRes);
if (sock == TRI_CLI_INVALID_SOCKET) {
cerr << "triangles-cli: connect to " << conn.host << ":" << conn.port cerr << "triangles-cli: connect to " << conn.host << ":" << conn.port
<< " failed: " << ec.message() << "\n" << " failed\n"
<< "(is trianglesd running and accepting JSON-RPC?)\n"; << "(is trianglesd running and accepting JSON-RPC?)\n";
return 1; return 1;
} }
ostringstream reqStream; // Build HTTP/1.1 request
reqStream << "POST / HTTP/1.1\r\n" string reqData =
<< "Host: " << conn.host << ":" << conn.port << "\r\n" "POST / HTTP/1.1\r\n"
<< "Authorization: Basic " << strAuth << "\r\n" "Host: " + conn.host + ":" + conn.port + "\r\n"
<< "Content-Type: application/json\r\n" "Authorization: Basic " + strAuth + "\r\n"
<< "Content-Length: " << strRequest.size() << "\r\n" "Content-Type: application/json\r\n"
<< "Connection: close\r\n" "Content-Length: " + to_string(strRequest.size()) + "\r\n"
<< "\r\n" "Connection: close\r\n"
<< strRequest; "\r\n" + strRequest;
asio::streambuf requestBuf;
std::ostream os(&requestBuf); // Send
os << reqStream.str(); size_t totalSent = 0;
asio::write(sock, requestBuf, ec); while (totalSent < reqData.size()) {
if (ec) { ssize_t n = ::send(sock, reqData.data() + totalSent,
cerr << "triangles-cli: write failed: " << ec.message() << "\n"; reqData.size() - totalSent, 0);
return 1; if (n <= 0) {
cerr << "triangles-cli: write failed\n";
close_socket(sock);
return 1;
}
totalSent += static_cast<size_t>(n);
} }
asio::streambuf responseBuf; // Read full response (until EOF)
boost::system::error_code readEc; string respData;
while (asio::read(sock, responseBuf, char buf[4096];
asio::transfer_at_least(1), readEc)) { while (true) {
// keep reading until EOF or error ssize_t n = ::recv(sock, buf, sizeof(buf), 0);
} if (n > 0) {
if (readEc && readEc != asio::error::eof) { respData.append(buf, static_cast<size_t>(n));
cerr << "triangles-cli: read failed: " << readEc.message() << "\n"; } else if (n == 0) {
return 1; break; // EOF
} else {
// Error
#ifdef _WIN32
int err = WSAGetLastError();
if (err == WSAECONNRESET || err == WSAECONNABORTED) {
// Treat as EOF
break;
}
#else
if (errno == EINTR) continue; // interrupted, retry
if (errno == ECONNRESET) break; // peer closed
#endif
cerr << "triangles-cli: read failed\n";
close_socket(sock);
return 1;
}
} }
close_socket(sock);
std::istream rs(&responseBuf); // Parse status line
string line; size_t hdrEnd = respData.find("\r\n\r\n");
if (!std::getline(rs, line)) { if (hdrEnd == string::npos) {
cerr << "triangles-cli: empty response\n"; cerr << "triangles-cli: malformed response (no header terminator)\n";
return 1; return 1;
} }
if (!line.empty() && line.back() == '\r') line.pop_back(); string statusLine = respData.substr(0, respData.find("\r\n"));
int status = 0; int status = 0;
{ {
istringstream iss(line); istringstream iss(statusLine);
string httpVer; string httpVer;
iss >> httpVer >> status; iss >> httpVer >> status;
} }
if (status != 200) { if (status != 200) {
cerr << "triangles-cli: server returned HTTP " << status << "\n"; cerr << "triangles-cli: server returned HTTP " << status << "\n";
ostringstream body; string body = respData.substr(hdrEnd + 4);
body << rs.rdbuf(); if (!body.empty()) cerr << body << "\n";
if (!body.str().empty()) cerr << body.str() << "\n";
return 1; return 1;
} }
while (std::getline(rs, line) && line != "\r" && !line.empty()) {} string body = respData.substr(hdrEnd + 4);
string body;
{
ostringstream oss;
oss << rs.rdbuf();
body = oss.str();
}
Value reply; Value reply;
if (!read_string(body, reply)) { if (!read_string(body, reply)) {
@@ -533,7 +599,6 @@ int main(int argc, char* argv[])
CommandLineHelp(cout); CommandLineHelp(cout);
return 0; return 0;
} }
// else fall through: delegate to daemon's help <command>
} }
if (!GetArg("-getinfo", "").empty()) { if (!GetArg("-getinfo", "").empty()) {