From 3ed9a42b3c9f97dca7c53690d65be28854d1549f Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Sun, 15 Mar 2026 23:33:09 -0700 Subject: [PATCH] v5.1.6: Fix block sync stall for fresh nodes, REST API refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fix: revert initial sync from getheaders back to getblocks. The headers-first change (05b1fd1) broke chain continuation — after downloading the first 2000 blocks, fresh nodes would stall because the getheaders path has no orphan-based continuation mechanism. The getblocks/inv/orphan cycle is required for full chain sync. Also adds getblocks fallback to the headers handler so if headers are used via other paths, sync still continues. Other changes: - Extract REST API into separate rest.cpp/rest.h - Add REST rate limiting and CORS support Co-Authored-By: Claude Opus 4.6 --- .github/workflows/build-all.yml | 2 +- Makefile | 2 +- src/clientversion.h | 2 +- src/init.cpp | 36 +- src/main.cpp | 7 +- src/makefile.mingw | 1 + src/makefile.unix | 1 + src/net.cpp | 60 ++- src/rest.cpp | 925 ++++++++++++++++++++++++++++++++ src/rest.h | 35 ++ src/trianglesrpc.cpp | 201 +------ src/version.h | 4 +- triangles-qt.pro | 2 + 13 files changed, 1071 insertions(+), 207 deletions(-) create mode 100644 src/rest.cpp create mode 100644 src/rest.h diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 37466c6..bae1c9b 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: env: - VERSION: "5.1.5" + VERSION: "5.1.6" jobs: build-windows-qt: diff --git a/Makefile b/Makefile index b875961..908d863 100644 --- a/Makefile +++ b/Makefile @@ -261,7 +261,7 @@ distclean: release-distclean debug-distclean FORCE -$(DEL_FILE) .qmake.stash E:/repos/triangles/src/leveldb/libleveldb.a: FORCE - cd E:/repos/triangles/src/leveldb && CC=gcc CXX=g++ TARGET_OS=OS_WINDOWS_CROSSCOMPILE $(MAKE) OPT="-fpermissive -O2" libleveldb.a libmemenv.a && ranlib E:/repos/triangles/src/leveldb/libleveldb.a && ranlib E:/repos/triangles/src/leveldb/libmemenv.a + cd E:/repos/triangles/src/leveldb && CC=gcc CXX=g++ TARGET_OS=OS_WINDOWS_CROSSCOMPILE $(MAKE) OPT="-fno-keep-inline-dllexport -march=nocona -msahf -mtune=generic -Wa,-mbig-obj -O2" libleveldb.a libmemenv.a && ranlib E:/repos/triangles/src/leveldb/libleveldb.a && ranlib E:/repos/triangles/src/leveldb/libmemenv.a release-mocclean: $(MAKE) -f $(MAKEFILE).Release mocclean diff --git a/src/clientversion.h b/src/clientversion.h index ff636d8..8ff6c03 100644 --- a/src/clientversion.h +++ b/src/clientversion.h @@ -8,7 +8,7 @@ // These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it #define CLIENT_VERSION_MAJOR 5 #define CLIENT_VERSION_MINOR 1 -#define CLIENT_VERSION_REVISION 5 +#define CLIENT_VERSION_REVISION 6 #define CLIENT_VERSION_BUILD 0 // Converts the parameter X to a string after macro replacement on X has been performed. diff --git a/src/init.cpp b/src/init.cpp index dadb35d..b1d31eb 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -403,6 +403,12 @@ std::string HelpMessage() " -rpcsslprivatekeyfile= " + _("Server private key (default: server.pem)") + "\n" + " -rpcsslciphers= " + _("Acceptable ciphers (default: TLSv1+HIGH:!SSLv2:!aNULL:!eNULL:!AH:!3DES:@STRENGTH)") + "\n" + + "\n" + _("REST API options:") + "\n" + + " -rest " + _("Enable public REST API on RPC port (default: 0)") + "\n" + + " -restcorsorigin= " + _("CORS Access-Control-Allow-Origin header (default: *)") + "\n" + + " -restapikey= " + _("Bearer token for authenticated wallet endpoints") + "\n" + + " -restratelimit= " + _("Max requests/sec per IP for public endpoints (default: 30, 0=disabled)") + "\n" + + "\n" + _("Secure messaging options:") + "\n" + " -nosmsg " + _("Disable secure messaging.") + "\n" + " -debugsmsg " + _("Log extra debug messages.") + "\n" + @@ -694,24 +700,26 @@ bool AppInit2() //if (nSocksVersion != 4 && nSocksVersion != 5) // return InitError(strprintf(_("Unknown -socks proxy version requested: %i"), nSocksVersion)); - do { + // Network selection: enable all networks (IPv4, IPv6, Tor) + // Tor is always enabled; clearnet is also allowed for seed node discovery + // Users can restrict to Tor-only with -onlynet=tor + if (mapArgs.count("-onlynet")) { std::set nets; - - - - - nets.insert(NET_TOR); - + BOOST_FOREACH(std::string snet, mapMultiArgs["-onlynet"]) { + enum Network net = ParseNetwork(snet); + if (net == NET_UNROUTABLE) + return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet.c_str())); + nets.insert(net); + } for (int n = 0; n < NET_MAX; n++) { enum Network net = (enum Network)n; if (!nets.count(net)) SetLimited(net); } - } while (false); + } - - CService addrOnion; - // need to move onion_port to a header + // Tor proxy: always configured for .onion connectivity + CService addrOnion; unsigned short const onion_port = 19099; if (mapArgs.count("-tor") && mapArgs["-tor"] != "0") { @@ -722,10 +730,8 @@ bool AppInit2() addrOnion = CService("127.0.0.1", onion_port); } - if (true) { - SetProxy(NET_TOR, addrOnion, 5); - SetReachable(NET_TOR); - } + SetProxy(NET_TOR, addrOnion, 5); + SetReachable(NET_TOR); // see Step 2: parameter interactions for more information about these fNoListen = !GetBoolArg("-listen", true); diff --git a/src/main.cpp b/src/main.cpp index accdf4f..f09ada8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3226,7 +3226,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) (nAskedForBlocks < 1 || vNodes.size() <= 1)) { nAskedForBlocks++; - pfrom->PushGetHeaders(pindexBest, uint256(0)); + pfrom->PushGetBlocks(pindexBest, uint256(0)); } // Relay alerts @@ -3599,6 +3599,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) if (nRequested > 0 && fDebug) printf("requested %d blocks from headers announcement\n", nRequested); + + // If we received a full batch, continue sync via getblocks + // (the getblocks/inv/orphan cycle handles chain continuation) + if (vHeaders.size() >= 2000) + pfrom->PushGetBlocks(pindexBest, uint256(0)); } diff --git a/src/makefile.mingw b/src/makefile.mingw index 8710033..57671dd 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -114,6 +114,7 @@ OBJS= \ obj/net_bootstrap.o \ obj/protocol.o \ obj/trianglesrpc.o \ + obj/rest.o \ obj/rpcdump.o \ obj/rpcnet.o \ obj/rpcmining.o \ diff --git a/src/makefile.unix b/src/makefile.unix index 11c78af..b53780a 100644 --- a/src/makefile.unix +++ b/src/makefile.unix @@ -149,6 +149,7 @@ OBJS= \ obj/net_bootstrap.o \ obj/protocol.o \ obj/trianglesrpc.o \ + obj/rest.o \ obj/rpcdump.o \ obj/rpcnet.o \ obj/rpcmining.o \ diff --git a/src/net.cpp b/src/net.cpp index 40b8bac..45beebd 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -40,6 +40,7 @@ void ThreadOpenAddedConnections2(void* parg); #ifdef USE_UPNP void ThreadMapPort2(void* parg); #endif +void ThreadDNSAddressSeed(void* parg); void ThreadDNSAddressSeed2(void* parg); bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false); @@ -1375,6 +1376,7 @@ void ThreadOnionSeed(void* parg) unsigned int pnSeed[] = { 0xCE58E9C2, // DNS2-OpenClaw: 194.233.88.206 + 0x13A7D04A, // DNS3-Sami: 74.208.167.19 }; void DumpAddresses() @@ -1416,6 +1418,58 @@ void ThreadDumpAddress(void* parg) printf("ThreadDumpAddress exited\n"); } +void ThreadDNSAddressSeed2(void* parg) +{ + static const char* strDNSSeed[] = { + "seed1.cryptographic-triangles.org", + "seed2.cryptographic-triangles.org", + "seed3.cryptographic-triangles.org", + "backup-seed.cryptographic-triangles.org", + }; + + printf("Loading addresses from DNS seeds...\n"); + int found = 0; + + for (unsigned int seed_idx = 0; seed_idx < ARRAYLEN(strDNSSeed); seed_idx++) + { + if (fShutdown) + return; + + vector vaddr; + if (LookupHost(strDNSSeed[seed_idx], vaddr)) + { + BOOST_FOREACH(CNetAddr& ip, vaddr) + { + CAddress addr(CService(ip, GetDefaultPort())); + addr.nTime = GetTime() - 3*24*60*60; // 3 days ago + addrman.Add(addr, CNetAddr(strDNSSeed[seed_idx], true)); + found++; + } + } + } + + printf("%d addresses found from DNS seeds\n", found); +} + +void ThreadDNSAddressSeed(void* parg) +{ + RenameThread("Triangles-dnsseed"); + try + { + vnThreadsRunning[THREAD_DNSSEED]++; + ThreadDNSAddressSeed2(parg); + vnThreadsRunning[THREAD_DNSSEED]--; + } + catch (std::exception& e) { + vnThreadsRunning[THREAD_DNSSEED]--; + PrintException(&e, "ThreadDNSAddressSeed()"); + } catch (...) { + vnThreadsRunning[THREAD_DNSSEED]--; + PrintException(NULL, "ThreadDNSAddressSeed()"); + } + printf("ThreadDNSAddressSeed exited\n"); +} + void ThreadOpenConnections(void* parg) { // Make this thread recognisable as the connection opening thread @@ -2030,9 +2084,9 @@ void StartNode(void* parg) if (fUseUPnP) MapPort(); - // Get addresses from IRC and advertise ours - //if (!NewThread(ThreadIRCSeed, NULL)) - // printf("Error: NewThread(ThreadIRCSeed) failed\n"); + // DNS seed lookup + if (!NewThread(ThreadDNSAddressSeed, NULL)) + printf("Error: NewThread(ThreadDNSAddressSeed) failed\n"); // Send and receive from sockets, accept connections if (!NewThread(ThreadSocketHandler, NULL)) diff --git a/src/rest.cpp b/src/rest.cpp new file mode 100644 index 0000000..ac9d343 --- /dev/null +++ b/src/rest.cpp @@ -0,0 +1,925 @@ +// Copyright (c) 2024 The Triangles developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "rest.h" +#include "trianglesrpc.h" +#include "main.h" +#include "sync.h" +#include "util.h" +#include "base58.h" +#include "addressindex.h" +#include "wallet.h" +#include "init.h" + +#include +#include + +using namespace std; +using namespace json_spirit; + +// Forward declarations from rpcblockchain.cpp +extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail); +extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry); + +// Forward declaration from trianglesrpc.cpp +extern bool HTTPAuthorized(map& mapHeaders); +extern string rfc1123Time(); + +// ============================================================================ +// Rate limiter +// ============================================================================ + +struct IPRateLimiter +{ + int64_t nTokens; + int64_t nLastRefill; + + IPRateLimiter() : nTokens(0), nLastRefill(0) {} + + bool Allow(int64_t nMaxTokens, int64_t nRefillRate) + { + int64_t nNow = GetTime(); + if (nLastRefill == 0) { + nLastRefill = nNow; + nTokens = nMaxTokens; + } + // Refill tokens + int64_t nElapsed = nNow - nLastRefill; + if (nElapsed > 0) { + nTokens += nElapsed * nRefillRate; + if (nTokens > nMaxTokens) + nTokens = nMaxTokens; + nLastRefill = nNow; + } + if (nTokens > 0) { + nTokens--; + return true; + } + return false; + } +}; + +static CCriticalSection cs_rateLimiter; +static map mapRateLimiters; +static int64_t nLastCleanup = 0; + +bool CheckRESTRateLimit(const string& strIP) +{ + int64_t nLimit = GetArg("-restratelimit", 30); + int64_t nBurst = nLimit * 2; // burst = 2x sustained rate + + if (nLimit <= 0) + return true; // rate limiting disabled + + LOCK(cs_rateLimiter); + + // Periodic cleanup of stale entries (every 60s) + int64_t nNow = GetTime(); + if (nNow - nLastCleanup > 60) { + map::iterator it = mapRateLimiters.begin(); + while (it != mapRateLimiters.end()) { + if (nNow - it->second.nLastRefill > 300) // 5 min stale + mapRateLimiters.erase(it++); + else + ++it; + } + nLastCleanup = nNow; + } + + return mapRateLimiters[strIP].Allow(nBurst, nLimit); +} + +// ============================================================================ +// HTTP response helpers +// ============================================================================ + +string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType) +{ + string strCorsOrigin = GetArg("-restcorsorigin", "*"); + + const char *cStatus; + if (nStatus == 200) cStatus = "OK"; + else if (nStatus == 204) cStatus = "No Content"; + else if (nStatus == 400) cStatus = "Bad Request"; + else if (nStatus == 401) cStatus = "Unauthorized"; + else if (nStatus == 403) cStatus = "Forbidden"; + else if (nStatus == 404) cStatus = "Not Found"; + else if (nStatus == 429) cStatus = "Too Many Requests"; + else if (nStatus == 500) cStatus = "Internal Server Error"; + else if (nStatus == 503) cStatus = "Service Unavailable"; + else cStatus = ""; + + return strprintf( + "HTTP/1.1 %d %s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "Content-Length: %" PRIszu "\r\n" + "Content-Type: %s\r\n" + "Access-Control-Allow-Origin: %s\r\n" + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, Authorization\r\n" + "Access-Control-Max-Age: 86400\r\n" + "Server: Triangles/%s\r\n" + "\r\n" + "%s", + nStatus, + cStatus, + rfc1123Time().c_str(), + strMsg.size(), + contentType.c_str(), + strCorsOrigin.c_str(), + FormatFullVersion().c_str(), + strMsg.c_str()); +} + +// ============================================================================ +// URL parsing +// ============================================================================ + +static void ParseRESTPath(const string& strURI, vector& parts, map& queryParams) +{ + string path = strURI; + + // Split query string + size_t qpos = path.find('?'); + string queryString; + if (qpos != string::npos) { + queryString = path.substr(qpos + 1); + path = path.substr(0, qpos); + } + + // Split path into parts + boost::split(parts, path, boost::is_any_of("/")); + + // Parse query parameters + if (!queryString.empty()) { + vector pairs; + boost::split(pairs, queryString, boost::is_any_of("&")); + for (size_t i = 0; i < pairs.size(); i++) { + size_t eq = pairs[i].find('='); + if (eq != string::npos) + queryParams[pairs[i].substr(0, eq)] = pairs[i].substr(eq + 1); + } + } +} + +bool IsRESTPath(const string& strURI) +{ + return strURI.size() >= 6 && strURI.substr(0, 6) == "/rest/"; +} + +// ============================================================================ +// Auth helpers +// ============================================================================ + +static bool RESTAuthorized(map& mapHeaders) +{ + // Check Bearer token first (if -restapikey is set) + string strApiKey = GetArg("-restapikey", ""); + if (!strApiKey.empty()) { + string strAuth = mapHeaders.count("authorization") ? mapHeaders["authorization"] : ""; + if (strAuth.substr(0, 7) == "Bearer ") { + string strToken = strAuth.substr(7); + boost::trim(strToken); + if (TimingResistantEqual(strToken, strApiKey)) + return true; + } + } + + // Fall back to Basic Auth (same as RPC) + if (mapHeaders.count("authorization")) + return HTTPAuthorized(mapHeaders); + + return false; +} + +// ============================================================================ +// JSON error helper +// ============================================================================ + +static string RESTError(const string& message, int code = -1) +{ + Object obj; + obj.push_back(Pair("error", message)); + if (code != -1) + obj.push_back(Pair("code", code)); + return write_string(Value(obj), false) + "\n"; +} + +// ============================================================================ +// Helper: call an RPC method and return JSON string +// ============================================================================ + +static bool CallRPCMethod(const string& method, const Array& params, + string& strReply, int& nStatus) +{ + try { + Value result = tableRPC.execute(method, params); + strReply = write_string(result, false) + "\n"; + nStatus = HTTP_OK; + return true; + } + catch (Object& objError) { + int code = find_value(objError, "code").get_int(); + string msg = find_value(objError, "message").get_str(); + if (code == RPC_METHOD_NOT_FOUND) nStatus = HTTP_NOT_FOUND; + else if (code == RPC_WALLET_UNLOCK_NEEDED) nStatus = HTTP_FORBIDDEN; + else if (code == RPC_INVALID_PARAMETER || code == RPC_INVALID_ADDRESS_OR_KEY) nStatus = HTTP_BAD_REQUEST; + else nStatus = HTTP_INTERNAL_SERVER_ERROR; + strReply = RESTError(msg, code); + return true; + } + catch (std::exception& e) { + nStatus = HTTP_INTERNAL_SERVER_ERROR; + strReply = RESTError(e.what()); + return true; + } +} + +// ============================================================================ +// Public endpoint handlers +// ============================================================================ + +// GET /rest/chaininfo +static bool HandleChainInfo(string& strReply, int& nStatus) +{ + LOCK(cs_main); + Object obj, diff; + obj.push_back(Pair("chain", fTestNet ? string("test") : string("main"))); + obj.push_back(Pair("blocks", (int)nBestHeight)); + obj.push_back(Pair("bestblockhash", hashBestChain.GetHex())); + diff.push_back(Pair("proof-of-work", GetDifficulty())); + diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true)))); + obj.push_back(Pair("difficulty", diff)); + obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply))); + strReply = write_string(Value(obj), false) + "\n"; + nStatus = HTTP_OK; + return true; +} + +// GET /rest/block/{hash_or_param}[.hex] +static bool HandleBlock(const string& param, const string& format, string& strReply, int& nStatus) +{ + LOCK(cs_main); + uint256 hash(param); + if (mapBlockIndex.count(hash) == 0) { + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Block not found"); + return true; + } + CBlock block; + CBlockIndex* pblockindex = mapBlockIndex[hash]; + block.ReadFromDisk(pblockindex, true); + + if (format == "hex") { + CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION); + ssBlock << block; + strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n"; + } else { + Object obj = blockToJSON(block, pblockindex, false); + strReply = write_string(Value(obj), false) + "\n"; + } + nStatus = HTTP_OK; + return true; +} + +// GET /rest/blockheader/{hash} +static bool HandleBlockHeader(const string& param, string& strReply, int& nStatus) +{ + LOCK(cs_main); + uint256 hash(param); + if (mapBlockIndex.count(hash) == 0) { + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Block not found"); + return true; + } + CBlockIndex* pblockindex = mapBlockIndex[hash]; + Object result; + result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex())); + result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1)); + result.push_back(Pair("height", pblockindex->nHeight)); + result.push_back(Pair("version", pblockindex->nVersion)); + result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex())); + result.push_back(Pair("time", (boost::int64_t)pblockindex->GetBlockTime())); + result.push_back(Pair("nonce", (boost::uint64_t)pblockindex->nNonce)); + result.push_back(Pair("bits", HexBits(pblockindex->nBits))); + result.push_back(Pair("difficulty", GetDifficulty(pblockindex))); + result.push_back(Pair("flags", strprintf("%s%s", + pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work", + pblockindex->GeneratedStakeModifier() ? " stake-modifier" : ""))); + if (pblockindex->pprev) + result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex())); + if (pblockindex->pnext) + result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex())); + strReply = write_string(Value(result), false) + "\n"; + nStatus = HTTP_OK; + return true; +} + +// GET /rest/tx/{txid}[.hex] +static bool HandleTx(const string& param, const string& format, string& strReply, int& nStatus) +{ + LOCK(cs_main); + uint256 hash(param); + CTransaction tx; + uint256 hashBlock = 0; + if (!GetTransaction(hash, tx, hashBlock)) { + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Transaction not found"); + return true; + } + if (format == "hex") { + CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); + ssTx << tx; + strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n"; + } else { + Object obj; + obj.push_back(Pair("txid", tx.GetHash().GetHex())); + TxToJSON(tx, hashBlock, obj); + strReply = write_string(Value(obj), false) + "\n"; + } + nStatus = HTTP_OK; + return true; +} + +// GET /rest/blockhashbyheight/{n} +static bool HandleBlockHashByHeight(const string& param, string& strReply, int& nStatus) +{ + LOCK(cs_main); + int nHeight = atoi(param.c_str()); + if (nHeight < 0 || nHeight > nBestHeight) { + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Block height out of range"); + return true; + } + CBlockIndex* pblockindex = FindBlockByHeight(nHeight); + Object obj; + obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex())); + strReply = write_string(Value(obj), false) + "\n"; + nStatus = HTTP_OK; + return true; +} + +// GET /rest/blockbyheight/{n} +static bool HandleBlockByHeight(const string& param, const string& format, string& strReply, int& nStatus) +{ + LOCK(cs_main); + int nHeight = atoi(param.c_str()); + if (nHeight < 0 || nHeight > nBestHeight) { + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Block height out of range"); + return true; + } + CBlockIndex* pblockindex = FindBlockByHeight(nHeight); + CBlock block; + block.ReadFromDisk(pblockindex, true); + + if (format == "hex") { + CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION); + ssBlock << block; + strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n"; + } else { + Object obj = blockToJSON(block, pblockindex, false); + strReply = write_string(Value(obj), false) + "\n"; + } + nStatus = HTTP_OK; + return true; +} + +// GET /rest/mempool +static bool HandleMempool(string& strReply, int& nStatus) +{ + vector vtxid; + mempool.queryHashes(vtxid); + Array a; + BOOST_FOREACH(const uint256& hash, vtxid) + a.push_back(hash.ToString()); + strReply = write_string(Value(a), false) + "\n"; + nStatus = HTTP_OK; + return true; +} + +// GET /rest/difficulty +static bool HandleDifficulty(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getdifficulty", params, strReply, nStatus); +} + +// GET /rest/supply +static bool HandleSupply(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("gettxoutsetinfo", params, strReply, nStatus); +} + +// GET /rest/staking +static bool HandleStaking(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getstakinginfo", params, strReply, nStatus); +} + +// GET /rest/mining +static bool HandleMining(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getmininginfo", params, strReply, nStatus); +} + +// GET /rest/subsidy +static bool HandleSubsidy(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getsubsidy", params, strReply, nStatus); +} + +// GET /rest/estimatefee +static bool HandleEstimateFee(string& strReply, int& nStatus) +{ + Array params; + params.push_back(6); // default 6 blocks + return CallRPCMethod("estimatefee", params, strReply, nStatus); +} + +// GET /rest/checkpoint +static bool HandleCheckpoint(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getcheckpoint", params, strReply, nStatus); +} + +// GET /rest/network +static bool HandleNetwork(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getnetworkinfo", params, strReply, nStatus); +} + +// GET /rest/peers +static bool HandlePeers(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getpeerinfo", params, strReply, nStatus); +} + +// GET /rest/validate/{address} +static bool HandleValidate(const string& addr, string& strReply, int& nStatus) +{ + Array params; + params.push_back(addr); + return CallRPCMethod("validateaddress", params, strReply, nStatus); +} + +// GET /rest/address/{addr}/balance +static bool HandleAddressBalance(const string& addr, string& strReply, int& nStatus) +{ + if (!fAddressIndex) { + nStatus = 503; + strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1"); + return true; + } + Object addrObj; + Array addrArray; + addrArray.push_back(addr); + addrObj.push_back(Pair("addresses", addrArray)); + Array params; + params.push_back(addrObj); + return CallRPCMethod("getaddressbalance", params, strReply, nStatus); +} + +// GET /rest/address/{addr}/utxos +static bool HandleAddressUtxos(const string& addr, string& strReply, int& nStatus) +{ + if (!fAddressIndex) { + nStatus = 503; + strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1"); + return true; + } + Object addrObj; + Array addrArray; + addrArray.push_back(addr); + addrObj.push_back(Pair("addresses", addrArray)); + Array params; + params.push_back(addrObj); + return CallRPCMethod("getaddressutxos", params, strReply, nStatus); +} + +// GET /rest/address/{addr}/txids[?start=N&end=N] +static bool HandleAddressTxids(const string& addr, const map& queryParams, + string& strReply, int& nStatus) +{ + if (!fAddressIndex) { + nStatus = 503; + strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1"); + return true; + } + Object addrObj; + Array addrArray; + addrArray.push_back(addr); + addrObj.push_back(Pair("addresses", addrArray)); + + map::const_iterator itStart = queryParams.find("start"); + map::const_iterator itEnd = queryParams.find("end"); + if (itStart != queryParams.end()) + addrObj.push_back(Pair("start", atoi(itStart->second.c_str()))); + if (itEnd != queryParams.end()) + addrObj.push_back(Pair("end", atoi(itEnd->second.c_str()))); + + Array params; + params.push_back(addrObj); + return CallRPCMethod("getaddresstxids", params, strReply, nStatus); +} + +// POST /rest/tx/decode body: {"hex":"..."} +static bool HandleTxDecode(const string& strBody, string& strReply, int& nStatus) +{ + Value valBody; + if (!read_string(strBody, valBody) || valBody.type() != obj_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}"); + return true; + } + Object bodyObj = valBody.get_obj(); + Value hexVal = find_value(bodyObj, "hex"); + if (hexVal.type() != str_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Missing 'hex' field in request body"); + return true; + } + Array params; + params.push_back(hexVal.get_str()); + return CallRPCMethod("decoderawtransaction", params, strReply, nStatus); +} + +// POST /rest/tx/send body: {"hex":"..."} +static bool HandleTxSend(const string& strBody, string& strReply, int& nStatus) +{ + Value valBody; + if (!read_string(strBody, valBody) || valBody.type() != obj_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}"); + return true; + } + Object bodyObj = valBody.get_obj(); + Value hexVal = find_value(bodyObj, "hex"); + if (hexVal.type() != str_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Missing 'hex' field in request body"); + return true; + } + Array params; + params.push_back(hexVal.get_str()); + return CallRPCMethod("sendrawtransaction", params, strReply, nStatus); +} + +// ============================================================================ +// Wallet endpoint handlers (authenticated) +// ============================================================================ + +// GET /rest/wallet/info +static bool HandleWalletInfo(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getwalletinfo", params, strReply, nStatus); +} + +// GET /rest/wallet/balance +static bool HandleWalletBalance(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getbalance", params, strReply, nStatus); +} + +// GET /rest/wallet/transactions[?count=N&skip=N] +static bool HandleWalletTransactions(const map& queryParams, + string& strReply, int& nStatus) +{ + Array params; + params.push_back("*"); // all accounts + + map::const_iterator itCount = queryParams.find("count"); + map::const_iterator itSkip = queryParams.find("skip"); + + int nCount = 10; + int nSkip = 0; + if (itCount != queryParams.end()) + nCount = atoi(itCount->second.c_str()); + if (itSkip != queryParams.end()) + nSkip = atoi(itSkip->second.c_str()); + + params.push_back(nCount); + params.push_back(nSkip); + return CallRPCMethod("listtransactions", params, strReply, nStatus); +} + +// GET /rest/wallet/transaction/{txid} +static bool HandleWalletTransaction(const string& txid, string& strReply, int& nStatus) +{ + Array params; + params.push_back(txid); + return CallRPCMethod("gettransaction", params, strReply, nStatus); +} + +// GET /rest/wallet/unspent[?minconf=N&maxconf=N] +static bool HandleWalletUnspent(const map& queryParams, + string& strReply, int& nStatus) +{ + Array params; + map::const_iterator itMin = queryParams.find("minconf"); + map::const_iterator itMax = queryParams.find("maxconf"); + + params.push_back(itMin != queryParams.end() ? atoi(itMin->second.c_str()) : 1); + params.push_back(itMax != queryParams.end() ? atoi(itMax->second.c_str()) : 9999999); + return CallRPCMethod("listunspent", params, strReply, nStatus); +} + +// GET /rest/wallet/addresses +static bool HandleWalletAddresses(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("listaddressgroupings", params, strReply, nStatus); +} + +// GET /rest/wallet/staking +static bool HandleWalletStaking(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("getstakinginfo", params, strReply, nStatus); +} + +// POST /rest/wallet/address/new body: {} or {"account":"..."} +static bool HandleWalletNewAddress(const string& strBody, string& strReply, int& nStatus) +{ + Array params; + if (!strBody.empty()) { + Value valBody; + if (read_string(strBody, valBody) && valBody.type() == obj_type) { + Value acctVal = find_value(valBody.get_obj(), "account"); + if (acctVal.type() == str_type) + params.push_back(acctVal.get_str()); + } + } + return CallRPCMethod("getnewaddress", params, strReply, nStatus); +} + +// POST /rest/wallet/send body: {"address":"...", "amount":N} +static bool HandleWalletSend(const string& strBody, string& strReply, int& nStatus) +{ + Value valBody; + if (!read_string(strBody, valBody) || valBody.type() != obj_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Invalid JSON body. Expected: {\"address\":\"...\", \"amount\":N}"); + return true; + } + Object bodyObj = valBody.get_obj(); + Value addrVal = find_value(bodyObj, "address"); + Value amtVal = find_value(bodyObj, "amount"); + if (addrVal.type() != str_type || (amtVal.type() != real_type && amtVal.type() != int_type)) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Missing 'address' (string) or 'amount' (number) in request body"); + return true; + } + Array params; + params.push_back(addrVal.get_str()); + params.push_back(amtVal); + + // Optional comment fields + Value commentVal = find_value(bodyObj, "comment"); + Value commentToVal = find_value(bodyObj, "comment_to"); + if (commentVal.type() == str_type) + params.push_back(commentVal.get_str()); + else + params.push_back(""); + if (commentToVal.type() == str_type) + params.push_back(commentToVal.get_str()); + + return CallRPCMethod("sendtoaddress", params, strReply, nStatus); +} + +// POST /rest/wallet/sendmany body: {"recipients":{"addr":amount,...}} +static bool HandleWalletSendMany(const string& strBody, string& strReply, int& nStatus) +{ + Value valBody; + if (!read_string(strBody, valBody) || valBody.type() != obj_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Invalid JSON body. Expected: {\"recipients\":{\"addr\":amount,...}}"); + return true; + } + Object bodyObj = valBody.get_obj(); + Value recipVal = find_value(bodyObj, "recipients"); + if (recipVal.type() != obj_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Missing 'recipients' object in request body"); + return true; + } + Array params; + params.push_back(""); // fromaccount (default) + params.push_back(recipVal); // {addr: amount, ...} + return CallRPCMethod("sendmany", params, strReply, nStatus); +} + +// POST /rest/wallet/unlock body: {"passphrase":"...", "timeout":N, "staking_only":bool} +static bool HandleWalletUnlock(const string& strBody, string& strReply, int& nStatus) +{ + Value valBody; + if (!read_string(strBody, valBody) || valBody.type() != obj_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Invalid JSON body. Expected: {\"passphrase\":\"...\", \"timeout\":N}"); + return true; + } + Object bodyObj = valBody.get_obj(); + Value passVal = find_value(bodyObj, "passphrase"); + Value timeVal = find_value(bodyObj, "timeout"); + if (passVal.type() != str_type || timeVal.type() != int_type) { + nStatus = HTTP_BAD_REQUEST; + strReply = RESTError("Missing 'passphrase' (string) or 'timeout' (integer) in request body"); + return true; + } + Array params; + params.push_back(passVal.get_str()); + params.push_back(timeVal.get_int()); + + Value stakingVal = find_value(bodyObj, "staking_only"); + if (stakingVal.type() == bool_type) + params.push_back(stakingVal.get_bool()); + + return CallRPCMethod("walletpassphrase", params, strReply, nStatus); +} + +// POST /rest/wallet/lock +static bool HandleWalletLock(string& strReply, int& nStatus) +{ + Array params; + return CallRPCMethod("walletlock", params, strReply, nStatus); +} + +// ============================================================================ +// Main router +// ============================================================================ + +bool HandleRESTRequest(const string& strMethod, + const string& strURI, + const string& strBody, + map& mapHeaders, + string& strReply, + string& strContentType, + int& nStatus) +{ + strContentType = "application/json"; + + // OPTIONS: CORS preflight + if (strMethod == "OPTIONS") { + nStatus = 204; + strReply = ""; + return true; + } + + // Parse path + vector parts; + map queryParams; + ParseRESTPath(strURI, parts, queryParams); + // parts: ["", "rest", "resource", "param", ...] + + if (parts.size() < 3) { + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Not found"); + return true; + } + + string resource = parts[2]; + + // Detect format suffix (.hex, .json) + string format = "json"; + string lastPart = parts.size() > 3 ? parts[parts.size() - 1] : ""; + size_t dotPos = lastPart.rfind('.'); + string param; + if (dotPos != string::npos) { + param = lastPart.substr(0, dotPos); + format = lastPart.substr(dotPos + 1); + } else { + param = lastPart; + } + + if (format == "hex") + strContentType = "text/plain"; + + // ---- Wallet endpoints (authenticated) ---- + if (resource == "wallet") { + if (!RESTAuthorized(mapHeaders)) { + nStatus = HTTP_UNAUTHORIZED; + strReply = RESTError("Authentication required for wallet endpoints"); + return true; + } + + if (parts.size() < 4) { + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Unknown wallet endpoint"); + return true; + } + + string walletResource = parts[3]; + + if (strMethod == "GET") { + if (walletResource == "info") + return HandleWalletInfo(strReply, nStatus); + if (walletResource == "balance") + return HandleWalletBalance(strReply, nStatus); + if (walletResource == "transactions") + return HandleWalletTransactions(queryParams, strReply, nStatus); + if (walletResource == "transaction" && parts.size() > 4) + return HandleWalletTransaction(parts[4], strReply, nStatus); + if (walletResource == "unspent") + return HandleWalletUnspent(queryParams, strReply, nStatus); + if (walletResource == "addresses") + return HandleWalletAddresses(strReply, nStatus); + if (walletResource == "staking") + return HandleWalletStaking(strReply, nStatus); + } + else if (strMethod == "POST") { + if (walletResource == "address" && parts.size() > 4 && parts[4] == "new") + return HandleWalletNewAddress(strBody, strReply, nStatus); + if (walletResource == "send") + return HandleWalletSend(strBody, strReply, nStatus); + if (walletResource == "sendmany") + return HandleWalletSendMany(strBody, strReply, nStatus); + if (walletResource == "unlock") + return HandleWalletUnlock(strBody, strReply, nStatus); + if (walletResource == "lock") + return HandleWalletLock(strReply, nStatus); + } + + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Unknown wallet endpoint"); + return true; + } + + // ---- Public GET endpoints ---- + if (strMethod == "GET") { + if (resource == "chaininfo") + return HandleChainInfo(strReply, nStatus); + if (resource == "block" && !param.empty()) + return HandleBlock(param, format, strReply, nStatus); + if (resource == "blockheader" && !param.empty()) + return HandleBlockHeader(param, strReply, nStatus); + if (resource == "tx") { + // /rest/tx/decode and /rest/tx/send are POST-only + if (!param.empty()) + return HandleTx(param, format, strReply, nStatus); + } + if (resource == "blockhashbyheight" && !param.empty()) + return HandleBlockHashByHeight(param, strReply, nStatus); + if (resource == "blockbyheight" && !param.empty()) + return HandleBlockByHeight(param, format, strReply, nStatus); + if (resource == "mempool") + return HandleMempool(strReply, nStatus); + if (resource == "difficulty") + return HandleDifficulty(strReply, nStatus); + if (resource == "supply") + return HandleSupply(strReply, nStatus); + if (resource == "staking") + return HandleStaking(strReply, nStatus); + if (resource == "mining") + return HandleMining(strReply, nStatus); + if (resource == "subsidy") + return HandleSubsidy(strReply, nStatus); + if (resource == "estimatefee") + return HandleEstimateFee(strReply, nStatus); + if (resource == "checkpoint") + return HandleCheckpoint(strReply, nStatus); + if (resource == "network") + return HandleNetwork(strReply, nStatus); + if (resource == "peers") + return HandlePeers(strReply, nStatus); + if (resource == "validate" && !param.empty()) + return HandleValidate(param, strReply, nStatus); + + // Address endpoints: /rest/address/{addr}/balance etc. + if (resource == "address" && parts.size() >= 5) { + string addr = parts[3]; + string addrAction = parts[4]; + if (addrAction == "balance") + return HandleAddressBalance(addr, strReply, nStatus); + if (addrAction == "utxos") + return HandleAddressUtxos(addr, strReply, nStatus); + if (addrAction == "txids") + return HandleAddressTxids(addr, queryParams, strReply, nStatus); + } + } + + // ---- Public POST endpoints ---- + if (strMethod == "POST") { + if (resource == "tx" && parts.size() >= 4) { + string txAction = parts[3]; + if (txAction == "decode") + return HandleTxDecode(strBody, strReply, nStatus); + if (txAction == "send") + return HandleTxSend(strBody, strReply, nStatus); + } + } + + nStatus = HTTP_NOT_FOUND; + strReply = RESTError("Unknown REST endpoint"); + return true; +} diff --git a/src/rest.h b/src/rest.h new file mode 100644 index 0000000..f5626d8 --- /dev/null +++ b/src/rest.h @@ -0,0 +1,35 @@ +// Copyright (c) 2024 The Triangles developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef TRIANGLES_REST_H +#define TRIANGLES_REST_H + +#include +#include + +class CBlock; +class CBlockIndex; +class CTransaction; + +// REST API response builder with CORS headers +std::string HTTPReplyREST(int nStatus, const std::string& strMsg, + const std::string& contentType = "application/json"); + +// Main REST request router +// Returns true if the URI was handled as a REST request, false if it should fall through to RPC +bool HandleRESTRequest(const std::string& strMethod, + const std::string& strURI, + const std::string& strBody, + std::map& mapHeaders, + std::string& strReply, + std::string& strContentType, + int& nStatus); + +// Check if a URI is a REST API path +bool IsRESTPath(const std::string& strURI); + +// Check rate limit for an IP address. Returns true if allowed. +bool CheckRESTRateLimit(const std::string& strIP); + +#endif // TRIANGLES_REST_H diff --git a/src/trianglesrpc.cpp b/src/trianglesrpc.cpp index c351999..3b2c4af 100644 --- a/src/trianglesrpc.cpp +++ b/src/trianglesrpc.cpp @@ -1020,187 +1020,10 @@ static string JSONRPCExecBatch(const Array& vReq) return write_string(Value(ret), false) + "\n"; } -// REST API support -extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail); -extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry); +// REST API support (implementation in rest.cpp) +#include "rest.h" -static string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType = "application/json") -{ - const char *cStatus; - if (nStatus == HTTP_OK) cStatus = "OK"; - else if (nStatus == HTTP_BAD_REQUEST) cStatus = "Bad Request"; - else if (nStatus == HTTP_NOT_FOUND) cStatus = "Not Found"; - else if (nStatus == HTTP_INTERNAL_SERVER_ERROR) cStatus = "Internal Server Error"; - else cStatus = ""; - return strprintf( - "HTTP/1.1 %d %s\r\n" - "Date: %s\r\n" - "Connection: close\r\n" - "Content-Length: %"PRIszu"\r\n" - "Content-Type: %s\r\n" - "Access-Control-Allow-Origin: *\r\n" - "Server: Triangles-json-rpc/%s\r\n" - "\r\n" - "%s", - nStatus, - cStatus, - rfc1123Time().c_str(), - strMsg.size(), - contentType.c_str(), - FormatFullVersion().c_str(), - strMsg.c_str()); -} - -static bool HandleRESTRequest(const string& strURI, string& strReply, string& strContentType, int& nStatus) -{ - // Parse: /rest/[/][.format] - vector parts; - string uri = strURI; - // Remove query string if present - size_t qpos = uri.find('?'); - if (qpos != string::npos) uri = uri.substr(0, qpos); - - boost::split(parts, uri, boost::is_any_of("/")); - // parts[0]="" parts[1]="rest" parts[2]="resource" parts[3]="param.format" - if (parts.size() < 3) { - nStatus = HTTP_NOT_FOUND; - strReply = "{\"error\":\"Not found\"}"; - return true; - } - - string resource = parts[2]; - - // Get param and format from last path component - string lastPart = parts.size() > 3 ? parts[parts.size()-1] : ""; - string param, format = "json"; - size_t dotPos = lastPart.rfind('.'); - if (dotPos != string::npos) { - param = lastPart.substr(0, dotPos); - format = lastPart.substr(dotPos+1); - } else { - param = lastPart; - } - - strContentType = (format == "hex") ? "text/plain" : "application/json"; - nStatus = HTTP_OK; - - try { - LOCK(cs_main); - - if (resource == "chaininfo") { - Object obj, diff; - obj.push_back(Pair("chain", fTestNet ? string("test") : string("main"))); - obj.push_back(Pair("blocks", (int)nBestHeight)); - obj.push_back(Pair("bestblockhash", hashBestChain.GetHex())); - diff.push_back(Pair("proof-of-work", GetDifficulty())); - diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true)))); - obj.push_back(Pair("difficulty", diff)); - obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply))); - strReply = write_string(Value(obj), false) + "\n"; - } - else if (resource == "block" && !param.empty()) { - uint256 hash(param); - if (mapBlockIndex.count(hash) == 0) { - nStatus = HTTP_NOT_FOUND; - strReply = "{\"error\":\"Block not found\"}"; - return true; - } - CBlock block; - CBlockIndex* pblockindex = mapBlockIndex[hash]; - block.ReadFromDisk(pblockindex, true); - - if (format == "hex") { - CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION); - ssBlock << block; - strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n"; - } else { - Object obj = blockToJSON(block, pblockindex, false); - strReply = write_string(Value(obj), false) + "\n"; - } - } - else if (resource == "blockheader" && !param.empty()) { - uint256 hash(param); - if (mapBlockIndex.count(hash) == 0) { - nStatus = HTTP_NOT_FOUND; - strReply = "{\"error\":\"Block not found\"}"; - return true; - } - CBlockIndex* pblockindex = mapBlockIndex[hash]; - Object result; - result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex())); - result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1)); - result.push_back(Pair("height", pblockindex->nHeight)); - result.push_back(Pair("version", pblockindex->nVersion)); - result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex())); - result.push_back(Pair("time", (boost::int64_t)pblockindex->GetBlockTime())); - result.push_back(Pair("nonce", (boost::uint64_t)pblockindex->nNonce)); - result.push_back(Pair("bits", HexBits(pblockindex->nBits))); - result.push_back(Pair("difficulty", GetDifficulty(pblockindex))); - result.push_back(Pair("flags", strprintf("%s%s", - pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work", - pblockindex->GeneratedStakeModifier() ? " stake-modifier" : ""))); - if (pblockindex->pprev) - result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex())); - if (pblockindex->pnext) - result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex())); - strReply = write_string(Value(result), false) + "\n"; - } - else if (resource == "tx" && !param.empty()) { - uint256 hash(param); - CTransaction tx; - uint256 hashBlock = 0; - if (!GetTransaction(hash, tx, hashBlock)) - { - nStatus = HTTP_NOT_FOUND; - strReply = "{\"error\":\"Transaction not found\"}"; - return true; - } - if (format == "hex") { - CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); - ssTx << tx; - strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n"; - } else { - Object obj; - obj.push_back(Pair("txid", tx.GetHash().GetHex())); - TxToJSON(tx, hashBlock, obj); - strReply = write_string(Value(obj), false) + "\n"; - } - } - else if (resource == "blockhashbyheight" && !param.empty()) { - int nHeight = atoi(param.c_str()); - if (nHeight < 0 || nHeight > nBestHeight) { - nStatus = HTTP_NOT_FOUND; - strReply = "{\"error\":\"Block height out of range\"}"; - return true; - } - CBlockIndex* pblockindex = FindBlockByHeight(nHeight); - Object obj; - obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex())); - strReply = write_string(Value(obj), false) + "\n"; - } - else if (resource == "mempool") { - vector vtxid; - mempool.queryHashes(vtxid); - Array a; - BOOST_FOREACH(const uint256& hash, vtxid) - a.push_back(hash.ToString()); - strReply = write_string(Value(a), false) + "\n"; - } - else { - nStatus = HTTP_NOT_FOUND; - strReply = "{\"error\":\"Unknown REST endpoint\"}"; - } - } - catch (std::exception& e) { - nStatus = HTTP_INTERNAL_SERVER_ERROR; - strReply = strprintf("{\"error\":\"%s\"}", e.what()); - } - catch (...) { - nStatus = HTTP_INTERNAL_SERVER_ERROR; - strReply = "{\"error\":\"Internal server error\"}"; - } - return true; -} +// Old HandleRESTRequest removed - now in rest.cpp /** * Handle SSE (Server-Sent Events) stream connection. @@ -1294,20 +1117,32 @@ void ThreadRPCServer3(void* parg) ReadHTTP(conn->stream(), mapHeaders, strRequest); - // Handle REST API requests (unauthenticated, read-only) + // Handle REST API requests string strHTTPMethod = mapHeaders.count("_method") ? mapHeaders["_method"] : "POST"; string strURI = mapHeaders.count("_uri") ? mapHeaders["_uri"] : "/"; - if (strHTTPMethod == "GET" && strURI.substr(0, 6) == "/rest/") + if (IsRESTPath(strURI) || (strHTTPMethod == "OPTIONS" && IsRESTPath(strURI))) { if (!GetBoolArg("-rest", false)) { conn->stream() << HTTPReplyREST(HTTP_FORBIDDEN, "{\"error\":\"REST API not enabled. Start with -rest=1\"}") << std::flush; break; } + + // Rate limit public (non-wallet) endpoints + if (strURI.find("/rest/wallet/") == string::npos) + { + string strPeerIP = conn->peer_address_to_string(); + if (!CheckRESTRateLimit(strPeerIP)) + { + conn->stream() << HTTPReplyREST(429, "{\"error\":\"Rate limit exceeded. Try again later.\"}") << std::flush; + break; + } + } + string strReply, strContentType; int nRESTStatus; - HandleRESTRequest(strURI, strReply, strContentType, nRESTStatus); + HandleRESTRequest(strHTTPMethod, strURI, strRequest, mapHeaders, strReply, strContentType, nRESTStatus); conn->stream() << HTTPReplyREST(nRESTStatus, strReply, strContentType) << std::flush; break; } diff --git a/src/version.h b/src/version.h index 2d01522..4cdc630 100644 --- a/src/version.h +++ b/src/version.h @@ -30,7 +30,7 @@ static const int DATABASE_VERSION = 70509; // network protocol versioning // -static const int PROTOCOL_VERSION = 70205; +static const int PROTOCOL_VERSION = 70206; // v5 hard fork: require new protocol version (disconnects old nodes) static const int MIN_PROTO_VERSION = 70205; @@ -53,7 +53,7 @@ static const int MEMPOOL_GD_VERSION = 60002; #define DISPLAY_VERSION_MAJOR 5 #define DISPLAY_VERSION_MINOR 1 -#define DISPLAY_VERSION_REVISION 5 +#define DISPLAY_VERSION_REVISION 6 #define DISPLAY_VERSION_BUILD 0 #endif diff --git a/triangles-qt.pro b/triangles-qt.pro index 903c383..5465886 100644 --- a/triangles-qt.pro +++ b/triangles-qt.pro @@ -261,6 +261,7 @@ HEADERS += src/qt/trianglesgui.h \ src/qt/transactionview.h \ src/qt/walletmodel.h \ src/trianglesrpc.h \ + src/rest.h \ src/qt/overviewpage.h \ src/qt/csvmodelwriter.h \ src/crypter.h \ @@ -351,6 +352,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \ src/qt/transactionview.cpp \ src/qt/walletmodel.cpp \ src/trianglesrpc.cpp \ + src/rest.cpp \ src/rpcdump.cpp \ src/rpcnet.cpp \ src/rpcmining.cpp \