Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/man/bitcoind.1
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ indexes are enabled (currently just basic).
\fB\-blocknotify=\fR<cmd>
.IP
Execute command when the best block changes (%s in cmd is replaced by
block hash)
block hash). If cmd starts with http:// a native HTTP GET request is
sent instead of invoking a shell command.
.HP
\fB\-blockreconstructionextratxn=\fR<n>
.IP
Expand Down
3 changes: 2 additions & 1 deletion share/examples/bitcoin.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
#blockfilterindex=<type>

# Execute command when the best block changes (%s in cmd is replaced by
# block hash)
# block hash). If cmd starts with http:// a native HTTP GET request is
# sent instead of invoking a shell command.
#blocknotify=<cmd>

# Extra transactions to keep in memory for compact block reconstructions
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ add_library(bitcoin_common STATIC EXCLUDE_FROM_ALL
core_write.cpp
deploymentinfo.cpp
external_signer.cpp
httpnotify.cpp
init/common.cpp
kernel/chainparams.cpp
key.cpp
Expand Down
11 changes: 9 additions & 2 deletions src/common/system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <common/system.h>

#include <httpnotify.h>
#include <logging.h>
#include <util/string.h>
#include <util/time.h>
Expand Down Expand Up @@ -45,10 +46,16 @@ std::string ShellEscape(const std::string& arg)
}
#endif

#if HAVE_SYSTEM
void runCommand(const std::string& strCommand)
{
if (strCommand.empty()) return;

if (strCommand.starts_with("http://")) {
Comment thread
pdath marked this conversation as resolved.
HttpNotify(strCommand);
return;
}

#if HAVE_SYSTEM
#ifndef WIN32
int nErr = ::system(strCommand.c_str());
#else
Expand All @@ -57,8 +64,8 @@ void runCommand(const std::string& strCommand)
if (nErr) {
LogWarning("runCommand error: system(%s) returned %d", strCommand, nErr);
}
}
#endif
}

void SetupEnvironment()
{
Expand Down
205 changes: 205 additions & 0 deletions src/httpnotify.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <httpnotify.h>

#include <compat/compat.h>
#include <logging.h>
#include <util/sock.h>

#include <charconv>
#include <cstdint>
#include <cstring>
#include <optional>
#include <string>

static constexpr int HTTP_NOTIFY_TIMEOUT_SECS = 5;

std::optional<ParsedUrl> ParseHttpUrl(const std::string& url)
{
const std::string prefix = "http://";
if (url.substr(0, prefix.size()) != prefix) {
LogWarning("httpnotify: URL does not start with http:// scheme: %s", url);
return std::nullopt;
}

std::string remainder = url.substr(prefix.size());

std::string host;
uint16_t port = 80;
std::string path = "/";

// Handle IPv6 bracket notation: http://[::1]:port/path
if (!remainder.empty() && remainder[0] == '[') {
size_t bracket_close = remainder.find(']');
if (bracket_close == std::string::npos) {
LogWarning("httpnotify: malformed IPv6 address (missing closing bracket): %s", url);
return std::nullopt;
}
host = remainder.substr(1, bracket_close - 1);
remainder = remainder.substr(bracket_close + 1);

// After the closing bracket, expect either ':', '/', or end
if (!remainder.empty()) {
if (remainder[0] == ':') {
remainder = remainder.substr(1);
// Extract port
size_t slash_pos = remainder.find('/');
std::string port_str = remainder.substr(0, slash_pos);
if (port_str.empty()) {
LogWarning("httpnotify: empty port value in URL: %s", url);
return std::nullopt;
}
uint32_t port_val{0};
auto [ptr, ec] = std::from_chars(port_str.data(), port_str.data() + port_str.size(), port_val);
if (ec != std::errc() || ptr != port_str.data() + port_str.size()) {
LogWarning("httpnotify: non-numeric port in URL: %s", url);
return std::nullopt;
}
if (port_val < 1 || port_val > 65535) {
LogWarning("httpnotify: port out of range (1-65535) in URL: %s", url);
return std::nullopt;
}
port = static_cast<uint16_t>(port_val);
if (slash_pos != std::string::npos) {
path = remainder.substr(slash_pos);
}
} else if (remainder[0] == '/') {
path = remainder;
} else {
LogWarning("httpnotify: unexpected character after IPv6 address: %s", url);
return std::nullopt;
}
}
} else {
// Non-IPv6: extract host, optional port, optional path
size_t colon_pos = remainder.find(':');
size_t slash_pos = remainder.find('/');

if (colon_pos != std::string::npos && (slash_pos == std::string::npos || colon_pos < slash_pos)) {
// Host:port case
host = remainder.substr(0, colon_pos);
std::string after_colon = remainder.substr(colon_pos + 1);
size_t path_pos = after_colon.find('/');
std::string port_str = after_colon.substr(0, path_pos);
if (port_str.empty()) {
LogWarning("httpnotify: empty port value in URL: %s", url);
return std::nullopt;
}
uint32_t port_val{0};
auto [ptr, ec] = std::from_chars(port_str.data(), port_str.data() + port_str.size(), port_val);
if (ec != std::errc() || ptr != port_str.data() + port_str.size()) {
LogWarning("httpnotify: non-numeric port in URL: %s", url);
return std::nullopt;
}
if (port_val < 1 || port_val > 65535) {
LogWarning("httpnotify: port out of range (1-65535) in URL: %s", url);
return std::nullopt;
}
port = static_cast<uint16_t>(port_val);
if (path_pos != std::string::npos) {
path = after_colon.substr(path_pos);
}
} else if (slash_pos != std::string::npos) {
// Host/path case (no port)
host = remainder.substr(0, slash_pos);
path = remainder.substr(slash_pos);
} else {
// Host only
host = remainder;
}
}

if (host.empty()) {
LogWarning("httpnotify: missing host in URL: %s", url);
return std::nullopt;
}

return ParsedUrl{host, port, path};
}

std::string BuildHttpGetRequest(const std::string& host, uint16_t port, const std::string& path)
{
std::string request = "GET " + path + " HTTP/1.1\r\n";
request += "Host: " + host;
if (port != 80) {
request += ":" + std::to_string(port);
}
request += "\r\n";
request += "Connection: close\r\n";
request += "\r\n";
return request;
}

void HttpNotify(const std::string& url)
{
auto parsed = ParseHttpUrl(url);
if (!parsed) {
return;
}

const std::string& host = parsed->host;
uint16_t port = parsed->port;
const std::string& path = parsed->path;

// DNS resolution
struct addrinfo hints;
std::memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

struct addrinfo* ai_result = nullptr;
std::string port_str = std::to_string(port);
int gai_err = getaddrinfo(host.c_str(), port_str.c_str(), &hints, &ai_result);
if (gai_err != 0) {
LogWarning("httpnotify: DNS resolution failed for host '%s': %s", host, gai_strerror(gai_err));
return;
}

// Create TCP socket
SOCKET sock = socket(ai_result->ai_family, ai_result->ai_socktype, ai_result->ai_protocol);
Comment thread
pdath marked this conversation as resolved.
Outdated
if (sock == INVALID_SOCKET) {
LogWarning("httpnotify: failed to create socket for '%s:%d': %s", host, port, NetworkErrorString(WSAGetLastError()));
freeaddrinfo(ai_result);
return;
}

// Set SO_SNDTIMEO for connect and send timeout
#ifdef WIN32
DWORD timeout_ms = HTTP_NOTIFY_TIMEOUT_SECS * 1000;
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (sockopt_arg_type)&timeout_ms, sizeof(timeout_ms));
#else
struct timeval timeout;
timeout.tv_sec = HTTP_NOTIFY_TIMEOUT_SECS;
timeout.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (sockopt_arg_type)&timeout, sizeof(timeout));
#endif

// Connect
if (connect(sock, ai_result->ai_addr, ai_result->ai_addrlen) == SOCKET_ERROR) {
Comment thread
pdath marked this conversation as resolved.
Outdated
LogWarning("httpnotify: connection failed to '%s:%d': %s", host, port, NetworkErrorString(WSAGetLastError()));
#ifdef WIN32
closesocket(sock);
#else
close(sock);
#endif
freeaddrinfo(ai_result);
return;
}

freeaddrinfo(ai_result);

// Build and send HTTP GET request
std::string request = BuildHttpGetRequest(host, port, path);
ssize_t sent = send(sock, request.c_str(), request.size(), MSG_NOSIGNAL);
if (sent == SOCKET_ERROR || static_cast<size_t>(sent) != request.size()) {
LogWarning("httpnotify: send failed to '%s:%d': %s", host, port, NetworkErrorString(WSAGetLastError()));
}

// Close socket
#ifdef WIN32
closesocket(sock);
#else
close(sock);
#endif
}
29 changes: 29 additions & 0 deletions src/httpnotify.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_HTTPNOTIFY_H
#define BITCOIN_HTTPNOTIFY_H

#include <cstdint>
#include <optional>
#include <string>

struct ParsedUrl {
std::string host;
uint16_t port;
std::string path;
};

/** Parse an http:// URL into host, port, and path components.
* Returns std::nullopt if the URL is malformed. */
std::optional<ParsedUrl> ParseHttpUrl(const std::string& url);

/** Build an HTTP/1.1 GET request string for the given host, port, and path. */
std::string BuildHttpGetRequest(const std::string& host, uint16_t port, const std::string& path);

/** Perform a single-shot HTTP GET notification to the given URL.
* Parses the URL, connects via raw socket, sends the request, and closes.
* All errors are logged and silently ignored (no retry). */
void HttpNotify(const std::string& url);

#endif // BITCOIN_HTTPNOTIFY_H
8 changes: 4 additions & 4 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,9 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-fastprune", "Use smaller block files and lower minimum prune height for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
#if HAVE_SYSTEM
argsman.AddArg("-blocknotify=<cmd>", "Execute command when the best block changes (%s in cmd is replaced by block hash)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-blocknotify=<cmd>", "Execute command when the best block changes (%s in cmd is replaced by block hash). Supports http:// URLs for native HTTP GET notifications.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
Comment thread
pdath marked this conversation as resolved.
Outdated
#else
argsman.AddArg("-blocknotify=<cmd>", "If it starts with http:// then will perform an HTTP GET request. (%s in URL is replaced by block hash)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#endif
argsman.AddArg("-blockreconstructionextratxn=<n>", strprintf("Extra transactions to keep in memory for compact block reconstructions (default: %u)", DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-blockreconstructionextratxnsize=<n>",
Expand Down Expand Up @@ -2240,7 +2242,6 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
}
}

#if HAVE_SYSTEM
if (args.IsArgSet("-blocknotify")) {
auto blocknotify_commands = args.GetArgs("-blocknotify");
uiInterface.NotifyBlockTip_connect([blocknotify_commands](SynchronizationState sync_state, const CBlockIndex* pBlockIndex) {
Expand All @@ -2250,11 +2251,10 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
ReplaceAll(command, "%s", blockhash_hex);

std::thread t(runCommand, command);
Comment thread
pdath marked this conversation as resolved.
t.detach(); // thread runs free
t.detach();
}
});
}
#endif

std::vector<fs::path> vImportFiles;
for (const std::string& strFile : args.GetArgs("-loadblock")) {
Expand Down
1 change: 1 addition & 0 deletions src/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ add_executable(test_bitcoin
getarg_tests.cpp
hash_tests.cpp
headers_sync_chainwork_tests.cpp
httpnotify_tests.cpp
httpserver_tests.cpp
i2p_tests.cpp
interfaces_tests.cpp
Expand Down
Loading