forked from bitcoin/bitcoin
-
Notifications
You must be signed in to change notification settings - Fork 160
mining: Add HTTP notify support for DATUM Gateway #307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pdath
wants to merge
2
commits into
bitcoinknots:29.x-knots
Choose a base branch
from
pdath:blocknotify-http
base: 29.x-knots
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| // 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 <netbase.h> | ||
| #include <util/sock.h> | ||
|
|
||
| #include <charconv> | ||
| #include <cstdint> | ||
| #include <cstring> | ||
| #include <optional> | ||
| #include <string> | ||
|
|
||
| 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: "; | ||
| if (host.find(':') != std::string::npos && host.find(']') == std::string::npos) { | ||
| request += "[" + host + "]"; | ||
| } else { | ||
| request += 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; | ||
| } | ||
|
|
||
| struct AddrInfoDeleter { | ||
| void operator()(addrinfo* p) const { freeaddrinfo(p); } | ||
| }; | ||
| std::unique_ptr<addrinfo, AddrInfoDeleter> ai_guard{ai_result}; | ||
|
|
||
| // Build HTTP GET request (outside loop — same for all candidates) | ||
| const std::string request = BuildHttpGetRequest(host, port, path); | ||
|
|
||
| // Try each address candidate in order until one connects | ||
| for (addrinfo* ai = ai_result; ai != nullptr; ai = ai->ai_next) { | ||
| // Create a socket for this address family; will be closed on scope exit | ||
| std::unique_ptr<Sock> sock = CreateSock(ai->ai_family, ai->ai_socktype, ai->ai_protocol); | ||
| if (!sock) { | ||
| LogWarning("httpnotify: failed to create socket for '%s:%d'", host, port); | ||
| continue; | ||
| } | ||
|
|
||
| // Use non-blocking connect to enforce timeout via Wait() | ||
| if (!sock->SetNonBlocking()) { | ||
| LogWarning("httpnotify: SetNonBlocking failed for '%s:%d'", host, port); | ||
| continue; | ||
| } | ||
|
|
||
| // Attempt connect | ||
| if (sock->Connect(ai->ai_addr, static_cast<socklen_t>(ai->ai_addrlen)) == SOCKET_ERROR) { | ||
| const int err = WSAGetLastError(); | ||
| if (err == WSAEINPROGRESS || err == WSAEWOULDBLOCK || err == WSAEINVAL) { | ||
| // Async connect in progress; wait up to nConnectTimeout ms | ||
| Sock::Event occurred{0}; | ||
| if (!sock->Wait(std::chrono::milliseconds{nConnectTimeout}, Sock::RECV | Sock::SEND, &occurred)) { | ||
| LogWarning("httpnotify: wait for connect failed for '%s:%d'", host, port); | ||
| continue; | ||
| } | ||
| if (occurred == 0) { | ||
| LogWarning("httpnotify: connect timed out for '%s:%d'", host, port); | ||
| continue; | ||
| } | ||
| // Check SO_ERROR to confirm actual connection success | ||
| int sockerr{0}; | ||
| socklen_t sockerr_len{sizeof(sockerr)}; | ||
| if (sock->GetSockOpt(SOL_SOCKET, SO_ERROR, &sockerr, &sockerr_len) == SOCKET_ERROR) { | ||
| LogWarning("httpnotify: getsockopt SO_ERROR failed for '%s:%d'", host, port); | ||
| continue; | ||
| } | ||
| if (sockerr != 0) { | ||
| LogWarning("httpnotify: connect failed after wait for '%s:%d': %s", host, port, NetworkErrorString(sockerr)); | ||
| continue; | ||
| } | ||
| } else { | ||
| #ifdef WIN32 | ||
| if (err != WSAEISCONN) { | ||
| #endif | ||
| LogWarning("httpnotify: connect() failed for '%s:%d': %s", host, port, NetworkErrorString(err)); | ||
| continue; | ||
| #ifdef WIN32 | ||
| } | ||
| #endif | ||
| } | ||
| } | ||
|
|
||
| // Connected; send the HTTP GET request | ||
| const ssize_t sent = sock->Send( | ||
| request.data(), request.size(), | ||
| #ifndef WIN32 | ||
| MSG_NOSIGNAL | ||
| #else | ||
| 0 | ||
| #endif | ||
| ); | ||
| if (sent < 0 || static_cast<size_t>(sent) != request.size()) { | ||
| LogWarning("httpnotify: send failed for '%s:%d'", host, port); | ||
| } | ||
| return; // Socket closed via RAII, addrinfo freed via ai_guard | ||
| } | ||
|
|
||
| // All address candidates failed | ||
| LogWarning("httpnotify: all candidates failed for '%s:%d'", host, port); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // 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; //!< Hostname or IP address (IPv6 without brackets) | ||
| uint16_t port; //!< TCP port (1–65535; defaults to 80 when omitted from URL) | ||
| std::string path; //!< Request path including leading '/'; defaults to "/" | ||
| }; | ||
|
|
||
| /** | ||
| * Parse an HTTP URL into its host, port, and path components. | ||
| * | ||
| * Accepts only the "http://" scheme. IPv6 addresses must be in | ||
| * bracket notation (e.g. [::1]). Port defaults to 80; path defaults to "/". | ||
| * | ||
| * @param[in] url The URL string to parse (e.g. "http://localhost:7152/NOTIFY"). | ||
| * @return A populated ParsedUrl on success, or std::nullopt if the URL | ||
| * is malformed, uses a non-http scheme, or has an out-of-range port. | ||
| */ | ||
| std::optional<ParsedUrl> ParseHttpUrl(const std::string& url); | ||
|
|
||
| /** | ||
| * Build an HTTP/1.1 GET request string. | ||
| * | ||
| * Produces a minimal request with a request line, Host header (port included | ||
| * only when non-80), Connection: close, and the blank line terminator. | ||
| * | ||
| * @param[in] host Hostname or IP for the Host header. | ||
| * @param[in] port TCP port; omitted from the Host header when equal to 80. | ||
| * @param[in] path Request path (must start with '/'). | ||
| * @return The complete HTTP/1.1 GET request as a std::string. | ||
| */ | ||
| std::string BuildHttpGetRequest(const std::string& host, uint16_t port, const std::string& path); | ||
|
|
||
| /** | ||
| * Perform a fire-and-forget HTTP GET notification to the given URL. | ||
| * | ||
| * Parses the URL, resolves DNS, iterates the addrinfo list attempting a | ||
| * non-blocking connect bounded by nConnectTimeout milliseconds, sends the | ||
| * request on the first successful connection, and closes the socket. | ||
| * All errors are logged at Warning level and silently ignored; no retry. | ||
| * | ||
| * @param[in] url The target URL (must use the "http://" scheme). | ||
| */ | ||
| void HttpNotify(const std::string& url); | ||
|
|
||
| #endif // BITCOIN_HTTPNOTIFY_H |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.