Merge 27cfaeed1e into merged_master (Bitcoin PR bitcoin/bitcoin#24098)

This commit is contained in:
James Dorfman 2024-07-09 02:27:59 +00:00
commit 18d0ebdd77
11 changed files with 334 additions and 96 deletions

View file

@ -47,18 +47,24 @@ The HTTP request and response are both handled entirely in-memory.
With the /notxdetails/ option JSON response will only contain the transaction hash instead of the complete transaction details. The option only affects the JSON response.
#### Blockheaders
`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
`GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
Given a block hash: returns <COUNT> amount of blockheaders in upward direction.
Returns empty if the block doesn't exist or it isn't in the active chain.
*Deprecated (but not removed) since 24.0:*
`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
#### Blockfilter Headers
`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
`GET /rest/blockfilterheaders/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
Given a block hash: returns <COUNT> amount of blockfilter headers in upward
direction for the filter type <FILTERTYPE>.
Returns empty if the block doesn't exist or it isn't in the active chain.
*Deprecated (but not removed) since 24.0:*
`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>`
#### Blockfilters
`GET /rest/blockfilter/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>`

View file

@ -0,0 +1,22 @@
Notable changes
===============
Updated REST APIs
-----------------
- The `/headers/` and `/blockfilterheaders/` endpoints have been updated to use
a query parameter instead of path parameter to specify the result count. The
count parameter is now optional, and defaults to 5 for both endpoints. The old
endpoints are still functional, and have no documented behaviour change.
For `/headers`, use
`GET /rest/headers/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
instead of
`GET /rest/headers/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` (deprecated)
For `/blockfilterheaders/`, use
`GET /rest/blockfilterheaders/<FILTERTYPE>/<BLOCK-HASH>.<bin|hex|json>?count=<COUNT=5>`
instead of
`GET /rest/blockfilterheaders/<FILTERTYPE>/<COUNT>/<BLOCK-HASH>.<bin|hex|json>` (deprecated)
(#24098)

View file

@ -219,6 +219,7 @@ BITCOIN_CORE_H = \
psbt.h \
random.h \
randomenv.h \
rest.h \
reverse_iterator.h \
rpc/blockchain.h \
rpc/client.h \

View file

@ -96,6 +96,7 @@ BITCOIN_TESTS =\
test/fs_tests.cpp \
test/getarg_tests.cpp \
test/hash_tests.cpp \
test/httpserver_tests.cpp \
test/i2p_tests.cpp \
test/interfaces_tests.cpp \
test/key_io_tests.cpp \
@ -118,6 +119,7 @@ BITCOIN_TESTS =\
test/prevector_tests.cpp \
test/raii_event_tests.cpp \
test/random_tests.cpp \
test/rest_tests.cpp \
test/reverselock_tests.cpp \
test/rpc_tests.cpp \
test/sanity_tests.cpp \

View file

@ -23,6 +23,7 @@
#include <deque>
#include <memory>
#include <optional>
#include <stdio.h>
#include <stdlib.h>
#include <string>
@ -30,11 +31,12 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <event2/thread.h>
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/util.h>
#include <event2/http.h>
#include <event2/keyvalq_struct.h>
#include <event2/thread.h>
#include <event2/util.h>
#include <support/events.h>
@ -639,6 +641,37 @@ HTTPRequest::RequestMethod HTTPRequest::GetRequestMethod() const
}
}
std::optional<std::string> HTTPRequest::GetQueryParameter(const std::string& key) const
{
const char* uri{evhttp_request_get_uri(req)};
return GetQueryParameterFromUri(uri, key);
}
std::optional<std::string> GetQueryParameterFromUri(const char* uri, const std::string& key)
{
evhttp_uri* uri_parsed{evhttp_uri_parse(uri)};
const char* query{evhttp_uri_get_query(uri_parsed)};
std::optional<std::string> result;
if (query) {
// Parse the query string into a key-value queue and iterate over it
struct evkeyvalq params_q;
evhttp_parse_query_str(query, &params_q);
for (struct evkeyval* param{params_q.tqh_first}; param != nullptr; param = param->next.tqe_next) {
if (param->key == key) {
result = param->value;
break;
}
}
evhttp_clear_headers(&params_q);
}
evhttp_uri_free(uri_parsed);
return result;
}
void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler)
{
LogPrint(BCLog::HTTP, "Registering HTTP handler for %s (exactmatch %d)\n", prefix, exactMatch);

View file

@ -5,8 +5,9 @@
#ifndef BITCOIN_HTTPSERVER_H
#define BITCOIN_HTTPSERVER_H
#include <string>
#include <functional>
#include <optional>
#include <string>
static const int DEFAULT_HTTP_THREADS=4;
static const int DEFAULT_HTTP_WORKQUEUE=16;
@ -83,6 +84,17 @@ public:
*/
RequestMethod GetRequestMethod() const;
/** Get the query parameter value from request uri for a specified key, or std::nullopt if the
* key is not found.
*
* If the query string contains duplicate keys, the first value is returned. Many web frameworks
* would instead parse this as an array of values, but this is not (yet) implemented as it is
* currently not needed in any of the endpoints.
*
* @param[in] key represents the query parameter of which the value is returned
*/
std::optional<std::string> GetQueryParameter(const std::string& key) const;
/**
* Get the request header specified by hdr, or an empty string.
* Return a pair (isPresent,string).
@ -115,6 +127,20 @@ public:
void WriteReply(int nStatus, const std::string& strReply = "");
};
/** Get the query parameter value from request uri for a specified key, or std::nullopt if the key
* is not found.
*
* If the query string contains duplicate keys, the first value is returned. Many web frameworks
* would instead parse this as an array of values, but this is not (yet) implemented as it is
* currently not needed in any of the endpoints.
*
* Helper function for HTTPRequest::GetQueryParameter.
*
* @param[in] uri is the entire request uri
* @param[in] key represents the query parameter of which the value is returned
*/
std::optional<std::string> GetQueryParameterFromUri(const char* uri, const std::string& key);
/** Event handler closure.
*/
class HTTPClosure

View file

@ -3,6 +3,8 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <rest.h>
#include <blockfilter.h>
#include <chain.h>
#include <chainparams.h>
@ -28,6 +30,7 @@
#include <version.h>
#include <any>
#include <string>
#include <boost/algorithm/string.hpp>
@ -41,21 +44,14 @@ using node::ReadBlockFromDisk;
static const size_t MAX_GETUTXOS_OUTPOINTS = 15; //allow a max of 15 outpoints to be queried at once
static constexpr unsigned int MAX_REST_HEADERS_RESULTS = 2000;
enum class RetFormat {
UNDEF,
BINARY,
HEX,
JSON,
};
static const struct {
RetFormat rf;
RESTResponseFormat rf;
const char* name;
} rf_names[] = {
{RetFormat::UNDEF, ""},
{RetFormat::BINARY, "bin"},
{RetFormat::HEX, "hex"},
{RetFormat::JSON, "json"},
{RESTResponseFormat::UNDEF, ""},
{RESTResponseFormat::BINARY, "bin"},
{RESTResponseFormat::HEX, "hex"},
{RESTResponseFormat::JSON, "json"},
};
struct CCoin {
@ -138,25 +134,28 @@ static ChainstateManager* GetChainman(const std::any& context, HTTPRequest* req)
return node_context->chainman.get();
}
static RetFormat ParseDataFormat(std::string& param, const std::string& strReq)
RESTResponseFormat ParseDataFormat(std::string& param, const std::string& strReq)
{
const std::string::size_type pos = strReq.rfind('.');
if (pos == std::string::npos)
{
param = strReq;
// Remove query string (if any, separated with '?') as it should not interfere with
// parsing param and data format
param = strReq.substr(0, strReq.rfind('?'));
const std::string::size_type pos_format{param.rfind('.')};
// No format string is found
if (pos_format == std::string::npos) {
return rf_names[0].rf;
}
param = strReq.substr(0, pos);
const std::string suff(strReq, pos + 1);
// Match format string to available formats
const std::string suffix(param, pos_format + 1);
for (const auto& rf_name : rf_names) {
if (suff == rf_name.name)
if (suffix == rf_name.name) {
param.erase(pos_format);
return rf_name.rf;
}
}
/* If no suffix is found, return original string. */
param = strReq;
// If no suffix is found, return RESTResponseFormat::UNDEF and original string without query string
return rf_names[0].rf;
}
@ -192,19 +191,29 @@ static bool rest_headers(const std::any& context,
if (!CheckWarmup(req))
return false;
std::string param;
const RetFormat rf = ParseDataFormat(param, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
std::vector<std::string> path;
boost::split(path, param, boost::is_any_of("/"));
if (path.size() != 2)
return RESTERR(req, HTTP_BAD_REQUEST, "No header count specified. Use /rest/headers/<count>/<hash>.<ext>.");
const auto parsed_count{ToIntegral<size_t>(path[0])};
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, path[0]));
std::string raw_count;
std::string hashStr;
if (path.size() == 2) {
// deprecated path: /rest/headers/<count>/<hash>
hashStr = path[1];
raw_count = path[0];
} else if (path.size() == 1) {
// new path with query parameter: /rest/headers/<hash>?count=<count>
hashStr = path[0];
raw_count = req->GetQueryParameter("count").value_or("5");
} else {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/headers/<hash>.<ext>?count=<count>");
}
const auto parsed_count{ToIntegral<size_t>(raw_count)};
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
}
std::string hashStr = path[1];
uint256 hash;
if (!ParseHashStr(hashStr, hash))
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + hashStr);
@ -230,7 +239,7 @@ static bool rest_headers(const std::any& context,
}
switch (rf) {
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION);
for (const CBlockIndex *pindex : headers) {
if (pindex->trimmed()) {
@ -248,7 +257,7 @@ static bool rest_headers(const std::any& context,
return true;
}
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
CDataStream ssHeader(SER_NETWORK, PROTOCOL_VERSION);
for (const CBlockIndex *pindex : headers) {
if (pindex->trimmed()) {
@ -266,7 +275,7 @@ static bool rest_headers(const std::any& context,
req->WriteReply(HTTP_OK, strHex);
return true;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue jsonHeaders(UniValue::VARR);
for (const CBlockIndex *pindex : headers) {
jsonHeaders.push_back(blockheaderToJSON(tip, pindex));
@ -290,7 +299,7 @@ static bool rest_block(const std::any& context,
if (!CheckWarmup(req))
return false;
std::string hashStr;
const RetFormat rf = ParseDataFormat(hashStr, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);
uint256 hash;
if (!ParseHashStr(hashStr, hash))
@ -318,7 +327,7 @@ static bool rest_block(const std::any& context,
}
switch (rf) {
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
ssBlock << block;
std::string binaryBlock = ssBlock.str();
@ -327,7 +336,7 @@ static bool rest_block(const std::any& context,
return true;
}
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
ssBlock << block;
std::string strHex = HexStr(ssBlock) + "\n";
@ -336,7 +345,7 @@ static bool rest_block(const std::any& context,
return true;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue objBlock = blockToJSON(block, tip, pblockindex, tx_verbosity);
std::string strJSON = objBlock.write() + "\n";
req->WriteHeader("Content-Type", "application/json");
@ -365,17 +374,32 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
if (!CheckWarmup(req)) return false;
std::string param;
const RetFormat rf = ParseDataFormat(param, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
std::vector<std::string> uri_parts;
boost::split(uri_parts, param, boost::is_any_of("/"));
if (uri_parts.size() != 3) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>");
std::string raw_count;
std::string raw_blockhash;
if (uri_parts.size() == 3) {
// deprecated path: /rest/blockfilterheaders/<filtertype>/<count>/<blockhash>
raw_blockhash = uri_parts[2];
raw_count = uri_parts[1];
} else if (uri_parts.size() == 2) {
// new path with query parameter: /rest/blockfilterheaders/<filtertype>/<blockhash>?count=<count>
raw_blockhash = uri_parts[1];
raw_count = req->GetQueryParameter("count").value_or("5");
} else {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<blockhash>.<ext>?count=<count>");
}
const auto parsed_count{ToIntegral<size_t>(raw_count)};
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, raw_count));
}
uint256 block_hash;
if (!ParseHashStr(uri_parts[2], block_hash)) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + uri_parts[2]);
if (!ParseHashStr(raw_blockhash, block_hash)) {
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid hash: " + raw_blockhash);
}
BlockFilterType filtertype;
@ -388,11 +412,6 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
return RESTERR(req, HTTP_BAD_REQUEST, "Index is not enabled for filtertype " + uri_parts[0]);
}
const auto parsed_count{ToIntegral<size_t>(uri_parts[1])};
if (!parsed_count.has_value() || *parsed_count < 1 || *parsed_count > MAX_REST_HEADERS_RESULTS) {
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Header count is invalid or out of acceptable range (1-%u): %s", MAX_REST_HEADERS_RESULTS, uri_parts[1]));
}
std::vector<const CBlockIndex*> headers;
headers.reserve(*parsed_count);
{
@ -431,7 +450,7 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
}
switch (rf) {
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
CDataStream ssHeader{SER_NETWORK, PROTOCOL_VERSION};
for (const uint256& header : filter_headers) {
ssHeader << header;
@ -442,7 +461,7 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
req->WriteReply(HTTP_OK, binaryHeader);
return true;
}
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
CDataStream ssHeader{SER_NETWORK, PROTOCOL_VERSION};
for (const uint256& header : filter_headers) {
ssHeader << header;
@ -453,7 +472,7 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
req->WriteReply(HTTP_OK, strHex);
return true;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue jsonHeaders(UniValue::VARR);
for (const uint256& header : filter_headers) {
jsonHeaders.push_back(header.GetHex());
@ -475,7 +494,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
if (!CheckWarmup(req)) return false;
std::string param;
const RetFormat rf = ParseDataFormat(param, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
// request is sent over URI scheme /rest/blockfilter/filtertype/blockhash
std::vector<std::string> uri_parts;
@ -531,7 +550,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
}
switch (rf) {
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
CDataStream ssResp{SER_NETWORK, PROTOCOL_VERSION};
ssResp << filter;
@ -540,7 +559,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
req->WriteReply(HTTP_OK, binaryResp);
return true;
}
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
CDataStream ssResp{SER_NETWORK, PROTOCOL_VERSION};
ssResp << filter;
@ -549,7 +568,7 @@ static bool rest_block_filter(const std::any& context, HTTPRequest* req, const s
req->WriteReply(HTTP_OK, strHex);
return true;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue ret(UniValue::VOBJ);
ret.pushKV("filter", HexStr(filter.GetEncodedFilter()));
std::string strJSON = ret.write() + "\n";
@ -571,10 +590,10 @@ static bool rest_chaininfo(const std::any& context, HTTPRequest* req, const std:
if (!CheckWarmup(req))
return false;
std::string param;
const RetFormat rf = ParseDataFormat(param, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
switch (rf) {
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
JSONRPCRequest jsonRequest;
jsonRequest.context = context;
jsonRequest.params = UniValue(UniValue::VARR);
@ -597,10 +616,10 @@ static bool rest_mempool_info(const std::any& context, HTTPRequest* req, const s
const CTxMemPool* mempool = GetMemPool(context, req);
if (!mempool) return false;
std::string param;
const RetFormat rf = ParseDataFormat(param, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
switch (rf) {
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue mempoolInfoObject = MempoolInfoToJSON(*mempool);
std::string strJSON = mempoolInfoObject.write() + "\n";
@ -620,10 +639,10 @@ static bool rest_mempool_contents(const std::any& context, HTTPRequest* req, con
const CTxMemPool* mempool = GetMemPool(context, req);
if (!mempool) return false;
std::string param;
const RetFormat rf = ParseDataFormat(param, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
switch (rf) {
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue mempoolObject = MempoolToJSON(*mempool, true);
std::string strJSON = mempoolObject.write() + "\n";
@ -642,7 +661,7 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
if (!CheckWarmup(req))
return false;
std::string hashStr;
const RetFormat rf = ParseDataFormat(hashStr, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(hashStr, strURIPart);
uint256 hash;
if (!ParseHashStr(hashStr, hash))
@ -661,7 +680,7 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
}
switch (rf) {
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
ssTx << tx;
@ -671,7 +690,7 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
return true;
}
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION | RPCSerializationFlags());
ssTx << tx;
@ -681,7 +700,7 @@ static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string
return true;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue objTx(UniValue::VOBJ);
TxToUniv(*tx, /*block_hash=*/hashBlock, /*entry=*/ objTx);
std::string strJSON = objTx.write() + "\n";
@ -701,7 +720,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
if (!CheckWarmup(req))
return false;
std::string param;
const RetFormat rf = ParseDataFormat(param, strURIPart);
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
std::vector<std::string> uriParts;
if (param.length() > 1)
@ -748,14 +767,14 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
}
switch (rf) {
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
// convert hex to bin, continue then with bin part
std::vector<unsigned char> strRequestV = ParseHex(strRequestMutable);
strRequestMutable.assign(strRequestV.begin(), strRequestV.end());
[[fallthrough]];
}
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
try {
//deserialize only if user sent a request
if (strRequestMutable.size() > 0)
@ -775,7 +794,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
break;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
if (!fInputParsed)
return RESTERR(req, HTTP_BAD_REQUEST, "Error: empty request");
break;
@ -829,7 +848,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
}
switch (rf) {
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
// serialize data
// use exact same output as mentioned in Bip64
CDataStream ssGetUTXOResponse(SER_NETWORK, PROTOCOL_VERSION);
@ -841,7 +860,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
return true;
}
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
CDataStream ssGetUTXOResponse(SER_NETWORK, PROTOCOL_VERSION);
ssGetUTXOResponse << chainman.ActiveChain().Height() << chainman.ActiveChain().Tip()->GetBlockHash() << bitmap << outs;
std::string strHex = HexStr(ssGetUTXOResponse) + "\n";
@ -851,7 +870,7 @@ static bool rest_getutxos(const std::any& context, HTTPRequest* req, const std::
return true;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
UniValue objGetUTXOResponse(UniValue::VOBJ);
// pack in some essentials
@ -893,7 +912,7 @@ static bool rest_blockhash_by_height(const std::any& context, HTTPRequest* req,
{
if (!CheckWarmup(req)) return false;
std::string height_str;
const RetFormat rf = ParseDataFormat(height_str, str_uri_part);
const RESTResponseFormat rf = ParseDataFormat(height_str, str_uri_part);
int32_t blockheight = -1; // Initialization done only to prevent valgrind false positive, see https://github.com/bitcoin/bitcoin/pull/18785
if (!ParseInt32(height_str, &blockheight) || blockheight < 0) {
@ -913,19 +932,19 @@ static bool rest_blockhash_by_height(const std::any& context, HTTPRequest* req,
pblockindex = active_chain[blockheight];
}
switch (rf) {
case RetFormat::BINARY: {
case RESTResponseFormat::BINARY: {
CDataStream ss_blockhash(SER_NETWORK, PROTOCOL_VERSION);
ss_blockhash << pblockindex->GetBlockHash();
req->WriteHeader("Content-Type", "application/octet-stream");
req->WriteReply(HTTP_OK, ss_blockhash.str());
return true;
}
case RetFormat::HEX: {
case RESTResponseFormat::HEX: {
req->WriteHeader("Content-Type", "text/plain");
req->WriteReply(HTTP_OK, pblockindex->GetBlockHash().GetHex() + "\n");
return true;
}
case RetFormat::JSON: {
case RESTResponseFormat::JSON: {
req->WriteHeader("Content-Type", "application/json");
UniValue resp = UniValue(UniValue::VOBJ);
resp.pushKV("blockhash", pblockindex->GetBlockHash().GetHex());

28
src/rest.h Normal file
View file

@ -0,0 +1,28 @@
// Copyright (c) 2015-2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_REST_H
#define BITCOIN_REST_H
#include <string>
enum class RESTResponseFormat {
UNDEF,
BINARY,
HEX,
JSON,
};
/**
* Parse a URI to get the data format and URI without data format
* and query string.
*
* @param[out] param The strReq without the data format string and
* without the query string (if any).
* @param[in] strReq The URI to be parsed.
* @return RESTResponseFormat that was parsed from the URI.
*/
RESTResponseFormat ParseDataFormat(std::string& param, const std::string& strReq);
#endif // BITCOIN_REST_H

View file

@ -0,0 +1,38 @@
// Copyright (c) 2012-2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <httpserver.h>
#include <test/util/setup_common.h>
#include <boost/test/unit_test.hpp>
BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
BOOST_AUTO_TEST_CASE(test_query_parameters)
{
std::string uri {};
// No parameters
uri = "localhost:8080/rest/headers/someresource.json";
BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p1").has_value());
// Single parameter
uri = "localhost:8080/rest/endpoint/someresource.json?p1=v1";
BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1");
BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p2").has_value());
// Multiple parameters
uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2";
BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1");
BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p2").value(), "v2");
// If the query string contains duplicate keys, the first value is returned
uri = "/rest/endpoint/someresource.json?p1=v1&p1=v2";
BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1");
// Invalid query string syntax is the same as not having parameters
uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p1").has_value());
}
BOOST_AUTO_TEST_SUITE_END()

48
src/test/rest_tests.cpp Normal file
View file

@ -0,0 +1,48 @@
// Copyright (c) 2012-2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <rest.h>
#include <test/util/setup_common.h>
#include <boost/test/unit_test.hpp>
#include <string>
BOOST_FIXTURE_TEST_SUITE(rest_tests, BasicTestingSetup)
BOOST_AUTO_TEST_CASE(test_query_string)
{
std::string param;
RESTResponseFormat rf;
// No query string
rf = ParseDataFormat(param, "/rest/endpoint/someresource.json");
BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
BOOST_CHECK_EQUAL(rf, RESTResponseFormat::JSON);
// Query string with single parameter
rf = ParseDataFormat(param, "/rest/endpoint/someresource.bin?p1=v1");
BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
BOOST_CHECK_EQUAL(rf, RESTResponseFormat::BINARY);
// Query string with multiple parameters
rf = ParseDataFormat(param, "/rest/endpoint/someresource.hex?p1=v1&p2=v2");
BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
BOOST_CHECK_EQUAL(rf, RESTResponseFormat::HEX);
// Incorrectly formed query string will not be handled
rf = ParseDataFormat(param, "/rest/endpoint/someresource.json&p1=v1");
BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource.json&p1=v1");
BOOST_CHECK_EQUAL(rf, RESTResponseFormat::UNDEF);
// Omitted data format with query string should return UNDEF and hide query string
rf = ParseDataFormat(param, "/rest/endpoint/someresource?p1=v1");
BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
BOOST_CHECK_EQUAL(rf, RESTResponseFormat::UNDEF);
// Data format specified after query string
rf = ParseDataFormat(param, "/rest/endpoint/someresource?p1=v1.json");
BOOST_CHECK_EQUAL(param, "/rest/endpoint/someresource");
BOOST_CHECK_EQUAL(rf, RESTResponseFormat::UNDEF);
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -10,6 +10,7 @@ import http.client
from io import BytesIO
import json
from struct import pack, unpack
import typing
import urllib.parse
@ -57,14 +58,21 @@ class RESTTest (BitcoinTestFramework):
args.append("-whitelist=noban@127.0.0.1")
self.supports_cli = False
def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, body='', status=200, ret_type=RetType.JSON):
def test_rest_request(
self,
uri: str,
http_method: str = 'GET',
req_type: ReqType = ReqType.JSON,
body: str = '',
status: int = 200,
ret_type: RetType = RetType.JSON,
query_params: typing.Dict[str, typing.Any] = None,
) -> typing.Union[http.client.HTTPResponse, bytes, str, None]:
rest_uri = '/rest' + uri
if req_type == ReqType.JSON:
rest_uri += '.json'
elif req_type == ReqType.BIN:
rest_uri += '.bin'
elif req_type == ReqType.HEX:
rest_uri += '.hex'
if req_type in ReqType:
rest_uri += f'.{req_type.name.lower()}'
if query_params:
rest_uri += f'?{urllib.parse.urlencode(query_params)}'
conn = http.client.HTTPConnection(self.url.hostname, self.url.port)
self.log.debug(f'{http_method} {rest_uri} {body}')
@ -83,6 +91,8 @@ class RESTTest (BitcoinTestFramework):
elif ret_type == RetType.JSON:
return json.loads(resp.read().decode('utf-8'), parse_float=Decimal)
return None
def run_test(self):
self.url = urllib.parse.urlparse(self.nodes[0].url)
self.wallet = MiniWallet(self.nodes[0])
@ -216,12 +226,12 @@ class RESTTest (BitcoinTestFramework):
# Each header has a OP_TRUE challenge script
# Check result if block does not exists
assert_equal(self.test_rest_request(f"/headers/1/{UNKNOWN_PARAM}"), [])
assert_equal(self.test_rest_request(f"/headers/{UNKNOWN_PARAM}", query_params={"count": 1}), [])
self.test_rest_request(f"/block/{UNKNOWN_PARAM}", status=404, ret_type=RetType.OBJ)
# Check result if block is not in the active chain
self.nodes[0].invalidateblock(bb_hash)
assert_equal(self.test_rest_request(f'/headers/1/{bb_hash}'), [])
assert_equal(self.test_rest_request(f'/headers/{bb_hash}', query_params={'count': 1}), [])
self.test_rest_request(f'/block/{bb_hash}')
self.nodes[0].reconsiderblock(bb_hash)
@ -231,7 +241,7 @@ class RESTTest (BitcoinTestFramework):
response_bytes = response.read()
# Compare with block header
response_header = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ)
response_header = self.test_rest_request(f"/headers/{bb_hash}", req_type=ReqType.BIN, ret_type=RetType.OBJ, query_params={"count": 1})
assert_equal(int(response_header.getheader('content-length')), 79) # ELEMENTS
response_header_bytes = response_header.read()
assert_equal(response_bytes[:BLOCK_HEADER_SIZE], response_header_bytes)
@ -243,7 +253,7 @@ class RESTTest (BitcoinTestFramework):
assert_equal(response_bytes.hex().encode(), response_hex_bytes)
# Compare with hex block header
response_header_hex = self.test_rest_request(f"/headers/1/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ)
response_header_hex = self.test_rest_request(f"/headers/{bb_hash}", req_type=ReqType.HEX, ret_type=RetType.OBJ, query_params={"count": 1})
assert_greater_than(int(response_header_hex.getheader('content-length')), 75*2) # ELEMENTS
response_header_hex_bytes = response_header_hex.read(79*2) # ELEMENTS
assert_equal(response_bytes[:BLOCK_HEADER_SIZE].hex().encode(), response_header_hex_bytes)
@ -270,7 +280,7 @@ class RESTTest (BitcoinTestFramework):
self.test_rest_request("/blockhashbyheight/", ret_type=RetType.OBJ, status=400)
# Compare with json block header
json_obj = self.test_rest_request(f"/headers/1/{bb_hash}")
json_obj = self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1})
assert_equal(len(json_obj), 1) # ensure that there is one header in the json response
assert_equal(json_obj[0]['hash'], bb_hash) # request/response hash should be the same
@ -281,9 +291,9 @@ class RESTTest (BitcoinTestFramework):
# See if we can get 5 headers in one response
self.generate(self.nodes[1], 5)
json_obj = self.test_rest_request(f"/headers/5/{bb_hash}")
json_obj = self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 5})
assert_equal(len(json_obj), 5) # now we should have 5 header objects
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}")
json_obj = self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 5})
first_filter_header = json_obj[0]
assert_equal(len(json_obj), 5) # now we should have 5 filter header objects
json_obj = self.test_rest_request(f"/blockfilter/basic/{bb_hash}")
@ -297,7 +307,7 @@ class RESTTest (BitcoinTestFramework):
for num in ['5a', '-5', '0', '2001', '99999999999999999999999999999999999']:
assert_equal(
bytes(f'Header count is invalid or out of acceptable range (1-2000): {num}\r\n', 'ascii'),
self.test_rest_request(f"/headers/{num}/{bb_hash}", ret_type=RetType.BYTES, status=400),
self.test_rest_request(f"/headers/{bb_hash}", ret_type=RetType.BYTES, status=400, query_params={"count": num}),
)
self.log.info("Test tx inclusion in the /mempool and /block URIs")
@ -354,6 +364,11 @@ class RESTTest (BitcoinTestFramework):
json_obj = self.test_rest_request("/chaininfo")
assert_equal(json_obj['bestblockhash'], bb_hash)
# Test compatibility of deprecated and newer endpoints
self.log.info("Test compatibility of deprecated and newer endpoints")
assert_equal(self.test_rest_request(f"/headers/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/headers/1/{bb_hash}"))
assert_equal(self.test_rest_request(f"/blockfilterheaders/basic/{bb_hash}", query_params={"count": 1}), self.test_rest_request(f"/blockfilterheaders/basic/5/{bb_hash}"))
if __name__ == '__main__':
RESTTest().main()