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:
+5
-13
@@ -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
@@ -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()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user