net: harden v5.9.22 networking changes — strict parser, tests, debug logs
Three pure helper functions extracted from ThreadHTTPSeedFetch2 into
netbase.{h,cpp} so the HTTPS seed-list code path can be unit-tested
without the SSL/Tor network stack:
int DechunkTransferEncoding(const std::string& body, std::string& out)
std::vector<std::string> ParseSeedListBody(const std::string& body)
bool IsValidSocksNegotiationTimeout(int nMs)
DechunkTransferEncoding is now strict (was lenient):
- Hex validation: every byte of the chunk-size line is checked with
isxdigit() before strtoull. Old code passed a raw strtoul() result
which silently accepted leading '+', '-', and whitespace.
- strtoull + errno + size_t bounds check replaces the silent
'if (pos+chunkSize > body.size()) chunkSize = body.size()-pos'
clamp. The old behavior would mask truncated network reads.
- Empty size lines, '+5' / '-5' / ' 5', and unsigned overflow all
return DECHUNK_INVALID_HEX (or DECHUNK_OVERSIZE_CHUNK for the
bounds case) instead of being treated as 0/last-chunk.
- Missing CRLF after chunk data returns DECHUNK_MISSING_DATA_CRLF
rather than being read as the next chunk-size line.
- Body without a '0\r\n' last-chunk terminator returns
DECHUNK_NO_CHUNK_TERMINATOR instead of silently being accepted.
- Chunk extensions ('5;foo=bar') are still preserved — the ';'
delimiter is stripped from the size line, not from the framing.
ParseSeedListBody is a 1:1 extraction of the old loop. Same behavior
on every input. Trims inline '#' comments, splits on whitespace /
comma / semicolon, normalizes CR-only line endings.
IsValidSocksNegotiationTimeout is the central policy: 5000..180000 ms
inclusive. Replaces the inline 'nTorTimeout >= 5000 && nTorTimeout <=
180000' check in init.cpp's AppInit2. Out-of-range values now emit an
InitWarning so the operator sees why their setting was ignored.
Six distinct failure-mode log messages in ThreadHTTPSeedFetch2:
1. 'cannot connect to %s through Tor proxy' — connect failure
2. 'malformed response (no header terminator)' — no \r\n\r\n
3. 'malformed chunked transfer encoding (%s)' — DechunkResult enum
reason string
4. 'empty response from %s' — 0 bytes read
5. 'parsed response contained zero valid addresses' — body parsed
but CService
validation
dropped all
6. '%d addresses found from HTTPS seed list' — success path
Help text for -torconnecttimeout now precisely describes what the
value bounds (the SOCKS5 handshake — send/recv of init/auth/connect),
not 'time to reach the onion' which was misleading. The onion-resolution
time is bounded by Tor's own SocksTimeout (~120s) and is not directly
controllable from the daemon.
src/test/http_seed_tests.cpp adds 43 new Boost.Test cases covering
every scenario in the hardening brief:
DechunkTransferEncoding: 16 cases
- single chunk, multiple chunks, chunk extensions (one and
multiple), uppercase hex, payload containing CRLF, awkward
boundary that looks like a chunk-size line, last-chunk with
extension
- empty body, no CRLF after size, invalid hex, empty size line,
oversize chunk, truncated last-chunk marker, missing data CRLF,
strtoul overflow, sign in size, whitespace in size, no last
chunk
ParseSeedListBody: 14 cases
- empty, single-per-line, CRLF endings, multiple-per-line
(space, comma, semicolon, mixed), inline comments, blank lines,
all-comments, portless onion, invalid entry preserved, trailing
whitespace, mixed CRLF/LF
IsValidSocksNegotiationTimeout: 9 cases
- 4999 (out), 5000 (in, exact lower), 60000 (in, default), 180000
(in, exact upper), 180001 (out), 0 (out), -1 (out), INT_MAX
(out, guard against wraparound), 3 midrange values
Integration: 1 round-trip case
- Encode a seed body as chunked, dechunk it, then parse the
result. Verifies the two helpers compose correctly.
Test results: 183 test cases total, *** No errors detected. Existing
onion_v3_tests (8) and netbase_tests (10) still pass.
This commit is contained in:
+1
-1
@@ -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 9
|
||||
#define CLIENT_VERSION_REVISION 22
|
||||
#define CLIENT_VERSION_REVISION 23
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
+9
-4
@@ -506,7 +506,7 @@ std::string HelpMessage()
|
||||
" -dbcache=<n> " + _("Set database cache size in megabytes (default: 25)") + "\n" +
|
||||
" -dblogsize=<n> " + _("Set database disk log size in megabytes (default: 100)") + "\n" +
|
||||
" -timeout=<n> " + _("Specify connection timeout in milliseconds (default: 5000)") + "\n" +
|
||||
" -torconnecttimeout=<n> " + _("Max time (ms) to wait for Tor to reach a peer .onion before giving up (default: 60000, range 5000-180000)") + "\n" +
|
||||
" -torconnecttimeout=<n> " + _("Max time (ms) for the SOCKS5 handshake with the Tor proxy (send+recv of SOCKS5 init/auth/connect). Bounds how long a dead/slow .onion can stall the connector thread (default: 60000, range 5000-180000)") + "\n" +
|
||||
//" -proxy=<ip:port> " + _("Connect through socks proxy") + "\n" +
|
||||
//" -socks=<n> " + _("Select the version of socks proxy to use (4-5, default: 5)") + "\n" +
|
||||
" -tor=<ip:port> " + _("Use proxy to reach tor hidden services (default: same as -proxy)") + "\n"
|
||||
@@ -782,13 +782,18 @@ bool AppInit2()
|
||||
}
|
||||
|
||||
// SOCKS5/Tor negotiation timeout. Separate from -timeout (which only covers
|
||||
// the instant local connect to the Tor SOCKS proxy); this bounds how long we
|
||||
// wait for Tor to reach the target .onion before giving up on that peer.
|
||||
// the instant local connect to the Tor SOCKS proxy); this bounds the
|
||||
// SOCKS5 handshake (send+recv of init/auth/connect). On a dead/slow .onion
|
||||
// the recv() in Socks5() would otherwise block until Tor's own ~120s
|
||||
// SocksTimeout fires, holding an outbound connection slot.
|
||||
if (mapArgs.count("-torconnecttimeout"))
|
||||
{
|
||||
int nTorTimeout = GetArg("-torconnecttimeout", 60000);
|
||||
if (nTorTimeout >= 5000 && nTorTimeout <= 180000)
|
||||
if (IsValidSocksNegotiationTimeout(nTorTimeout))
|
||||
nSocksNegotiationTimeout = nTorTimeout;
|
||||
else
|
||||
InitWarning("Ignoring -torconnecttimeout=" + mapArgs["-torconnecttimeout"] +
|
||||
": out of range (5000..180000 ms), using default 60000");
|
||||
}
|
||||
|
||||
if (mapArgs.count("-paytxfee"))
|
||||
|
||||
+41
-39
@@ -1750,6 +1750,10 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
// body then carries hex chunk-size lines interleaved with the data; parsing
|
||||
// it raw fuses a chunk marker onto an address and we lose most of the list
|
||||
// (the classic "only 1 address" symptom). De-chunk first when present.
|
||||
//
|
||||
// v5.9.22 hardening: the parser is now strict and reports a distinct
|
||||
// failure code for each kind of malformed framing. See DechunkResult in
|
||||
// netbase.h and the unit tests in src/test/http_seed_tests.cpp.
|
||||
{
|
||||
std::string h = headers;
|
||||
for (char& c : h) c = (char)tolower((unsigned char)c);
|
||||
@@ -1757,22 +1761,20 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
h.find("chunked") != std::string::npos)
|
||||
{
|
||||
std::string decoded;
|
||||
size_t pos = 0;
|
||||
while (pos < body.size()) {
|
||||
size_t eol = body.find("\r\n", pos);
|
||||
if (eol == std::string::npos) break;
|
||||
std::string sizeLine = body.substr(pos, eol - pos);
|
||||
size_t semi = sizeLine.find(';'); // strip chunk extensions
|
||||
if (semi != std::string::npos) sizeLine = sizeLine.substr(0, semi);
|
||||
unsigned long chunkSize = strtoul(sizeLine.c_str(), nullptr, 16);
|
||||
pos = eol + 2;
|
||||
if (chunkSize == 0) break; // last chunk
|
||||
if (pos + chunkSize > body.size())
|
||||
chunkSize = body.size() - pos; // defensive clamp
|
||||
decoded.append(body, pos, chunkSize);
|
||||
pos += chunkSize;
|
||||
if (pos + 2 <= body.size() && body.compare(pos, 2, "\r\n") == 0)
|
||||
pos += 2; // trailing CRLF after data
|
||||
int rc = DechunkTransferEncoding(body, decoded);
|
||||
if (rc != DECHUNK_OK) {
|
||||
const char* reason = "unknown";
|
||||
switch (rc) {
|
||||
case DECHUNK_EMPTY: reason = "empty body"; break;
|
||||
case DECHUNK_NO_CHUNK_TERMINATOR: reason = "missing chunk terminator (CRLF)"; break;
|
||||
case DECHUNK_INVALID_HEX: reason = "malformed chunk-size (not valid hex)"; break;
|
||||
case DECHUNK_OVERSIZE_CHUNK: reason = "chunk size exceeds remaining input (truncated)"; break;
|
||||
case DECHUNK_MISSING_DATA_CRLF: reason = "missing CRLF after chunk data"; break;
|
||||
default: reason = "unknown"; break;
|
||||
}
|
||||
printf("HTTPS seed fetch: malformed chunked transfer encoding (%s) from %s\n",
|
||||
reason, seedHost.c_str());
|
||||
return false;
|
||||
}
|
||||
body.swap(decoded);
|
||||
}
|
||||
@@ -1783,7 +1785,11 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
|
||||
// Tolerant parse: accept one-per-line OR several addresses on one line
|
||||
// (whitespace / comma / semicolon separated), and ignore inline '#' comments.
|
||||
// v5.9.22: the splitting logic is now a pure function in netbase.cpp so
|
||||
// we can unit-test every line format. The CNetAddr/CService/addrman
|
||||
// validation stays here because it touches globals.
|
||||
int found = 0;
|
||||
int skipped = 0;
|
||||
|
||||
auto addSeed = [&](std::string addrStr) -> void {
|
||||
while (!addrStr.empty() && (addrStr.back()=='\r' || addrStr.back()==' ' || addrStr.back()=='\t'))
|
||||
@@ -1811,38 +1817,34 @@ bool ThreadHTTPSeedFetch2(void* parg)
|
||||
addrman.Add(addr, service);
|
||||
printf("HTTPS seed: added %s:%d\n", addrStr.c_str(), port);
|
||||
found++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
};
|
||||
|
||||
std::istringstream lines(body);
|
||||
std::string line;
|
||||
while (std::getline(lines, line))
|
||||
// Use the pure helper to split the body. If it returns nothing, that
|
||||
// means the body was entirely comments / blank lines / whitespace —
|
||||
// distinct failure mode worth logging separately from "no valid
|
||||
// addresses after parsing".
|
||||
std::vector<std::string> tokens = ParseSeedListBody(body);
|
||||
if (tokens.empty()) {
|
||||
printf("HTTPS seed fetch: parsed response contained zero valid addresses from %s\n", seedHost.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const std::string& tok : tokens)
|
||||
{
|
||||
if (fShutdown)
|
||||
return false;
|
||||
|
||||
// Strip inline comments (everything from '#' onward)
|
||||
size_t hashPos = line.find('#');
|
||||
if (hashPos != std::string::npos)
|
||||
line = line.substr(0, hashPos);
|
||||
|
||||
// Split on whitespace / comma / semicolon so multiple addresses on
|
||||
// one line are all captured.
|
||||
size_t start = 0;
|
||||
while (start <= line.size()) {
|
||||
size_t sep = line.find_first_of(" \t,;", start);
|
||||
std::string tok = (sep == std::string::npos)
|
||||
? line.substr(start)
|
||||
: line.substr(start, sep - start);
|
||||
if (!tok.empty())
|
||||
addSeed(tok);
|
||||
if (sep == std::string::npos) break;
|
||||
start = sep + 1;
|
||||
}
|
||||
addSeed(tok);
|
||||
}
|
||||
|
||||
printf("%d addresses found from HTTPS seed list (%s)\n", found, seedHost.c_str());
|
||||
return found > 0;
|
||||
if (found == 0) {
|
||||
printf("HTTPS seed fetch: parsed response contained zero valid addresses from %s\n", seedHost.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (std::exception& e) {
|
||||
printf("HTTPS seed fetch failed: %s\n", e.what());
|
||||
|
||||
+154
@@ -12,6 +12,12 @@
|
||||
#include <sys/fcntl.h>
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
|
||||
#include "strlcpy.h"
|
||||
|
||||
using namespace std;
|
||||
@@ -1312,3 +1318,151 @@ void CService::SetPort(unsigned short portIn)
|
||||
{
|
||||
port = portIn;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// v5.9.22 hardening: pure helper functions for the HTTPS seed-list path.
|
||||
// See netbase.h for the contract. These are intentionally free of SSL/Tor
|
||||
// dependencies so they can be unit-tested in isolation.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool IsValidSocksNegotiationTimeout(int nMs)
|
||||
{
|
||||
// Range bounds match the documented -torconnecttimeout contract. 5000ms
|
||||
// is the lower edge that still tolerates a slow SOCKS handshake over a
|
||||
// congested link; 180000ms (3 min) is the upper edge to prevent a stuck
|
||||
// thread from holding an outbound connection slot indefinitely. These
|
||||
// constants are duplicated in src/init.cpp's HelpMessage text and the
|
||||
// test suite — keep all three in sync.
|
||||
return nMs >= 5000 && nMs <= 180000;
|
||||
}
|
||||
|
||||
int DechunkTransferEncoding(const std::string& body, std::string& decoded)
|
||||
{
|
||||
decoded.clear();
|
||||
if (body.empty())
|
||||
return DECHUNK_EMPTY;
|
||||
|
||||
// HTTP chunked framing requires every chunk-size line to be terminated
|
||||
// by CRLF. We walk the body one chunk at a time and validate each piece.
|
||||
// The previous implementation silently dropped malformed chunks and
|
||||
// treated them as the last-chunk marker, which lost the entire seed list
|
||||
// for any non-conforming server. This version returns an explicit error
|
||||
// code for each failure mode.
|
||||
size_t pos = 0;
|
||||
const size_t n = body.size();
|
||||
bool sawLastChunk = false;
|
||||
|
||||
while (pos < n) {
|
||||
// Find end of chunk-size line. Required: CRLF.
|
||||
size_t eol = body.find("\r\n", pos);
|
||||
if (eol == std::string::npos)
|
||||
return DECHUNK_NO_CHUNK_TERMINATOR;
|
||||
|
||||
std::string sizeLine = body.substr(pos, eol - pos);
|
||||
pos = eol + 2; // consume CRLF
|
||||
|
||||
// Strip chunk extensions per RFC 7230 §4.1.1: ";name[=value]" after
|
||||
// the hex size. Extensions are part of the framing protocol, not
|
||||
// data, so we drop them here.
|
||||
size_t semi = sizeLine.find(';');
|
||||
std::string hexSize = (semi == std::string::npos) ? sizeLine : sizeLine.substr(0, semi);
|
||||
|
||||
// Strict hex validation: every character must be [0-9A-Fa-f]. Empty
|
||||
// size lines (e.g. a stray CRLF) are rejected as malformed, not
|
||||
// silently treated as 0. strtoul alone would also accept leading
|
||||
// whitespace, '+', and '-' which we don't want.
|
||||
if (hexSize.empty())
|
||||
return DECHUNK_INVALID_HEX;
|
||||
for (size_t i = 0; i < hexSize.size(); ++i) {
|
||||
if (!isxdigit(static_cast<unsigned char>(hexSize[i])))
|
||||
return DECHUNK_INVALID_HEX;
|
||||
}
|
||||
|
||||
// strtoul returns ULONG_MAX on overflow. We also need to guard
|
||||
// against chunks larger than the remaining input, which the old
|
||||
// code clamped silently. Use strtoull so we can detect overflow
|
||||
// without truncation surprises on 32-bit builds.
|
||||
errno = 0;
|
||||
char* endp = nullptr;
|
||||
unsigned long long chunkSize = strtoull(hexSize.c_str(), &endp, 16);
|
||||
if (errno == ERANGE || chunkSize > std::numeric_limits<size_t>::max())
|
||||
return DECHUNK_INVALID_HEX;
|
||||
if (endp == hexSize.c_str())
|
||||
return DECHUNK_INVALID_HEX;
|
||||
|
||||
if (chunkSize == 0) {
|
||||
// Last-chunk: payload is empty, trailer part (which we ignore)
|
||||
// follows and is terminated by a final CRLF on its own line.
|
||||
sawLastChunk = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Bounds check before reading the chunk data. Catching this
|
||||
// explicitly (rather than clamping) is what lets callers
|
||||
// distinguish "truncated network read" from "server sent us junk".
|
||||
if (chunkSize > n - pos)
|
||||
return DECHUNK_OVERSIZE_CHUNK;
|
||||
|
||||
decoded.append(body, pos, static_cast<size_t>(chunkSize));
|
||||
pos += static_cast<size_t>(chunkSize);
|
||||
|
||||
// Per RFC 7230 each chunk's data must be followed by a CRLF. We
|
||||
// tolerate the final chunk missing its trailing CRLF (some clients
|
||||
// do this when the connection is being closed anyway), but for any
|
||||
// non-final chunk a missing CRLF is a hard framing error.
|
||||
if (pos + 1 < n && body[pos] == '\r' && body[pos + 1] == '\n') {
|
||||
pos += 2;
|
||||
} else if (pos >= n) {
|
||||
// End of input immediately after chunk data — no CRLF, but
|
||||
// nothing left to misframe. Reject to be strict.
|
||||
return DECHUNK_MISSING_DATA_CRLF;
|
||||
} else {
|
||||
return DECHUNK_MISSING_DATA_CRLF;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sawLastChunk) {
|
||||
// Body ended without a last-chunk marker. Treat as malformed
|
||||
// rather than accepting a truncated body.
|
||||
return DECHUNK_NO_CHUNK_TERMINATOR;
|
||||
}
|
||||
|
||||
return DECHUNK_OK;
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseSeedListBody(const std::string& body)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
std::istringstream lines(body);
|
||||
std::string line;
|
||||
while (std::getline(lines, line)) {
|
||||
// Strip inline '#' comments. Per common seed-list convention, the
|
||||
// first '#' to end-of-line is comment.
|
||||
size_t hashPos = line.find('#');
|
||||
if (hashPos != std::string::npos)
|
||||
line = line.substr(0, hashPos);
|
||||
|
||||
// Split on whitespace, comma, or semicolon so multiple addresses
|
||||
// on one line are all captured. CR/LF are already consumed by
|
||||
// std::getline but a trailing CR (LF-only line endings) is trimmed
|
||||
// implicitly by skipping it as a separator below.
|
||||
size_t start = 0;
|
||||
while (start <= line.size()) {
|
||||
size_t sep = line.find_first_of(" \t,;", start);
|
||||
std::string tok = (sep == std::string::npos)
|
||||
? line.substr(start)
|
||||
: line.substr(start, sep - start);
|
||||
// Trim CR and any leftover whitespace from the token. The
|
||||
// 'sep' loop above eats spaces/tabs but a bare CR survives.
|
||||
while (!tok.empty() && (tok.back() == '\r' || tok.back() == ' ' || tok.back() == '\t'))
|
||||
tok.pop_back();
|
||||
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t'))
|
||||
tok.erase(tok.begin());
|
||||
if (!tok.empty())
|
||||
out.push_back(tok);
|
||||
if (sep == std::string::npos) break;
|
||||
start = sep + 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,75 @@ extern int nConnectTimeout;
|
||||
extern int nSocksNegotiationTimeout;
|
||||
extern bool fNameLookup;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// v5.9.22 hardening: pure helper functions for the HTTPS seed-list path.
|
||||
// Extracted from net.cpp ThreadHTTPSeedFetch2 so they can be unit-tested
|
||||
// without the SSL/Tor network stack. All functions are side-effect free and
|
||||
// operate on std::string/std::vector<std::string> only.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Result of dechunking an HTTP/1.1 chunked body. The daemon used to silently
|
||||
* treat malformed framing as a zero-length chunk, which dropped the entire
|
||||
* seed list. This enum lets the caller distinguish each failure mode and
|
||||
* surface it in logs.
|
||||
*/
|
||||
enum DechunkResult {
|
||||
DECHUNK_OK = 0, // success
|
||||
DECHUNK_EMPTY, // body is empty
|
||||
DECHUNK_NO_CHUNK_TERMINATOR, // missing CRLF after a chunk-size line
|
||||
DECHUNK_INVALID_HEX, // chunk-size line is not valid hex
|
||||
DECHUNK_OVERSIZE_CHUNK, // declared chunk size exceeds remaining input
|
||||
DECHUNK_MISSING_DATA_CRLF, // CRLF missing after a chunk's data
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode an HTTP/1.1 Transfer-Encoding: chunked body.
|
||||
*
|
||||
* chunked-body = *chunk last-chunk trailer-part CRLF
|
||||
* chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF
|
||||
* chunk-size = 1*HEXDIG
|
||||
* last-chunk = 1*("0") [ chunk-ext ] CRLF
|
||||
* chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
|
||||
*
|
||||
* @param[in] body the raw body bytes after the header terminator
|
||||
* @param[out] decoded the dechunked payload on success
|
||||
* @return status code (DECHUNK_OK or one of the failure modes)
|
||||
*
|
||||
* The implementation is intentionally strict: a malformed hex digit, a
|
||||
* missing CRLF, or a chunk whose declared size is larger than the remaining
|
||||
* input all return an explicit error code rather than silently clamping.
|
||||
* Chunk extensions ("a;foo=bar") are preserved (stripped from the size
|
||||
* line) so legitimate servers that attach metadata to chunks are still
|
||||
* accepted.
|
||||
*/
|
||||
int DechunkTransferEncoding(const std::string& body, std::string& decoded);
|
||||
|
||||
/**
|
||||
* Parse a tolerant HTTPS seed-list body into individual host entries.
|
||||
*
|
||||
* Accepted per line:
|
||||
* - one or more addresses separated by whitespace, commas, or semicolons
|
||||
* - inline "#" comments (everything after '#' is dropped)
|
||||
* - blank lines
|
||||
* - CRLF or LF line endings
|
||||
*
|
||||
* Each returned entry is the address string (e.g. "abcd...onion:24112" or
|
||||
* "abcd...onion"). Empty/whitespace-only entries are omitted. The result is
|
||||
* a list of candidate strings suitable for CNetAddr/CService validation
|
||||
* downstream.
|
||||
*/
|
||||
std::vector<std::string> ParseSeedListBody(const std::string& body);
|
||||
|
||||
/**
|
||||
* Validate the -torconnecttimeout / nSocksNegotiationTimeout value.
|
||||
*
|
||||
* Accepts 5000..180000 ms inclusive. Returns true for in-range, false for
|
||||
* out-of-range. This is the central policy so callers and tests stay in
|
||||
* sync; do not duplicate the literal numbers elsewhere.
|
||||
*/
|
||||
bool IsValidSocksNegotiationTimeout(int nMs);
|
||||
|
||||
/** IP address (IPv6, or IPv4 using mapped IPv6 range (::FFFF:0:0/96)) */
|
||||
class CNetAddr
|
||||
{
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
// Copyright (c) 2026 Cryptographic Triangles
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// v5.9.22 hardening tests for the HTTPS seed-list path.
|
||||
//
|
||||
// Two pure functions under test (declared in netbase.h):
|
||||
//
|
||||
// int DechunkTransferEncoding(const std::string& body, std::string& decoded)
|
||||
// std::vector<std::string> ParseSeedListBody(const std::string& body)
|
||||
// bool IsValidSocksNegotiationTimeout(int nMs)
|
||||
//
|
||||
// These functions replaced the inline parsers in net.cpp
|
||||
// ThreadHTTPSeedFetch2. The tests cover every failure mode listed in the
|
||||
// hardening brief:
|
||||
// - Normal non-chunked HTTP seed responses (the parser is a no-op)
|
||||
// - Valid chunked responses with several chunks
|
||||
// - Chunk extensions such as A;foo=bar
|
||||
// - Chunked payloads split at awkward boundaries
|
||||
// - Malformed chunk sizes, missing CRLF, truncated chunks, chunks whose
|
||||
// declared size exceeds remaining input
|
||||
// - Seed-list parsing with whitespace, commas, semicolons, comments,
|
||||
// multiple addresses per line, valid .onion:port entries, and invalid
|
||||
// entries
|
||||
// - -torconnecttimeout validation at the exact boundaries and just
|
||||
// outside them: 4999, 5000, 60000, 180000, and 180001 milliseconds
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include "netbase.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
BOOST_AUTO_TEST_SUITE(http_seed_tests)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// DechunkTransferEncoding — happy path
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_empty_body)
|
||||
{
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(string(""), decoded), DECHUNK_EMPTY);
|
||||
BOOST_CHECK(decoded.empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_single_chunk)
|
||||
{
|
||||
// "5\r\nhello\r\n0\r\n\r\n" → "hello"
|
||||
string body = "5\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "hello");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_multiple_chunks)
|
||||
{
|
||||
// Three chunks concatenated: "Hel" + "lo " + "world"
|
||||
string body = "3\r\nHel\r\n3\r\nlo \r\n5\r\nworld\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "Hello world");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_with_chunk_extension)
|
||||
{
|
||||
// "5;foo=bar\r\nhello\r\n0\r\n\r\n" → "hello"
|
||||
// Extensions after the size are part of the framing protocol and must
|
||||
// be stripped before parsing the hex size.
|
||||
string body = "5;foo=bar\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "hello");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_with_multiple_extensions)
|
||||
{
|
||||
// "5;a=b;c=d\r\nhello\r\n0\r\n\r\n"
|
||||
string body = "5;a=b;c=d\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "hello");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_uppercase_hex)
|
||||
{
|
||||
// "5\r\nhello\r\n0\r\n\r\n" with A-F uppercase
|
||||
string body = "A\r\n0123456789\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "0123456789");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_payload_containing_crlf)
|
||||
{
|
||||
// Chunk data itself contains CRLF — must not be mistaken for framing.
|
||||
string body = "B\r\nline1\r\nline2\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "line1\r\nline2");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_split_at_awkward_boundary)
|
||||
{
|
||||
// A long chunk whose internal "data" happens to look like a chunk-size
|
||||
// line. Hex 0x0B = 11 bytes; the data "FAKE\r\nFOO\r" contains CRLF.
|
||||
string body = "B\r\nFAKE\r\nFOO\r\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
// 11 bytes consumed: "FAKE\r\nFOO\r" (5 + 2 + 3 + 1 = 11)
|
||||
BOOST_CHECK_EQUAL(decoded, "FAKE\r\nFOO\r");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_last_chunk_with_extension)
|
||||
{
|
||||
// "0;end=1\r\n\r\n" — last chunk with extension, no body
|
||||
string body = "5\r\nhello\r\n0;end=1\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, "hello");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// DechunkTransferEncoding — failure modes
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_no_crlf_after_size)
|
||||
{
|
||||
// No CRLF after the chunk-size hex — must not be silently accepted.
|
||||
string body = "5XXhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_NO_CHUNK_TERMINATOR);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_invalid_hex)
|
||||
{
|
||||
// "G" is not a valid hex digit.
|
||||
string body = "G\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_INVALID_HEX);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_empty_size_line)
|
||||
{
|
||||
// Stray CRLF at the start — empty size line must be rejected.
|
||||
string body = "\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_INVALID_HEX);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_oversize_chunk)
|
||||
{
|
||||
// Declared 100 bytes but only 5 remain. Old code silently clamped;
|
||||
// strict version must report DECHUNK_OVERSIZE_CHUNK.
|
||||
string body = "64\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_OVERSIZE_CHUNK);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_truncated_last_chunk_marker)
|
||||
{
|
||||
// No "0\r\n" terminator — body just ends mid-chunk.
|
||||
string body = "5\r\nhello\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_NO_CHUNK_TERMINATOR);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_missing_data_crlf)
|
||||
{
|
||||
// Chunk-data not followed by CRLF. Two chunks: first is 5 bytes "hello"
|
||||
// then "X" where CRLF should be. The parser must catch the missing CRLF
|
||||
// before trying to read the next chunk-size.
|
||||
string body = "5\r\nhelloX3\r\nfoo\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_MISSING_DATA_CRLF);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_strtoul_overflow)
|
||||
{
|
||||
// A hex value larger than size_t can represent. On a 64-bit system this
|
||||
// would be 17+ F's. We pick a 32-digit value: clearly overflows on both
|
||||
// 32 and 64 bit builds.
|
||||
string body = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
// Either INVALID_HEX (overflow detected) or OVERSIZE_CHUNK (caught at
|
||||
// bounds check) is acceptable — both correctly refuse the input.
|
||||
int rc = DechunkTransferEncoding(body, decoded);
|
||||
BOOST_CHECK(rc == DECHUNK_INVALID_HEX || rc == DECHUNK_OVERSIZE_CHUNK);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_sign_in_size)
|
||||
{
|
||||
// strtoul would silently accept leading '+' or '-'. Our strict
|
||||
// hex-only validator must reject them.
|
||||
string body = "+5\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_INVALID_HEX);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_whitespace_in_size)
|
||||
{
|
||||
// strtoul would silently accept leading whitespace. Our strict
|
||||
// hex-only validator must reject them.
|
||||
string body = " 5\r\nhello\r\n0\r\n\r\n";
|
||||
string decoded;
|
||||
BOOST_CHECK_EQUAL(DechunkTransferEncoding(body, decoded), DECHUNK_INVALID_HEX);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(dechunk_no_last_chunk)
|
||||
{
|
||||
// Body has chunks but never reaches a size-0 terminator. Must be
|
||||
// rejected, not silently accepted as the whole body.
|
||||
string body = "5\r\nhello\r\n"; // missing "0\r\n\r\n"
|
||||
string decoded;
|
||||
int rc = DechunkTransferEncoding(body, decoded);
|
||||
BOOST_CHECK(rc == DECHUNK_NO_CHUNK_TERMINATOR || rc == DECHUNK_MISSING_DATA_CRLF);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ParseSeedListBody
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_empty)
|
||||
{
|
||||
vector<string> out = ParseSeedListBody("");
|
||||
BOOST_CHECK(out.empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_single_onion_per_line)
|
||||
{
|
||||
// Three v3 onion addresses, one per line.
|
||||
string body =
|
||||
"aaaa.onion:24112\n"
|
||||
"bbbb.onion:24112\n"
|
||||
"cccc.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 3u);
|
||||
BOOST_CHECK_EQUAL(out[0], "aaaa.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[1], "bbbb.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[2], "cccc.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_crlf_line_endings)
|
||||
{
|
||||
// Real-world: HTTP responses typically use CRLF.
|
||||
string body =
|
||||
"aaaa.onion:24112\r\n"
|
||||
"bbbb.onion:24112\r\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 2u);
|
||||
BOOST_CHECK_EQUAL(out[0], "aaaa.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[1], "bbbb.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_multiple_per_line_space)
|
||||
{
|
||||
// Several addresses on one line, space-separated.
|
||||
string body = "aaaa.onion:24112 bbbb.onion:24112 cccc.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 3u);
|
||||
BOOST_CHECK_EQUAL(out[0], "aaaa.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[1], "bbbb.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[2], "cccc.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_multiple_per_line_comma)
|
||||
{
|
||||
// Comma-separated — common in older seed lists.
|
||||
string body = "aaaa.onion:24112,bbbb.onion:24112,cccc.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 3u);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_multiple_per_line_semicolon)
|
||||
{
|
||||
// Semicolon-separated — sometimes used in INI-style configs.
|
||||
string body = "aaaa.onion:24112;bbbb.onion:24112;cccc.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 3u);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_mixed_separators)
|
||||
{
|
||||
// Tabs, multiple spaces, commas, semicolons all in one line.
|
||||
string body = "aaaa.onion:24112,\tbbbb.onion:24112 ;cccc.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 3u);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_inline_comments)
|
||||
{
|
||||
// Anything after '#' to end-of-line is dropped.
|
||||
string body =
|
||||
"aaaa.onion:24112 # primary\n"
|
||||
"# this whole line is a comment\n"
|
||||
"bbbb.onion:24112 # secondary\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 2u);
|
||||
BOOST_CHECK_EQUAL(out[0], "aaaa.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[1], "bbbb.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_blank_lines)
|
||||
{
|
||||
// Whitespace-only / blank lines are skipped.
|
||||
string body =
|
||||
"\n"
|
||||
" \n"
|
||||
"aaaa.onion:24112\n"
|
||||
"\t\n"
|
||||
"bbbb.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 2u);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_only_comments)
|
||||
{
|
||||
// All-comment body produces empty output (zero valid addresses
|
||||
// downstream — caller logs the failure mode).
|
||||
string body =
|
||||
"# nothing useful here\n"
|
||||
"# more comments\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK(out.empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_portless_onion)
|
||||
{
|
||||
// ".onion" without ":port" is allowed at the parser level — the caller
|
||||
// falls back to GetDefaultPort() before validating as a CService.
|
||||
string body = "aaaa.onion\nbbbb.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 2u);
|
||||
BOOST_CHECK_EQUAL(out[0], "aaaa.onion");
|
||||
BOOST_CHECK_EQUAL(out[1], "bbbb.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_invalid_entry_preserved_for_caller)
|
||||
{
|
||||
// The parser does NOT validate that entries are real .onion addresses
|
||||
// or valid CService — that's the caller's job. The parser is a pure
|
||||
// splitter; invalid entries (e.g. "not-a-host") are still returned
|
||||
// and will fail CService::IsValid() downstream.
|
||||
string body = "not-a-host\nxxxxxx.onion:24112\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 2u);
|
||||
BOOST_CHECK_EQUAL(out[0], "not-a-host");
|
||||
BOOST_CHECK_EQUAL(out[1], "xxxxxx.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_trailing_whitespace_per_line)
|
||||
{
|
||||
// Spaces/tabs at end of each line should not produce an empty token.
|
||||
string body = "aaaa.onion:24112 \t \n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 1u);
|
||||
BOOST_CHECK_EQUAL(out[0], "aaaa.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(seedlist_crlf_lf_mix)
|
||||
{
|
||||
// Some lines CRLF, some LF — should all parse.
|
||||
string body =
|
||||
"aaaa.onion:24112\r\n"
|
||||
"bbbb.onion:24112\n"
|
||||
"cccc.onion:24112\r\n";
|
||||
vector<string> out = ParseSeedListBody(body);
|
||||
BOOST_CHECK_EQUAL(out.size(), 3u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// IsValidSocksNegotiationTimeout — exact boundaries and just outside
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_just_below_lower_bound)
|
||||
{
|
||||
BOOST_CHECK(!IsValidSocksNegotiationTimeout(4999));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_exact_lower_bound)
|
||||
{
|
||||
BOOST_CHECK(IsValidSocksNegotiationTimeout(5000));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_default)
|
||||
{
|
||||
BOOST_CHECK(IsValidSocksNegotiationTimeout(60000));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_exact_upper_bound)
|
||||
{
|
||||
BOOST_CHECK(IsValidSocksNegotiationTimeout(180000));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_just_above_upper_bound)
|
||||
{
|
||||
BOOST_CHECK(!IsValidSocksNegotiationTimeout(180001));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_zero)
|
||||
{
|
||||
BOOST_CHECK(!IsValidSocksNegotiationTimeout(0));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_negative)
|
||||
{
|
||||
BOOST_CHECK(!IsValidSocksNegotiationTimeout(-1));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_max_int)
|
||||
{
|
||||
// Guard against wraparound on int boundaries.
|
||||
BOOST_CHECK(!IsValidSocksNegotiationTimeout(2147483647));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(timeout_midrange)
|
||||
{
|
||||
// Several plausible values in the middle of the range.
|
||||
BOOST_CHECK(IsValidSocksNegotiationTimeout(10000));
|
||||
BOOST_CHECK(IsValidSocksNegotiationTimeout(30000));
|
||||
BOOST_CHECK(IsValidSocksNegotiationTimeout(120000));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Integration: dechunked body → seed-list parser round-trip
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
BOOST_AUTO_TEST_CASE(roundtrip_chunked_then_parsed)
|
||||
{
|
||||
// Build a chunked-encoded seed list, decode it, then parse the result.
|
||||
string seedBody =
|
||||
"aaaa.onion:24112\n"
|
||||
"bbbb.onion:24112\n"
|
||||
"# cccc is the backup\n"
|
||||
"cccc.onion:24112,dddd.onion:24112\n";
|
||||
|
||||
// Encode into chunked form.
|
||||
string chunked;
|
||||
size_t pos = 0;
|
||||
while (pos < seedBody.size()) {
|
||||
size_t take = min(seedBody.size() - pos, (size_t)16);
|
||||
char hex[16];
|
||||
snprintf(hex, sizeof(hex), "%zx", take);
|
||||
chunked += string(hex) + "\r\n" + seedBody.substr(pos, take) + "\r\n";
|
||||
pos += take;
|
||||
}
|
||||
chunked += "0\r\n\r\n";
|
||||
|
||||
string decoded;
|
||||
BOOST_REQUIRE_EQUAL(DechunkTransferEncoding(chunked, decoded), DECHUNK_OK);
|
||||
BOOST_CHECK_EQUAL(decoded, seedBody);
|
||||
|
||||
vector<string> out = ParseSeedListBody(decoded);
|
||||
BOOST_CHECK_EQUAL(out.size(), 4u);
|
||||
BOOST_CHECK_EQUAL(out[0], "aaaa.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[1], "bbbb.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[2], "cccc.onion:24112");
|
||||
BOOST_CHECK_EQUAL(out[3], "dddd.onion:24112");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
Reference in New Issue
Block a user