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
triangles-cli.cpp
)
# Boost: header-only asio via Boost::boost target (sets include dirs;
# available in Boost 1.83+). libboost_system is the small compiled
# library that provides error_code/error_category symbols; link it
# explicitly by name to avoid needing the per-component CMake config
# file (which Homebrew's boost formula doesn't ship for the system
# component on macOS).
find_package(Boost REQUIRED)
# No Boost dependency: uses raw POSIX/Winsock sockets for HTTP. Only links
# the json_compat header-only shim and the platform's native socket lib
# (Winsock ws2_32 on Windows; libc on POSIX). Keeps the binary small and
# avoids per-platform Boost linking pain (MSYS2 uses versioned -mt- names;
# Homebrew doesn't ship the boost_system CMake config).
target_link_libraries(triangles-cli
PRIVATE
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)
set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe")
# boost::asio needs ws2_32 on Windows
target_link_libraries(triangles-cli PRIVATE ws2_32)
endif()
+141 -76
View File
@@ -10,9 +10,11 @@
// Build with -DBUILD_CLI=ON (default ON).
//
// 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 +
// program_options + filesystem + system), and OpenSSL (for base64).
// This keeps the CLI binary small (~600 KB stripped on Linux, ~1.5 MB on Windows).
// Only links json_compat (nlohmann/json via json_spirit shim) and the platform's
// native socket library (Winsock on Windows, libc on POSIX). No Boost dependency
// 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):
// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport
@@ -38,9 +40,6 @@
#include "json/json_compat.h"
#include <boost/asio.hpp>
#include <boost/asio/streambuf.hpp>
#include <filesystem>
#include <algorithm>
@@ -58,9 +57,32 @@
#include <utility>
#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;
namespace asio = boost::asio;
using boost::asio::ip::tcp;
namespace fs = std::filesystem;
using namespace json_spirit;
@@ -92,17 +114,14 @@ static void ReadConfigFile(const string& path)
if (!f.good()) return;
string line;
while (getline(f, line)) {
// Strip CR (Windows) and leading whitespace
if (!line.empty() && line.back() == '\r') line.pop_back();
size_t start = line.find_first_not_of(" \t");
if (start == string::npos) continue;
if (line[start] == '#') continue;
// Parse key = value
size_t eq = line.find('=', start);
if (eq == string::npos) continue;
string key = line.substr(start, eq - start);
string value = line.substr(eq + 1);
// Trim whitespace on both ends
auto trim = [](string& s) {
size_t a = s.find_first_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(value);
// Strip surrounding quotes
if (value.size() >= 2 &&
((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()
{
#ifdef WIN32
// %APPDATA%/CryptographicTriangles
#ifdef _WIN32
const char* appdata = getenv("APPDATA");
if (appdata && *appdata) {
return fs::path(appdata) / "CryptographicTriangles";
}
return fs::path("C:/CryptographicTriangles");
#elif defined(__APPLE__)
// ~/Library/Application Support/CryptographicTriangles
const char* home = getenv("HOME");
if (home && *home) {
return fs::path(home) / "Library/Application Support/CryptographicTriangles";
}
return fs::path("/tmp/CryptographicTriangles");
#else
// ~/.cryptographic-triangles (matches daemon's GetDefaultDataDir)
const char* home = getenv("HOME");
if (home && *home) {
return fs::path(home) / ".cryptographic-triangles";
@@ -171,7 +185,6 @@ static void ParseCommandLine(int argc, char* const argv[])
mapMultiArgs.clear();
for (int i = 1; i < argc; ++i) {
string str(argv[i]);
// Bare "-" means: read remaining args from stdin
if (str == "-") {
mapMultiArgs["-"].push_back("-");
continue;
@@ -203,9 +216,6 @@ struct RPCConn {
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();
if (!confPath.empty()) ReadConfigFile(confPath.string());
@@ -234,7 +244,6 @@ static Value ParseCLIParam(const string& arg)
return Value(string(""));
}
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) {
return v;
}
@@ -247,7 +256,6 @@ static void ParseCommandLineRPCParams(int argc, char* const argv[],
method = Value(string(""));
params.clear();
int i = 1;
// Skip leading flags
static const set<string> valFlags = {
"-conf", "-datadir", "-rpcconnect", "-rpcport",
"-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,
const Array& params, Value& result)
{
SocketInit sockInit;
Object req;
req.push_back(Pair("jsonrpc", Value(string("1.0"))));
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);
asio::io_context io;
tcp::resolver resolver(io);
boost::system::error_code ec;
auto endpoints = resolver.resolve(conn.host, conn.port, ec);
if (ec) {
// Resolve host:port via getaddrinfo
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
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
<< " failed: " << ec.message() << "\n";
<< " failed: " << gai_strerror(rc) << "\n";
if (addrRes) freeaddrinfo(addrRes);
return 1;
}
tcp::socket sock(io);
sock.connect(*endpoints.begin(), ec);
if (ec) {
// Try each resolved address until one connects
socket_t sock = TRI_CLI_INVALID_SOCKET;
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
<< " failed: " << ec.message() << "\n"
<< " failed\n"
<< "(is trianglesd running and accepting JSON-RPC?)\n";
return 1;
}
ostringstream reqStream;
reqStream << "POST / HTTP/1.1\r\n"
<< "Host: " << conn.host << ":" << conn.port << "\r\n"
<< "Authorization: Basic " << strAuth << "\r\n"
<< "Content-Type: application/json\r\n"
<< "Content-Length: " << strRequest.size() << "\r\n"
<< "Connection: close\r\n"
<< "\r\n"
<< strRequest;
asio::streambuf requestBuf;
std::ostream os(&requestBuf);
os << reqStream.str();
asio::write(sock, requestBuf, ec);
if (ec) {
cerr << "triangles-cli: write failed: " << ec.message() << "\n";
return 1;
// Build HTTP/1.1 request
string reqData =
"POST / HTTP/1.1\r\n"
"Host: " + conn.host + ":" + conn.port + "\r\n"
"Authorization: Basic " + strAuth + "\r\n"
"Content-Type: application/json\r\n"
"Content-Length: " + to_string(strRequest.size()) + "\r\n"
"Connection: close\r\n"
"\r\n" + strRequest;
// Send
size_t totalSent = 0;
while (totalSent < reqData.size()) {
ssize_t n = ::send(sock, reqData.data() + totalSent,
reqData.size() - totalSent, 0);
if (n <= 0) {
cerr << "triangles-cli: write failed\n";
close_socket(sock);
return 1;
}
totalSent += static_cast<size_t>(n);
}
asio::streambuf responseBuf;
boost::system::error_code readEc;
while (asio::read(sock, responseBuf,
asio::transfer_at_least(1), readEc)) {
// keep reading until EOF or error
}
if (readEc && readEc != asio::error::eof) {
cerr << "triangles-cli: read failed: " << readEc.message() << "\n";
return 1;
// Read full response (until EOF)
string respData;
char buf[4096];
while (true) {
ssize_t n = ::recv(sock, buf, sizeof(buf), 0);
if (n > 0) {
respData.append(buf, static_cast<size_t>(n));
} else if (n == 0) {
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);
string line;
if (!std::getline(rs, line)) {
cerr << "triangles-cli: empty response\n";
// Parse status line
size_t hdrEnd = respData.find("\r\n\r\n");
if (hdrEnd == string::npos) {
cerr << "triangles-cli: malformed response (no header terminator)\n";
return 1;
}
if (!line.empty() && line.back() == '\r') line.pop_back();
string statusLine = respData.substr(0, respData.find("\r\n"));
int status = 0;
{
istringstream iss(line);
istringstream iss(statusLine);
string httpVer;
iss >> httpVer >> status;
}
if (status != 200) {
cerr << "triangles-cli: server returned HTTP " << status << "\n";
ostringstream body;
body << rs.rdbuf();
if (!body.str().empty()) cerr << body.str() << "\n";
string body = respData.substr(hdrEnd + 4);
if (!body.empty()) cerr << body << "\n";
return 1;
}
while (std::getline(rs, line) && line != "\r" && !line.empty()) {}
string body;
{
ostringstream oss;
oss << rs.rdbuf();
body = oss.str();
}
string body = respData.substr(hdrEnd + 4);
Value reply;
if (!read_string(body, reply)) {
@@ -533,7 +599,6 @@ int main(int argc, char* argv[])
CommandLineHelp(cout);
return 0;
}
// else fall through: delegate to daemon's help <command>
}
if (!GetArg("-getinfo", "").empty()) {